Repository: ibis-project/ibis

Issue:
feat(api): support selectors in window function `group_by` and `order_by` arguments
### Is your feature request related to a problem?

Yes, the problem is that I can't use selectors with `over(group_by=..., order_by=...)`.

### What is the motivation behind your request?

I'm trying to use selectors with window function specifications.

### Describe the solution you'd like

I'd like to use selectors with window function specifications.

### What version of ibis are you running?

`main`

### What backend(s) are you using, if any?

DuckDB

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Discussion:

Patch:
diff --git a/ci/check_disallowed_imports.py b/ci/check_disallowed_imports.py
deleted file mode 100755
index 391f6d302..000000000
--- a/ci/check_disallowed_imports.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-
-import collections
-import fnmatch
-import json
-import pathlib
-import subprocess
-import sys
-
-CURRENT_DIR = pathlib.Path(__file__).parent.absolute()
-
-
-def generate_dependency_graph(*args):
-    command = ("pydeps", "--show-deps", *args)
-    print(f"Running: {' '.join(command)}")  # noqa: T201
-    result = subprocess.check_output(command, text=True)  # noqa: S603
-    return json.loads(result)
-
-
-def check_dependency_rules(dependency_graph, disallowed_imports):
-    prohibited_deps = collections.defaultdict(set)
-
-    for module, module_data in dependency_graph.items():
-        imports = module_data.get("imports", [])
-
-        for pattern, disallow_rules in disallowed_imports.items():
-            if fnmatch.fnmatch(module, pattern):
-                for disallow_rule in disallow_rules:
-                    for imported in imports:
-                        if fnmatch.fnmatch(imported, disallow_rule):
-                            prohibited_deps[module].add(imported)
-
-    return prohibited_deps
-
-
-disallowed_imports = {
-    "ibis.expr.*": ["numpy", "pandas"],
-}
-
-
-if __name__ == "__main__":
-    dependency_graph = generate_dependency_graph(*sys.argv[1:])
-    prohibited_deps = check_dependency_rules(dependency_graph, disallowed_imports)
-
-    print("\n")  # noqa: T201
-    print("Prohibited dependencies:")  # noqa: T201
-    print("------------------------")  # noqa: T201
-    for module, deps in prohibited_deps.items():
-        print(f"\n{module}:")  # noqa: T201
-        for dep in deps:
-            print(f"  <= {dep}")  # noqa: T201
-
-    if prohibited_deps:
-        sys.exit(1)
diff --git a/conda/environment-arm64-flink.yml b/conda/environment-arm64-flink.yml
index f7d2cf553..746679da7 100644
--- a/conda/environment-arm64-flink.yml
+++ b/conda/environment-arm64-flink.yml
@@ -87,7 +87,6 @@ dependencies:
   - poetry-plugin-export
   - pre-commit
   - prettier
-  - pydeps >=1.12.7
   - pyinstrument
   - ruff >=0.1.8
   - taplo
diff --git a/conda/environment-arm64.yml b/conda/environment-arm64.yml
index ff3eb085c..ef0b5a58e 100644
--- a/conda/environment-arm64.yml
+++ b/conda/environment-arm64.yml
@@ -86,7 +86,6 @@ dependencies:
   - poetry-plugin-export
   - pre-commit
   - prettier
-  - pydeps >=1.12.7
   - pyinstrument
   - ruff >=0.1.8
   - taplo
diff --git a/conda/environment.yml b/conda/environment.yml
index 4be800d63..2ed9223e3 100644
--- a/conda/environment.yml
+++ b/conda/environment.yml
@@ -90,7 +90,6 @@ dependencies:
   - poetry-plugin-export
   - pre-commit
   - prettier
-  - pydeps >=1.12.7
   - pyinstrument
   - ruff >=0.1.8
   - taplo
