Skip to content
This repository was archived by the owner on Mar 29, 2023. It is now read-only.

Commit b6bbfbe

Browse files
authored
fix: compatibility with ibis 1.4.0 (and possibly 1.2, 1.3) (#31)
* fix: compatibility with ibis 1.4.0 * add module-level connect method * workaround for circular import * allow test_scalar_param tests to work on ibis 1.4 * mskip tests on 1.4 * fix compatibility for 1.2
1 parent 4c5cb57 commit b6bbfbe

File tree

8 files changed

+248
-56
lines changed

8 files changed

+248
-56
lines changed

ibis_bigquery/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
import google.auth.credentials
66
import google.cloud.bigquery # noqa: F401, fail early if bigquery is missing
77
import pydata_google_auth
8-
from ibis.backends.base import BaseBackend
98
from pydata_google_auth import cache
109

1110
from . import version as ibis_bigquery_version
1211
from .client import (BigQueryClient, BigQueryDatabase, BigQueryQuery,
1312
BigQueryTable)
1413
from .compiler import BigQueryExprTranslator, BigQueryQueryBuilder
1514

15+
try:
16+
from ibis.backends.base import BaseBackend
17+
except ImportError:
18+
from .backcompat import BaseBackend
19+
1620
try:
1721
from .udf import udf # noqa F401
1822
except ImportError:
@@ -42,6 +46,13 @@ class Backend(BaseBackend):
4246
database_class = BigQueryDatabase
4347
table_class = BigQueryTable
4448

49+
# These were moved from TestConf for use in common test suite.
50+
# TODO: Indicate RoundAwayFromZero and UnorderedComparator.
51+
# https://github.com/ibis-project/ibis-bigquery/issues/30
52+
supports_divide_by_zero = True
53+
supports_floating_modulus = False
54+
returned_timestamp_unit = 'us'
55+
4556
def connect(
4657
self,
4758
project_id: Optional[str] = None,

ibis_bigquery/backcompat.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"""Helpers to make this backend compatible with Ibis versions < 2.0.
2+
3+
Keep in sync with:
4+
https://github.com/ibis-project/ibis/blob/master/ibis/backends/base/__init__.py
5+
6+
TODO: Remove this after Ibis 2.0 release and support for earlier versions of
7+
Ibis < 2.0 is dropped.
8+
"""
9+
10+
import abc
11+
12+
try:
13+
from ibis.common.exceptions import TranslationError
14+
except ImportError:
15+
# 1.2
16+
from ibis.common import TranslationError
17+
18+
__all__ = ('BaseBackend',)
19+
20+
21+
class BaseBackend(abc.ABC):
22+
"""
23+
Base backend class.
24+
All Ibis backends are expected to subclass this `Backend` class,
25+
and implement all the required methods.
26+
"""
27+
28+
@property
29+
@abc.abstractmethod
30+
def name(self) -> str:
31+
"""
32+
Name of the backend, for example 'sqlite'.
33+
"""
34+
pass
35+
36+
@property
37+
@abc.abstractmethod
38+
def kind(self):
39+
"""
40+
Backend kind. One of:
41+
sqlalchemy
42+
Backends using a SQLAlchemy dialect.
43+
sql
44+
SQL based backends, not based on a SQLAlchemy dialect.
45+
pandas
46+
Backends using pandas to store data and perform computations.
47+
spark
48+
Spark based backends.
49+
"""
50+
pass
51+
52+
@property
53+
@abc.abstractmethod
54+
def builder(self):
55+
pass
56+
57+
@property
58+
@abc.abstractmethod
59+
def translator(self):
60+
pass
61+
62+
@property
63+
def dialect(self):
64+
"""
65+
Dialect class of the backend.
66+
We generate it dynamically to avoid repeating the code for each
67+
backend.
68+
"""
69+
# TODO importing dialects inside the function to avoid circular
70+
# imports. In the future instead of this if statement we probably
71+
# want to create subclasses for each of the kinds
72+
# (e.g. `BaseSQLAlchemyBackend`)
73+
# TODO check if the below dialects can be merged into a single one
74+
if self.kind == 'sqlalchemy':
75+
from ibis.backends.base_sqlalchemy.alchemy import AlchemyDialect
76+
77+
dialect_class = AlchemyDialect
78+
elif self.kind in ('sql', 'pandas'):
79+
try:
80+
from ibis.backends.base_sqlalchemy.compiler import Dialect
81+
except ImportError:
82+
from ibis.sql.compiler import Dialect
83+
84+
dialect_class = Dialect
85+
elif self.kind == 'spark':
86+
from ibis.backends.base_sql.compiler import BaseDialect
87+
88+
dialect_class = BaseDialect
89+
else:
90+
raise ValueError(
91+
f'Backend class "{self.kind}" unknown. '
92+
'Expected one of "sqlalchemy", "sql", '
93+
'"pandas" or "spark".'
94+
)
95+
96+
dialect_class.translator = self.translator
97+
return dialect_class
98+
99+
@abc.abstractmethod
100+
def connect(connection_string, **options):
101+
"""
102+
Connect to the underlying database and return a client object.
103+
"""
104+
pass
105+
106+
def register_options(self):
107+
"""
108+
If the backend has custom options, register them here.
109+
They will be prefixed with the name of the backend.
110+
"""
111+
pass
112+
113+
def compile(self, expr, params=None):
114+
"""
115+
Compile the expression.
116+
"""
117+
context = self.dialect.make_context(params=params)
118+
builder = self.builder(expr, context=context)
119+
query_ast = builder.get_result()
120+
# TODO make all builders return a QueryAST object
121+
if isinstance(query_ast, list):
122+
query_ast = query_ast[0]
123+
compiled = query_ast.compile()
124+
return compiled
125+
126+
def verify(self, expr, params=None):
127+
"""
128+
Verify `expr` is an expression that can be compiled.
129+
"""
130+
try:
131+
self.compile(expr, params=params)
132+
return True
133+
except TranslationError:
134+
return False

ibis_bigquery/client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
import google.cloud.bigquery as bq
88
import ibis
9-
import ibis.common.exceptions as com
9+
10+
try:
11+
import ibis.common.exceptions as com
12+
except ImportError:
13+
import ibis.common as com
14+
1015
import ibis.expr.datatypes as dt
1116
import ibis.expr.lineage as lin
1217
import ibis.expr.operations as ops

ibis_bigquery/compiler.py

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,54 @@
55
from functools import partial
66

77
import ibis
8-
import ibis.backends.base_sqlalchemy.compiler as comp
9-
import ibis.common.exceptions as com
8+
9+
try:
10+
import ibis.backends.base_sqlalchemy.compiler as comp
11+
except ImportError:
12+
import ibis.sql.compiler as comp
13+
try:
14+
import ibis.common.exceptions as com
15+
except ImportError:
16+
import ibis.common as com
17+
1018
import ibis.expr.datatypes as dt
1119
import ibis.expr.lineage as lin
1220
import ibis.expr.operations as ops
1321
import ibis.expr.types as ir
1422
import numpy as np
1523
import regex as re
1624
import toolz
17-
from ibis.backends.base.sql import (fixed_arity, literal, operation_registry,
18-
reduction, unary)
19-
from ibis.backends.base_sql.compiler import (BaseExprTranslator, BaseSelect,
20-
BaseTableSetFormatter)
25+
26+
try:
27+
# 2.x
28+
from ibis.backends.base.sql import (fixed_arity, literal,
29+
operation_registry, reduction, unary)
30+
except ImportError:
31+
try:
32+
# 1.4
33+
from ibis.backends.base_sql import (fixed_arity, literal,
34+
operation_registry, reduction,
35+
unary)
36+
except ImportError:
37+
# 1.2
38+
from ibis.impala.compiler import _literal as literal
39+
from ibis.impala.compiler import \
40+
_operation_registry as operation_registry
41+
from ibis.impala.compiler import _reduction as reduction
42+
from ibis.impala.compiler import fixed_arity, unary
43+
44+
try:
45+
from ibis.backends.base_sql.compiler import (BaseExprTranslator,
46+
BaseSelect,
47+
BaseTableSetFormatter)
48+
except ImportError:
49+
# 1.2
50+
from ibis.impala.compiler import ImpalaExprTranslator as BaseExprTranslator
51+
from ibis.impala.compiler import ImpalaSelect as BaseSelect
52+
from ibis.impala.compiler import (
53+
ImpalaTableSetFormatter as BaseTableSetFormatter
54+
)
55+
2156
from multipledispatch import Dispatcher
2257

2358
from .datatypes import ibis_type_to_bigquery_type
@@ -369,18 +404,13 @@ def _formatter(translator, expr):
369404
}
370405
_operation_registry.update(
371406
{
372-
ops.BitAnd: reduction('BIT_AND'),
373-
ops.BitOr: reduction('BIT_OR'),
374-
ops.BitXor: reduction('BIT_XOR'),
375407
ops.ExtractYear: _extract_field('year'),
376-
ops.ExtractQuarter: _extract_field('quarter'),
377408
ops.ExtractMonth: _extract_field('month'),
378409
ops.ExtractDay: _extract_field('day'),
379410
ops.ExtractHour: _extract_field('hour'),
380411
ops.ExtractMinute: _extract_field('minute'),
381412
ops.ExtractSecond: _extract_field('second'),
382413
ops.ExtractMillisecond: _extract_field('millisecond'),
383-
ops.ExtractEpochSeconds: _extract_field('epochseconds'),
384414
ops.Hash: _hash,
385415
ops.StringReplace: fixed_arity('REPLACE', 3),
386416
ops.StringSplit: fixed_arity('SPLIT', 2),
@@ -427,6 +457,25 @@ def _formatter(translator, expr):
427457
}
428458
)
429459

460+
461+
def _try_register_op(op_name: str, value):
462+
"""Register operation if it exists in Ibis.
463+
464+
This allows us to decouple slightly from ibis-framework releases.
465+
"""
466+
if hasattr(ops, op_name):
467+
_operation_registry[getattr(ops, op_name)] = value
468+
469+
470+
# 2.x
471+
_try_register_op('BitAnd', reduction('BIT_AND'))
472+
_try_register_op('BitOr', reduction('BIT_OR'))
473+
_try_register_op('BitXor', reduction('BIT_XOR'))
474+
# 1.4
475+
_try_register_op('ExtractQuarter', _extract_field('quarter'))
476+
_try_register_op('ExtractEpochSeconds', _extract_field('epochseconds'))
477+
478+
430479
_invalid_operations = {
431480
ops.Translate,
432481
ops.FindInSet,

ibis_bigquery/udf/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66

77
import ibis.expr.datatypes as dt
88
import ibis.expr.rules as rlz
9-
import ibis.udf.validate as v
9+
10+
try:
11+
from ibis.udf.validate import validate_output_type
12+
except ImportError:
13+
# 1.2
14+
def validate_output_type(*args):
15+
pass
16+
1017
from ibis.expr.signature import Argument as Arg
1118

1219
from ..compiler import BigQueryUDFNode, compiles
@@ -163,7 +170,7 @@ class Rectangle {
163170
return my_rectangle(width, height);
164171
""";
165172
'''
166-
v.validate_output_type(output_type)
173+
validate_output_type(output_type)
167174

168175
if libraries is None:
169176
libraries = []

tests/system/conftest.py

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import os
2-
from pathlib import Path
32

4-
import ibis
5-
import ibis.expr.types as ir
3+
import ibis # noqa: F401
64
import pytest
75
from google.oauth2 import service_account
8-
from ibis.backends.tests.base import (BackendTest, RoundAwayFromZero,
9-
UnorderedComparator)
106

117
import ibis_bigquery
128

@@ -42,38 +38,6 @@ def _credentials():
4238
)
4339

4440

45-
class TestConf(UnorderedComparator, BackendTest, RoundAwayFromZero):
46-
supports_divide_by_zero = True
47-
supports_floating_modulus = False
48-
returned_timestamp_unit = 'us'
49-
50-
@staticmethod
51-
def connect(data_directory: Path) -> ibis.client.Client:
52-
project_id = os.environ.get('GOOGLE_BIGQUERY_PROJECT_ID')
53-
if project_id is None:
54-
pytest.skip(
55-
'Environment variable GOOGLE_BIGQUERY_PROJECT_ID '
56-
'not defined'
57-
)
58-
elif not project_id:
59-
pytest.skip(
60-
'Environment variable GOOGLE_BIGQUERY_PROJECT_ID is empty'
61-
)
62-
return bq.connect(
63-
project_id=project_id,
64-
dataset_id=DATASET_ID,
65-
credentials=_credentials(),
66-
)
67-
68-
@property
69-
def batting(self) -> ir.TableExpr:
70-
return None
71-
72-
@property
73-
def awards_players(self) -> ir.TableExpr:
74-
return None
75-
76-
7741
@pytest.fixture(scope='session')
7842
def project_id():
7943
return PROJECT_ID

0 commit comments

Comments
 (0)