diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml index 9f806dd2c916..f6c137fbceb0 100644 --- a/.github/workflows/check-generated-files.yml +++ b/.github/workflows/check-generated-files.yml @@ -36,7 +36,7 @@ jobs: run: sudo apt-get update -y -q - name: install system dependencies - run: sudo apt-get install -y -q build-essential graphviz libgeos-dev libkrb5-dev krb5-config freetds-dev + run: sudo apt-get install -y -q build-essential graphviz libgeos-dev freetds-dev unixodbc-dev - name: install poetry run: pip install 'poetry==1.7.1' diff --git a/.github/workflows/ibis-backends.yml b/.github/workflows/ibis-backends.yml index b876b440e745..c4796e11b20d 100644 --- a/.github/workflows/ibis-backends.yml +++ b/.github/workflows/ibis-backends.yml @@ -43,6 +43,7 @@ jobs: runs-on: ${{ matrix.os }} env: SQLALCHEMY_WARN_20: "1" + ODBCSYSINI: "${{ github.workspace }}/.odbc" strategy: fail-fast: false matrix: @@ -130,15 +131,14 @@ jobs: - ninja-build - name: mssql title: MS SQL Server - serial: true extras: - mssql services: - mssql sys-deps: - - libkrb5-dev - - krb5-config - freetds-dev + - unixodbc-dev + - tdsodbc - name: trino title: Trino extras: @@ -237,15 +237,14 @@ jobs: backend: name: mssql title: MS SQL Server - serial: true extras: - mssql services: - mssql sys-deps: - - libkrb5-dev - - krb5-config - freetds-dev + - unixodbc-dev + - tdsodbc - os: windows-latest backend: name: trino @@ -315,6 +314,16 @@ jobs: - name: checkout uses: actions/checkout@v4 + - name: setup odbc for mssql + if: ${{ matrix.backend.name == 'mssql' }} + run: | + mkdir -p "$ODBCSYSINI" + + { + echo '[FreeTDS]' + echo "Driver = libtdsodbc.so" + } > "$ODBCSYSINI/odbcinst.ini" + - uses: extractions/setup-just@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -664,6 +673,10 @@ jobs: - mssql extras: - mssql + sys-deps: + - freetds-dev + - unixodbc-dev + - tdsodbc - name: mysql title: MySQL services: @@ -678,6 +691,8 @@ jobs: extras: - geospatial - postgres + sys-deps: + - libgeos-dev - name: sqlite title: SQLite extras: @@ -700,21 +715,29 @@ jobs: - oracle services: - oracle + env: + ODBCSYSINI: "${{ github.workspace }}/.odbc" steps: - name: checkout uses: actions/checkout@v4 - - name: install libgeos for shapely - if: ${{ matrix.backend.name == 'postgres' }} + - name: update and install system dependencies + if: matrix.backend.sys-deps != null run: | - sudo apt-get update -y -qq - sudo apt-get install -qq -y build-essential libgeos-dev + set -euo pipefail - - name: install freetds-dev for mssql + sudo apt-get update -qq -y + sudo apt-get install -qq -y build-essential ${{ join(matrix.backend.sys-deps, ' ') }} + + - name: setup odbc for mssql if: ${{ matrix.backend.name == 'mssql' }} run: | - sudo apt-get update -y -qq - sudo apt-get install -qq -y build-essential libkrb5-dev krb5-config freetds-dev + mkdir -p "$ODBCSYSINI" + + { + echo '[FreeTDS]' + echo "Driver = libtdsodbc.so" + } > "$ODBCSYSINI/odbcinst.ini" - uses: extractions/setup-just@v1 env: diff --git a/.github/workflows/ibis-docs-lint.yml b/.github/workflows/ibis-docs-lint.yml index 10e8b371c651..f4cfc6e300d6 100644 --- a/.github/workflows/ibis-docs-lint.yml +++ b/.github/workflows/ibis-docs-lint.yml @@ -101,9 +101,7 @@ jobs: python-version: "3.11" - name: install system dependencies - run: | - sudo apt-get update -y -qq - sudo apt-get install -qq -y build-essential libgeos-dev freetds-dev libkrb5-dev krb5-config + run: sudo apt-get install -qq -y build-essential libgeos-dev freetds-dev unixodbc-dev - uses: syphar/restore-virtualenv@v1 with: diff --git a/.github/workflows/ibis-main.yml b/.github/workflows/ibis-main.yml index 1805db355ff7..dddc13697856 100644 --- a/.github/workflows/ibis-main.yml +++ b/.github/workflows/ibis-main.yml @@ -165,8 +165,8 @@ jobs: run: | set -euo pipefail - sudo apt-get update -y -qq - sudo apt-get install -y -q build-essential graphviz libgeos-dev libkrb5-dev freetds-dev + sudo apt-get update -y -q + sudo apt-get install -y -q build-essential graphviz libgeos-dev freetds-dev unixodbc-dev - name: checkout uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 94081a6d8d15..0d77090914c5 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ ibis/examples/descriptions # chat *zuliprc* +.odbc diff --git a/flake.nix b/flake.nix index 6112576e4613..f754a47f4baa 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,9 @@ duckdb # mysql mariadb-client + # pyodbc setup debugging + # in particular: odbcinst -j + unixODBC # pyspark openjdk17_headless # postgres client @@ -111,6 +114,12 @@ MSSQL_SA_PASSWORD = "1bis_Testing!"; DRUID_URL = "druid://localhost:8082/druid/v2/sql"; + # needed for mssql+pyodbc + ODBCSYSINI = pkgs.writeTextDir "odbcinst.ini" '' + [FreeTDS] + Driver = ${pkgs.lib.makeLibraryPath [ pkgs.freetds ]}/libtdsodbc.so + ''; + __darwinAllowLocalNetworking = true; }; in diff --git a/ibis/backends/base/sql/alchemy/__init__.py b/ibis/backends/base/sql/alchemy/__init__.py index 6cbdf62393db..6088e75447b8 100644 --- a/ibis/backends/base/sql/alchemy/__init__.py +++ b/ibis/backends/base/sql/alchemy/__init__.py @@ -3,7 +3,6 @@ import abc import atexit import contextlib -import getpass import warnings from operator import methodcaller from typing import TYPE_CHECKING, Any @@ -136,11 +135,20 @@ def _compile_type(self, dtype) -> str: self.compiler.translator_class.get_sqla_type(dtype) ).compile(dialect=dialect) - def _build_alchemy_url(self, url, host, port, user, password, database, driver): + def _build_alchemy_url( + self, + url: str | None, + host: str | None, + port: int | None, + user: str | None, + password: str | None, + database: str | None, + driver: str | None, + query: Mapping[str, Any] | None = None, + ) -> sa.engine.URL: if url is not None: return sa.engine.url.make_url(url) - user = user or getpass.getuser() return sa.engine.url.URL.create( driver, host=host, @@ -148,6 +156,7 @@ def _build_alchemy_url(self, url, host, port, user, password, database, driver): username=user, password=password, database=database, + query=query or {}, ) @property @@ -875,8 +884,11 @@ def _get_compiled_statement( compiled = definition.compile( dialect=self.con.dialect, compile_kwargs=compile_kwargs ) - lines = self._get_temp_view_definition(name, definition=compiled) - return lines, compiled.params + create_view = self._get_temp_view_definition(name, definition=compiled) + params = compiled.params + if compiled.positional: + params = tuple(params.values()) + return create_view, params def _create_temp_view(self, view: sa.Table, definition: sa.sql.Selectable) -> None: raw_name = view.name diff --git a/ibis/backends/mssql/__init__.py b/ibis/backends/mssql/__init__.py index 3b1abbc0fa95..f424864a5791 100644 --- a/ibis/backends/mssql/__init__.py +++ b/ibis/backends/mssql/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal +import contextlib +from typing import TYPE_CHECKING, Any import sqlalchemy as sa -import sqlglot as sg import toolz from ibis.backends.base import CanCreateDatabase @@ -35,10 +35,16 @@ def do_connect( port: int = 1433, database: str | None = None, url: str | None = None, - driver: Literal["pymssql"] = "pymssql", + query: Mapping[str, Any] | None = None, + driver: str | None = None, + **kwargs: Any, ) -> None: - if driver != "pymssql": - raise NotImplementedError("pymssql is currently the only supported driver") + if query is None: + query = {} + + if driver is not None: + query["driver"] = driver + alchemy_url = self._build_alchemy_url( url=url, host=host, @@ -46,10 +52,13 @@ def do_connect( user=user, password=password, database=database, - driver=f"mssql+{driver}", + driver="mssql+pyodbc", + query=query, ) - engine = sa.create_engine(alchemy_url, poolclass=sa.pool.StaticPool) + engine = sa.create_engine( + alchemy_url, poolclass=sa.pool.StaticPool, connect_args=kwargs + ) @sa.event.listens_for(engine, "connect") def connect(dbapi_connection, connection_record): @@ -85,40 +94,20 @@ def list_databases(self, like: str | None = None) -> list[str]: def current_schema(self) -> str: return self._scalar_query(sa.select(sa.func.schema_name())) - def list_tables( - self, - like: str | None = None, - database: str | None = None, - schema: str | None = None, - ) -> list[str]: - tablequery = sg.select("name").from_( - sg.table("tables", db="sys", catalog=database) + @contextlib.contextmanager + def _safe_raw_sql(self, stmt, *args, **kwargs): + sql = str( + stmt.compile( + dialect=self.con.dialect, compile_kwargs={"literal_binds": True} + ) ) - viewquery = sg.select("name").from_( - sg.table("views", db="sys", catalog=database) - ) - - if schema is not None: - table_predicate = sg.func( - "schema_name", - sg.column("schema_id", table="tables", db="sys", catalog=database), - ).eq(schema) - view_predicate = sg.func( - "schema_name", - sg.column("schema_id", table="views", db="sys", catalog=database), - ).eq(schema) - tablequery = tablequery.where(table_predicate) - viewquery = viewquery.where(view_predicate) - - tablequery = sa.text(tablequery.sql(dialect="tsql")) - viewquery = sa.text(viewquery.sql(dialect="tsql")) - with self.begin() as con: - tablequery = list(con.execute(tablequery).scalars()) - viewresults = list(con.execute(viewquery).scalars()) - results = tablequery + viewresults + yield con.exec_driver_sql(sql, *args, **kwargs) - return self._filter_with_like(results, like) + def _get_compiled_statement(self, view: sa.Table, definition: sa.sql.Selectable): + return super()._get_compiled_statement( + view, definition, compile_kwargs={"literal_binds": True} + ) def _get_temp_view_definition( self, name: str, definition: sa.sql.compiler.Compiled diff --git a/ibis/backends/mssql/tests/conftest.py b/ibis/backends/mssql/tests/conftest.py index 87161387bb72..adc3977f7109 100644 --- a/ibis/backends/mssql/tests/conftest.py +++ b/ibis/backends/mssql/tests/conftest.py @@ -19,6 +19,7 @@ MSSQL_HOST = os.environ.get("IBIS_TEST_MSSQL_HOST", "localhost") MSSQL_PORT = int(os.environ.get("IBIS_TEST_MSSQL_PORT", 1433)) IBIS_TEST_MSSQL_DB = os.environ.get("IBIS_TEST_MSSQL_DATABASE", "ibis_testing") +MSSQL_PYODBC_DRIVER = os.environ.get("IBIS_TEST_MSSQL_PYODBC_DRIVER", "FreeTDS") class TestConf(ServiceBackendTest): @@ -32,7 +33,7 @@ class TestConf(ServiceBackendTest): supports_json = False rounding_method = "half_to_even" service_name = "mssql" - deps = "pymssql", "sqlalchemy" + deps = "pyodbc", "sqlalchemy" @property def test_files(self) -> Iterable[Path]: @@ -57,10 +58,12 @@ def _load_data( script_dir Location of scripts defining schemas """ + params = f"driver={MSSQL_PYODBC_DRIVER}" + url = sa.engine.make_url( + f"mssql+pyodbc://{user}:{password}@{host}:{port:d}/{database}?{params}" + ) init_database( - url=sa.engine.make_url( - f"mssql+pymssql://{user}:{password}@{host}:{port:d}/{database}" - ), + url=url, database=database, schema=self.ddl_script, isolation_level="AUTOCOMMIT", @@ -75,6 +78,7 @@ def connect(*, tmpdir, worker_id, **kw): password=MSSQL_PASS, database=IBIS_TEST_MSSQL_DB, port=MSSQL_PORT, + driver=MSSQL_PYODBC_DRIVER, **kw, ) diff --git a/ibis/backends/mssql/tests/test_client.py b/ibis/backends/mssql/tests/test_client.py index 77018b8e1024..12012ac929d6 100644 --- a/ibis/backends/mssql/tests/test_client.py +++ b/ibis/backends/mssql/tests/test_client.py @@ -120,7 +120,7 @@ def count_big(x, where: bool = True) -> int: ft = con.tables.functional_alltypes expr = count_big(ft.id) with pytest.raises( - sa.exc.OperationalError, match="An expression of non-boolean type specified" + sa.exc.ProgrammingError, match="An expression of non-boolean type specified" ): assert expr.execute() diff --git a/ibis/backends/tests/test_client.py b/ibis/backends/tests/test_client.py index 21adb2121e68..21b06a249adf 100644 --- a/ibis/backends/tests/test_client.py +++ b/ibis/backends/tests/test_client.py @@ -596,7 +596,7 @@ def test_list_databases(alchemy_con): @pytest.mark.never( ["bigquery", "postgres", "mssql", "mysql", "snowflake", "oracle"], reason="backend does not support client-side in-memory tables", - raises=(sa.exc.OperationalError, TypeError), + raises=(sa.exc.OperationalError, TypeError, sa.exc.InterfaceError), ) @pytest.mark.notyet( ["trino"], reason="memory connector doesn't allow writing to tables" diff --git a/ibis/backends/tests/test_generic.py b/ibis/backends/tests/test_generic.py index b4f9406e3568..9256c85d1d1b 100644 --- a/ibis/backends/tests/test_generic.py +++ b/ibis/backends/tests/test_generic.py @@ -774,7 +774,7 @@ def test_select_filter_select(backend, alltypes, df): @pytest.mark.notimpl(["datafusion"], raises=com.OperationNotDefinedError) -@pytest.mark.broken(["mssql"], raises=sa.exc.OperationalError) +@pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError) def test_between(backend, alltypes, df): expr = alltypes.double_col.between(5, 10) result = expr.execute().rename("double_col") diff --git a/ibis/backends/tests/test_numeric.py b/ibis/backends/tests/test_numeric.py index b9f18011b18a..6bde1fd6609e 100644 --- a/ibis/backends/tests/test_numeric.py +++ b/ibis/backends/tests/test_numeric.py @@ -487,7 +487,7 @@ def test_numeric_literal(con, backend, expr, expected_types): "DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: " 'Check messages from the SQL Server\n")' "[SQL: SELECT %(param_1)s AS [Decimal('Infinity')]]", - raises=sa.exc.ProgrammingError, + raises=(sa.exc.ProgrammingError, KeyError), ), pytest.mark.broken( ["druid"], @@ -568,7 +568,7 @@ def test_numeric_literal(con, backend, expr, expected_types): "DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: " 'Check messages from the SQL Server\n")' "[SQL: SELECT %(param_1)s AS [Decimal('-Infinity')]]", - raises=sa.exc.ProgrammingError, + raises=(sa.exc.ProgrammingError, KeyError), ), pytest.mark.broken( ["druid"], @@ -652,7 +652,7 @@ def test_numeric_literal(con, backend, expr, expected_types): "DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: " 'Check messages from the SQL Server\n")' "[SQL: SELECT %(param_1)s AS [Decimal('NaN')]]", - raises=sa.exc.ProgrammingError, + raises=(sa.exc.ProgrammingError, KeyError), ), pytest.mark.broken( ["mssql"], @@ -1269,7 +1269,9 @@ def test_mod(backend, alltypes, df): backend.assert_series_equal(result, expected, check_dtype=False) -@pytest.mark.notimpl(["mssql"], raises=sa.exc.OperationalError) +@pytest.mark.notimpl( + ["mssql"], raises=(sa.exc.OperationalError, sa.exc.ProgrammingError) +) @pytest.mark.notyet( ["druid"], raises=AssertionError, reason="mod with floats is integer mod" ) @@ -1433,7 +1435,7 @@ def test_floating_mod(backend, alltypes, df): raises=AssertionError, reason="returns NULL when dividing by zero", ) -@pytest.mark.notyet(["mssql"], raises=sa.exc.OperationalError) +@pytest.mark.notyet(["mssql"], raises=(sa.exc.OperationalError, sa.exc.DataError)) @pytest.mark.notyet(["postgres"], raises=sa.exc.DataError) @pytest.mark.notyet(["snowflake"], raises=sa.exc.ProgrammingError) @pytest.mark.notimpl(["exasol"], raises=(sa.exc.DBAPIError, com.IbisTypeError)) diff --git a/ibis/backends/tests/test_string.py b/ibis/backends/tests/test_string.py index 96760f314b7d..bfc8f1f9579e 100644 --- a/ibis/backends/tests/test_string.py +++ b/ibis/backends/tests/test_string.py @@ -148,7 +148,7 @@ def uses_java_re(t): pytest.mark.broken( ["mssql"], reason="mssql doesn't allow like outside of filters", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), ], ), @@ -163,7 +163,7 @@ def uses_java_re(t): pytest.mark.broken( ["mssql"], reason="mssql doesn't allow like outside of filters", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), ], ), @@ -178,7 +178,7 @@ def uses_java_re(t): pytest.mark.broken( ["mssql"], reason="mssql doesn't allow like outside of filters", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), ], ), @@ -194,7 +194,7 @@ def uses_java_re(t): pytest.mark.broken( ["mssql"], reason="mssql doesn't allow like outside of filters", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), ], ), @@ -558,22 +558,7 @@ def uses_java_re(t): ["dask", "pyspark"], raises=com.OperationNotDefinedError, ), - pytest.mark.broken( - ["druid"], - raises=sa.exc.ProgrammingError, - ), - pytest.mark.broken( - ["mssql"], - raises=sa.exc.OperationalError, - reason=( - '(pymssql._pymssql.OperationalError) (156, b"Incorrect syntax near the keyword ' - "'LIKE'.DB-Lib error message 20018, severity 15:\nGeneral SQL Server error: " - 'Check messages from the SQL Server\n")' - "[SQL: SELECT (CASE t0.int_col WHEN %(param_1)s THEN %(param_2)s WHEN %(param_3)s " - "THEN %(param_4)s ELSE %(param_5)s END LIKE %(param_6)s + '%') AS tmp" - "FROM functional_alltypes AS t0]" - ), - ), + pytest.mark.broken(["druid", "mssql"], raises=sa.exc.ProgrammingError), ], ), param( @@ -588,16 +573,7 @@ def uses_java_re(t): ["dask", "datafusion", "pyspark"], raises=com.OperationNotDefinedError, ), - pytest.mark.broken(["druid"], raises=sa.exc.ProgrammingError), - pytest.mark.broken( - ["mssql"], - reason=( - '(pymssql._pymssql.OperationalError) (156, b"Incorrect syntax near ' - "the keyword 'LIKE'.DB-Lib error message 20018, severity 15:\n" - 'General SQL Server error: Check messages from the SQL Server\n")' - ), - raises=sa.exc.OperationalError, - ), + pytest.mark.broken(["druid", "mssql"], raises=sa.exc.ProgrammingError), ], ), param( @@ -609,15 +585,7 @@ def uses_java_re(t): ["dask"], raises=com.OperationNotDefinedError, ), - pytest.mark.broken( - ["mssql"], - raises=sa.exc.OperationalError, - reason=( - '(pymssql._pymssql.OperationalError) (156, b"Incorrect syntax near ' - "the keyword 'LIKE'.DB-Lib error message 20018, severity 15:\n" - 'General SQL Server error: Check messages from the SQL Server\n")' - ), - ), + pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError), ], ), param( @@ -629,15 +597,7 @@ def uses_java_re(t): ["dask", "datafusion"], raises=com.OperationNotDefinedError, ), - pytest.mark.broken( - ["mssql"], - raises=sa.exc.OperationalError, - reason=( - '(pymssql._pymssql.OperationalError) (156, b"Incorrect syntax near ' - "the keyword 'LIKE'.DB-Lib error message 20018, severity 15:\n" - 'General SQL Server error: Check messages from the SQL Server\n")' - ), - ), + pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError), ], ), param( @@ -677,7 +637,7 @@ def uses_java_re(t): pytest.mark.broken( ["mssql"], reason="substr requires 3 arguments", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), ], ), @@ -890,14 +850,7 @@ def test_re_replace_global(con): assert result == "cbc" -@pytest.mark.broken( - ["mssql"], - raises=sa.exc.OperationalError, - reason=( - '(pymssql._pymssql.OperationalError) (4145, b"An expression of non-boolean type specified in ' - "a context where a condition is expected, near 'THEN'.DB-Lib error message 20018, severity 15:\n" - ), -) +@pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError) @pytest.mark.notimpl(["druid"], raises=ValidationError) @pytest.mark.broken( ["oracle"], @@ -1080,7 +1033,7 @@ def test_levenshtein(con, right): @pytest.mark.notyet( ["mssql"], reason="doesn't allow boolean expressions in select statements", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ) @pytest.mark.broken( ["oracle"], diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py index 697320cb001a..f4fd52582b6c 100644 --- a/ibis/backends/tests/test_temporal.py +++ b/ibis/backends/tests/test_temporal.py @@ -2179,17 +2179,11 @@ def test_timestamp_literal(con, backend): @pytest.mark.parametrize( ("timezone", "expected"), [ - param( - "Europe/London", - "2022-02-04 16:20:00GMT", - id="name", - marks=[pytest.mark.broken(["mssql"], raises=TypeError)], - ), + param("Europe/London", "2022-02-04 16:20:00GMT", id="name"), param( "PST8PDT", "2022-02-04 08:20:00PST", # The time zone for Berkeley, California. id="iso", - marks=[pytest.mark.broken(["mssql"], raises=TypeError)], ), ], ) @@ -2296,7 +2290,7 @@ def test_time_literal(con, backend): 561021, marks=[ pytest.mark.notimpl( - ["mssql", "mysql"], + ["mysql"], raises=AssertionError, reason="doesn't have enough precision to capture microseconds", ), @@ -2706,24 +2700,13 @@ def test_large_timestamp(con): @pytest.mark.parametrize( ("ts", "scale", "unit"), [ - param( - "2023-01-07 13:20:05.561", - 3, - "ms", - id="ms", - marks=pytest.mark.broken( - ["mssql"], reason="incorrect result", raises=AssertionError - ), - ), + param("2023-01-07 13:20:05.561", 3, "ms", id="ms"), param( "2023-01-07 13:20:05.561021", 6, "us", id="us", marks=[ - pytest.mark.broken( - ["mssql"], reason="incorrect result", raises=AssertionError - ), pytest.mark.notyet( ["sqlite"], reason="doesn't support microseconds", @@ -2755,7 +2738,7 @@ def test_large_timestamp(con): pytest.mark.notyet( ["mssql"], reason="doesn't support nanoseconds", - raises=sa.exc.OperationalError, + raises=sa.exc.ProgrammingError, ), pytest.mark.notyet( ["bigquery"], diff --git a/ibis/backends/tests/test_window.py b/ibis/backends/tests/test_window.py index c0ecb83a4cd4..8b3543a3961b 100644 --- a/ibis/backends/tests/test_window.py +++ b/ibis/backends/tests/test_window.py @@ -846,7 +846,8 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): raises=com.UnsupportedOperationError, reason="Flink engine does not support generic window clause with no order by", ), - pytest.mark.broken(["mssql", "mysql"], raises=sa.exc.OperationalError), + pytest.mark.broken(["mysql"], raises=sa.exc.OperationalError), + pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError), pytest.mark.notyet( ["snowflake"], reason="backend requires ordering", @@ -897,7 +898,8 @@ def test_simple_ungrouped_window_with_scalar_order_by(alltypes): raises=com.UnsupportedOperationError, reason="Flink engine does not support generic window clause with no order by", ), - pytest.mark.broken(["mssql", "mysql"], raises=sa.exc.OperationalError), + pytest.mark.broken(["mysql"], raises=sa.exc.OperationalError), + pytest.mark.broken(["mssql"], raises=sa.exc.ProgrammingError), pytest.mark.notyet( ["snowflake"], reason="backend requires ordering", @@ -1022,11 +1024,7 @@ def test_ungrouped_unbounded_window( reason="RANGE OFFSET frame for 'DB::ColumnNullable' ORDER BY column is not implemented", raises=ClickHouseOperationalError, ) -@pytest.mark.notyet( - ["mssql"], - reason="RANGE is only supported with UNBOUNDED and CURRENT ROW window frame delimiters", - raises=sa.exc.OperationalError, -) +@pytest.mark.notyet(["mssql"], raises=sa.exc.ProgrammingError) def test_grouped_bounded_range_window(backend, alltypes, df): # Explanation of the range window spec below: # diff --git a/pyproject.toml b/pyproject.toml index 0386acc7931a..0a9b80c67789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,10 +79,10 @@ oracledb = { version = ">=1.3.1,<2", optional = true } packaging = { version = ">=21.3,<24", optional = true } polars = { version = ">=0.19.3,<1", optional = true } psycopg2 = { version = ">=2.8.4,<3", optional = true } -pymssql = { version = ">=2.2.5,<3", optional = true } pydata-google-auth = { version = ">=1.4.0,<2", optional = true } pydruid = { version = ">=0.6.5,<1", optional = true, extras = ["sqlalchemy"] } pymysql = { version = ">=1,<2", optional = true } +pyodbc = { version = ">=4.0.39,<5", optional = true } pyspark = { version = ">=3,<3.4", optional = true } # pyspark is heavily broken by numpy >=1.24 and pandas >=2 # used to support posix regexen in the pandas, dask and sqlite backends regex = { version = ">=2021.7.6", optional = true } @@ -172,8 +172,8 @@ all = [ "psycopg2", "pydata-google-auth", "pydruid", - "pymssql", "pymysql", + "pyodbc", "pyspark", "regex", "requests", @@ -200,7 +200,7 @@ exasol = ["sqlalchemy", "sqlalchemy-exasol", "sqlalchemy-views"] flink = [] geospatial = ["geoalchemy2", "geopandas", "shapely"] impala = ["fsspec", "impyla", "requests", "sqlalchemy"] -mssql = ["sqlalchemy", "pymssql", "sqlalchemy-views"] +mssql = ["sqlalchemy", "pyodbc", "sqlalchemy-views"] mysql = ["sqlalchemy", "pymysql", "sqlalchemy-views"] oracle = ["sqlalchemy", "oracledb", "packaging", "sqlalchemy-views"] pandas = ["regex"]