diff --git a/ibis/backends/mssql/__init__.py b/ibis/backends/mssql/__init__.py
index a1adff0fb..79a86611b 100644
--- a/ibis/backends/mssql/__init__.py
+++ b/ibis/backends/mssql/__init__.py
@@ -114,8 +114,7 @@ class Backend(SQLBackend, CanCreateCatalog, CanCreateDatabase, CanCreateSchema,
 
         con = pyodbc.connect(
             user=user,
-            server=host,
-            port=port,
+            server=f"{host},{port}",
             password=password,
             database=database,
             driver=driver,
diff --git a/ibis/common/selectors.py b/ibis/common/selectors.py
new file mode 100644
index 000000000..10e1a96f1
--- /dev/null
+++ b/ibis/common/selectors.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+import abc
+from typing import TYPE_CHECKING
+
+from ibis.common.grounds import Concrete
+
+if TYPE_CHECKING:
+    from collections.abc import Sequence
+
+    import ibis.expr.types as ir
+
+
+class Selector(Concrete):
+    """A column selector."""
+
+    @abc.abstractmethod
+    def expand(self, table: ir.Table) -> Sequence[ir.Value]:
+        """Expand `table` into value expressions that match the selector.
+
+        Parameters
+        ----------
+        table
+            An ibis table expression
+
+        Returns
+        -------
+        Sequence[Value]
+            A sequence of value expressions that match the selector
+
+        """
diff --git a/ibis/expr/builders.py b/ibis/expr/builders.py
index 59d131123..1cb5b194c 100644
--- a/ibis/expr/builders.py
+++ b/ibis/expr/builders.py
@@ -13,6 +13,7 @@ from ibis.common.annotations import annotated, attribute
 from ibis.common.deferred import Deferred, Resolver, deferrable
 from ibis.common.exceptions import IbisInputError
 from ibis.common.grounds import Concrete
+from ibis.common.selectors import Selector  # noqa: TCH001
 from ibis.common.typing import VarTuple  # noqa: TCH001
 
 if TYPE_CHECKING:
@@ -145,8 +146,8 @@ class WindowBuilder(Builder):
     how: Literal["rows", "range"] = "rows"
     start: Optional[RangeWindowBoundary] = None
     end: Optional[RangeWindowBoundary] = None
-    groupings: VarTuple[Union[str, Resolver, ops.Value]] = ()
-    orderings: VarTuple[Union[str, Resolver, ops.SortKey]] = ()
+    groupings: VarTuple[Union[str, Resolver, Selector, ops.Value]] = ()
+    orderings: VarTuple[Union[str, Resolver, Selector, ops.SortKey]] = ()
 
     @attribute
     def _table(self):
diff --git a/ibis/expr/types/numeric.py b/ibis/expr/types/numeric.py
index 36da3740a..c99c357c7 100644
--- a/ibis/expr/types/numeric.py
+++ b/ibis/expr/types/numeric.py
@@ -836,7 +836,10 @@ class NumericColumn(Column, NumericValue):
             The correlation of `left` and `right`
         """
         return ops.Correlation(
-            self, right, how=how, where=self._bind_to_parent_table(where)
+            self,
+            self._bind_to_parent_table(right),
+            how=how,
+            where=self._bind_to_parent_table(where),
         ).to_expr()
 
     def cov(
@@ -862,7 +865,10 @@ class NumericColumn(Column, NumericValue):
             The covariance of `self` and `right`
         """
         return ops.Covariance(
-            self, right, how=how, where=self._bind_to_parent_table(where)
+            self,
+            self._bind_to_parent_table(right),
+            how=how,
+            where=self._bind_to_parent_table(where),
         ).to_expr()
 
     def mean(
diff --git a/ibis/expr/types/relations.py b/ibis/expr/types/relations.py
index 7fd384e4b..0198f5a53 100644
--- a/ibis/expr/types/relations.py
+++ b/ibis/expr/types/relations.py
@@ -18,12 +18,12 @@ import ibis.expr.operations as ops
 import ibis.expr.schema as sch
 from ibis import util
 from ibis.common.deferred import Deferred, Resolver
+from ibis.common.selectors import Selector
 from ibis.expr.rewrites import DerefMap
 from ibis.expr.types.core import Expr, _FixedTextJupyterMixin
 from ibis.expr.types.generic import Value, literal
 from ibis.expr.types.pretty import to_rich
 from ibis.expr.types.temporal import TimestampColumn
-from ibis.selectors import Selector
 from ibis.util import deprecated
 
 if TYPE_CHECKING:
diff --git a/ibis/selectors.py b/ibis/selectors.py
index 6af620c6c..2b89e1ffb 100644
--- a/ibis/selectors.py
+++ b/ibis/selectors.py
@@ -50,7 +50,6 @@ Using a composition of selectors this is much less tiresome:
 
 from __future__ import annotations
 
-import abc
 import functools
 import inspect
 import operator
@@ -67,27 +66,8 @@ from ibis import util
 from ibis.common.collections import frozendict  # noqa: TCH001
 from ibis.common.deferred import Deferred, Resolver
 from ibis.common.exceptions import IbisError
-from ibis.common.grounds import Concrete, Singleton
-
-
-class Selector(Concrete):
-    """A column selector."""
-
-    @abc.abstractmethod
-    def expand(self, table: ir.Table) -> Sequence[ir.Value]:
-        """Expand `table` into value expressions that match the selector.
-
-        Parameters
-        ----------
-        table
-            An ibis table expression
-
-        Returns
-        -------
-        Sequence[Value]
-            A sequence of value expressions that match the selector
-
-        """
+from ibis.common.grounds import Singleton
+from ibis.common.selectors import Selector
 
 
 class Predicate(Selector):
diff --git a/justfile b/justfile
index 5b2769889..a4610d7e3 100644
--- a/justfile
+++ b/justfile
@@ -177,10 +177,6 @@ view-changelog flags="":
         -- conventional-changelog --config ./.conventionalcommits.js \
         | ([ "{{ flags }}" = "--pretty" ] && glow -p - || cat -)
 
-# run the decouple script to check for prohibited inter-module dependencies
-decouple +args:
-    python ci/check_disallowed_imports.py {{ args }}
-
 # profile something
 profile +args:
     pyinstrument {{ args }}
diff --git a/poetry.lock b/poetry.lock
index af1133431..ea0b75d45 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -5061,20 +5061,6 @@ google-auth = {version = ">=1.25.0,<3.0dev", markers = "python_version >= \"3.6\
 google-auth-oauthlib = {version = ">=0.4.0", markers = "python_version >= \"3.6\""}
 setuptools = "*"
 
-[[package]]
-name = "pydeps"
-version = "1.12.20"
-description = "Display module dependencies"
-optional = false
-python-versions = "*"
-files = [
-    {file = "pydeps-1.12.20-py3-none-any.whl", hash = "sha256:7278a8912bdada26b773d3550f0cc8fd76bed172d23dc1472c457d6292b67fd7"},
-    {file = "pydeps-1.12.20.tar.gz", hash = "sha256:4f6a833a7998bfa24e339c55535882905c22960efc14902c361f74b2952aff19"},
-]
-
-[package.dependencies]
-stdlib-list = "*"
-
 [[package]]
 name = "pydruid"
 version = "0.6.9"
@@ -6840,24 +6826,6 @@ build = ["cython (>=0.29.33)"]
 develop = ["colorama", "cython (>=0.29.33)", "cython (>=3.0.10,<4)", "flake8", "isort", "joblib", "matplotlib (>=3)", "pytest (>=7.3.0,<8)", "pytest-cov", "pytest-randomly", "pytest-xdist", "pywinpty", "setuptools-scm[toml] (>=8.0,<9.0)"]
 docs = ["ipykernel", "jupyter-client", "matplotlib", "nbconvert", "nbformat", "numpydoc", "pandas-datareader", "sphinx"]
 
-[[package]]
-name = "stdlib-list"
-version = "0.10.0"
-description = "A list of Python Standard Libraries (2.7 through 3.12)."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "stdlib_list-0.10.0-py3-none-any.whl", hash = "sha256:b3a911bc441d03e0332dd1a9e7d0870ba3bb0a542a74d7524f54fb431256e214"},
-    {file = "stdlib_list-0.10.0.tar.gz", hash = "sha256:6519c50d645513ed287657bfe856d527f277331540691ddeaf77b25459964a14"},
-]
-
-[package.extras]
-dev = ["build", "stdlib-list[doc,lint,test]"]
-doc = ["furo", "sphinx"]
-lint = ["black", "mypy", "ruff"]
-support = ["sphobjinv"]
-test = ["coverage[toml]", "pytest", "pytest-cov"]
-
 [[package]]
 name = "tabulate"
 version = "0.9.0"
@@ -7771,4 +7739,4 @@ visualization = ["graphviz"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "95a451a0189c8fa22f51935c279138dabe0fa1155d7996dda6990ecbabed6c60"
+content-hash = "e4c7b86eba7e816e771cb86f25138ddae33163dbb5732418f6358fefe6490edc"
diff --git a/pyproject.toml b/pyproject.toml
index 4dd5f8322..41794d1d1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -100,7 +100,6 @@ ipython = ">=8.7.0,<9"
 poetry-plugin-export = ">=1.6.0,<2"
 poetry-dynamic-versioning = ">=0.18.0,<2"
 pre-commit = ">=3.1,<4"
-pydeps = ">=1.12.7,<2"
 pyinstrument = ">=4.5.1,<5"
 ruff = ">=0.1.8"
 tqdm = ">=4.66.1,<5"
@@ -345,14 +344,6 @@ markers = [
   "tpcds: TPC-DS tests",
 ]
 
-[tool.pydeps]
-exclude = ["__future__", "collections", "os", "re", "sys"]
-verbose = 0
-pylib = false
-show_dot = false
-no_dot = true
-show_deps = true
-
 [tool.ruff]
 line-length = 88
 respect-gitignore = true
diff --git a/requirements-dev.txt b/requirements-dev.txt
index c5e35f49b..c968454e1 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -195,7 +195,6 @@ pycparser==2.22 ; python_version >= "3.10" and python_version < "4.0"
 pydantic-core==2.20.1 ; python_version >= "3.10" and python_version < "3.13"
 pydantic==2.8.2 ; python_version >= "3.10" and python_version < "3.13"
 pydata-google-auth==1.8.2 ; python_version >= "3.10" and python_version < "4.0"
-pydeps==1.12.20 ; python_version >= "3.10" and python_version < "4.0"
 pydruid==0.6.9 ; python_version >= "3.10" and python_version < "4.0"
 pyexasol[pandas]==0.26.0 ; python_version >= "3.10" and python_version < "4.0"
 pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
@@ -259,7 +258,6 @@ sqlalchemy==2.0.31 ; python_version >= "3.10" and python_version < "3.13"
 sqlglot==25.6.1 ; python_version >= "3.10" and python_version < "4.0"
 stack-data==0.6.3 ; python_version >= "3.10" and python_version < "4.0"
 statsmodels==0.14.2 ; python_version >= "3.10" and python_version < "3.13"
-stdlib-list==0.10.0 ; python_version >= "3.10" and python_version < "4.0"
 tabulate==0.9.0 ; python_version >= "3.10" and python_version < "3.13"
 tenacity==8.5.0 ; python_version >= "3.10" and python_version < "3.13"
 terminado==0.18.1 ; python_version >= "3.10" and python_version < "3.13"


Test patch:
diff --git a/ibis/expr/tests/test_reductions.py b/ibis/expr/tests/test_reductions.py
index ccca62988..5f27f25c4 100644
--- a/ibis/expr/tests/test_reductions.py
+++ b/ibis/expr/tests/test_reductions.py
@@ -106,3 +106,10 @@ def test_argminmax_deferred(func_name):
     t = ibis.table({"a": "int", "b": "int"}, name="t")
     func = getattr(t.a, func_name)
     assert func(_.b).equals(func(t.b))
+
+
+@pytest.mark.parametrize("func_name", ["cov", "corr"])
+def test_cov_corr_deferred(func_name):
+    t = ibis.table({"a": "int", "b": "int"}, name="t")
+    func = getattr(t.a, func_name)
+    assert func(_.b).equals(func(t.b))
diff --git a/ibis/tests/expr/test_selectors.py b/ibis/tests/expr/test_selectors.py
index 62c7821e9..fac487163 100644
--- a/ibis/tests/expr/test_selectors.py
+++ b/ibis/tests/expr/test_selectors.py
@@ -494,3 +494,26 @@ def test_order_by_with_selectors(penguins):
 
     with pytest.raises(exc.IbisError):
         penguins.order_by(~s.all())
+
+
+def test_window_function_group_by(penguins):
+    expr = penguins.species.count().over(group_by=s.c("island"))
+    assert expr.equals(penguins.species.count().over(group_by=penguins.island))
+
+
+def test_window_function_order_by(penguins):
+    expr = penguins.island.count().over(order_by=s.c("species"))
+    assert expr.equals(penguins.island.count().over(order_by=penguins.species))
+
+
+def test_window_function_group_by_order_by(penguins):
+    expr = penguins.species.count().over(
+        group_by=s.c("island"),
+        order_by=s.c("year") | (~s.c("island", "species") & s.of_type("str")),
+    )
+    assert expr.equals(
+        penguins.species.count().over(
+            group_by=penguins.island,
+            order_by=[penguins.sex, penguins.year],
+        )
+    )