Skip to content

Commit 7429b2c

Browse files
feat: FIR-38073 ecosystem support for geography type for python sdk (#404)
1 parent 2bddcd3 commit 7429b2c

File tree

11 files changed

+105
-69
lines changed

11 files changed

+105
-69
lines changed

setup.cfg

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ project_urls =
2626
packages = find:
2727
install_requires =
2828
aiorwlock==1.1.0
29-
anyio>=3.7.1
29+
anyio>=3.7.1,<4.5.0
3030
appdirs>=1.4.4
3131
appdirs-stubs>=0.1.0
3232
async-generator>=1.10
@@ -55,7 +55,7 @@ dev =
5555
devtools==0.7.0
5656
mypy==1.*,<1.10.0
5757
pre-commit==3.5.0
58-
pyfakefs>=4.5.3
58+
pyfakefs>=4.5.3,<=5.6.0
5959
pytest==7.2.0
6060
pytest-cov==3.0.0
6161
pytest-httpx>=0.13.0

src/firebolt/client/auth/base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,9 @@ async def async_auth_flow(
171171
finally:
172172
# token gets updated only after flow.send is called
173173
# so unlock only after that
174-
if self._lock.locked() and self._lock._owner_task == get_current_task():
174+
# TODO: FIR-38687 Fix support for anyio 4.5.0+
175+
if (
176+
self._lock.locked()
177+
and self._lock._owner_task == get_current_task() # type: ignore
178+
):
175179
self._lock.release()

src/firebolt/common/_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class _InternalType(Enum):
177177
Boolean = "boolean"
178178

179179
Bytea = "bytea"
180+
Geography = "geography"
180181

181182
Nothing = "Nothing"
182183

@@ -198,6 +199,7 @@ def python_type(self) -> type:
198199
_InternalType.TimestampTz: datetime,
199200
_InternalType.Boolean: bool,
200201
_InternalType.Bytea: bytes,
202+
_InternalType.Geography: str,
201203
# For simplicity, this could happen only during 'select null' query
202204
_InternalType.Nothing: str,
203205
}

tests/integration/dbapi/async/V2/test_errors_async.py

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from firebolt.utils.exception import (
66
AccountNotFoundOrNoAccessError,
77
FireboltStructuredError,
8-
OperationalError,
98
)
109

1110

@@ -64,7 +63,7 @@ async def test_engine_name_not_exists(
6463
api_endpoint: str,
6564
) -> None:
6665
"""Connection properly reacts to invalid engine name error."""
67-
with raises(OperationalError):
66+
with raises(FireboltStructuredError) as exc_info:
6867
async with await connect(
6968
engine_name=engine_name + "_________",
7069
database=database_name,
@@ -74,6 +73,10 @@ async def test_engine_name_not_exists(
7473
) as connection:
7574
await connection.cursor().execute("show tables")
7675

76+
assert f"Engine '{engine_name}_________' does not exist" in str(
77+
exc_info.value
78+
), "Invalid engine error message."
79+
7780

7881
async def test_database_not_exists(
7982
engine_name: str,
@@ -84,7 +87,7 @@ async def test_database_not_exists(
8487
) -> None:
8588
"""Connection properly reacts to invalid database error."""
8689
new_db_name = database_name + "_"
87-
with raises(OperationalError) as exc_info:
90+
with raises(FireboltStructuredError) as exc_info:
8891
async with await connect(
8992
engine_name=engine_name,
9093
database=new_db_name,
@@ -102,23 +105,7 @@ async def test_database_not_exists(
102105
async def test_sql_error(connection: Connection) -> None:
103106
"""Connection properly reacts to SQL execution error."""
104107
with connection.cursor() as c:
105-
with raises(OperationalError) as exc_info:
106-
await c.execute("select ]")
107-
108-
assert str(exc_info.value).startswith(
109-
"Error executing query"
110-
), "Invalid SQL error message."
111-
112-
113-
async def test_structured_error(connection_system_engine_no_db: Connection) -> None:
114-
"""Connection properly reacts to structured error."""
115-
with connection_system_engine_no_db.cursor() as c:
116-
await c.execute("SET advanced_mode=1")
117-
await c.execute("SET enable_json_error_output_format=true")
118-
119108
with raises(FireboltStructuredError) as exc_info:
120-
await c.execute("select 'dummy'::int")
109+
await c.execute("select ]")
121110

122-
assert "Cannot parse string" in str(
123-
exc_info.value
124-
), "Invalid structured error message"
111+
assert "syntax error" in str(exc_info.value), "Invalid SQL error message."

tests/integration/dbapi/async/V2/test_queries_async.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,23 @@ async def test_connection_with_mixed_case_db_and_engine(
409409
await cursor.execute('CREATE TABLE "test_table" (id int)')
410410
# This fails if we're not running on a user engine
411411
await cursor.execute('INSERT INTO "test_table" VALUES (1)')
412+
413+
414+
async def test_select_geography(
415+
connection: Connection,
416+
select_geography_query: str,
417+
select_geography_description: List[Column],
418+
select_geography_response: List[ColType],
419+
) -> None:
420+
with connection.cursor() as c:
421+
await c.execute(select_geography_query)
422+
assert (
423+
c.description == select_geography_description
424+
), "Invalid description value"
425+
res = await c.fetchall()
426+
assert len(res) == 1, "Invalid data length"
427+
assert_deep_eq(
428+
res,
429+
select_geography_response,
430+
"Invalid data returned by fetchall",
431+
)

tests/integration/dbapi/async/V2/test_system_engine_async.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from firebolt.async_db import Connection
77
from firebolt.common._types import ColType, Column
8-
from firebolt.utils.exception import OperationalError
8+
from firebolt.utils.exception import FireboltStructuredError
99
from tests.integration.dbapi.utils import assert_deep_eq
1010

1111
system_error_pattern = re.compile(
@@ -49,14 +49,14 @@ async def test_system_engine(
4949

5050
if connection_system_engine.init_parameters.get("database"):
5151
await c.execute("show tables")
52-
with raises(OperationalError) as e:
52+
with raises(FireboltStructuredError) as e:
5353
# Either one or another query fails if we're not on a user engine
5454
await c.execute('create table if not exists "test_async"(id int)')
5555
await c.execute('insert into "test_async" values (1)')
5656
assert system_error_pattern.search(str(e.value)), "Invalid error message"
5757
else:
5858
await c.execute("show databases")
59-
with raises(OperationalError):
59+
with raises(FireboltStructuredError):
6060
await c.execute("show tables")
6161

6262

@@ -89,6 +89,6 @@ async def test_system_engine_use_engine(
8989
await cursor.execute(f'INSERT INTO "{table_name}" VALUES (1)')
9090
await cursor.execute('USE ENGINE "system"')
9191
# Werify we've switched to system by making previous query fail
92-
with raises(OperationalError):
92+
with raises(FireboltStructuredError):
9393
await cursor.execute(f'INSERT INTO "{table_name}" VALUES (1)')
9494
await cursor.execute(f'DROP TABLE IF EXISTS "{table_name}"')

tests/integration/dbapi/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,18 @@ def create_drop_description() -> List[Column]:
194194
Column("num_hosts_remaining", int, None, None, None, None, None),
195195
Column("num_hosts_active", int, None, None, None, None, None),
196196
]
197+
198+
199+
@fixture
200+
def select_geography_query() -> str:
201+
return "SELECT 'POINT(1 1)'::geography as \"geography\""
202+
203+
204+
@fixture
205+
def select_geography_description() -> List[Column]:
206+
return [Column("geography", str, None, None, None, None, None)]
207+
208+
209+
@fixture
210+
def select_geography_response() -> List[ColType]:
211+
return [["0101000020E6100000FEFFFFFFFFFFEF3F000000000000F03F"]]

tests/integration/dbapi/sync/V2/test_errors.py

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from pytest import mark, raises
1+
from pytest import raises
22

33
from firebolt.client.auth import ClientCredentials
44
from firebolt.db import Connection, connect
55
from firebolt.utils.exception import (
66
AccountNotFoundOrNoAccessError,
7-
FireboltDatabaseError,
87
FireboltStructuredError,
9-
OperationalError,
108
)
119

1210

@@ -63,7 +61,7 @@ def test_engine_name_not_exists(
6361
api_endpoint: str,
6462
) -> None:
6563
"""Connection properly reacts to invalid engine name error."""
66-
with raises(OperationalError):
64+
with raises(FireboltStructuredError) as exc_info:
6765
with connect(
6866
account_name=account_name,
6967
engine_name=engine_name + "_________",
@@ -73,53 +71,39 @@ def test_engine_name_not_exists(
7371
) as connection:
7472
connection.cursor().execute("show tables")
7573

74+
assert f"Engine '{engine_name}_________' does not exist" in str(
75+
exc_info.value
76+
), "Invalid engine error message."
77+
7678

77-
@mark.skip(reason="Behaviour is different in prod vs dev")
7879
def test_database_not_exists(
79-
engine_url: str,
80+
engine_name: str,
8081
database_name: str,
8182
auth: ClientCredentials,
82-
account_name: str,
8383
api_endpoint: str,
84+
account_name: str,
8485
) -> None:
8586
"""Connection properly reacts to invalid database error."""
8687
new_db_name = database_name + "_"
87-
with connect(
88-
account_name=account_name,
89-
engine_url=engine_url,
90-
database=new_db_name,
91-
auth=auth,
92-
api_endpoint=api_endpoint,
93-
) as connection:
94-
with raises(FireboltDatabaseError) as exc_info:
88+
with raises(FireboltStructuredError) as exc_info:
89+
with connect(
90+
engine_name=engine_name,
91+
database=new_db_name,
92+
auth=auth,
93+
account_name=account_name,
94+
api_endpoint=api_endpoint,
95+
) as connection:
9596
connection.cursor().execute("show tables")
9697

97-
assert (
98-
str(exc_info.value)
99-
== f"Engine {engine_name} is attached to {database_name} instead of {new_db_name}"
100-
), "Invalid database name error message"
98+
assert f"Database '{new_db_name}' does not exist or not authorized" in str(
99+
exc_info.value
100+
), "Invalid database error message."
101101

102102

103103
def test_sql_error(connection: Connection) -> None:
104104
"""Connection properly reacts to sql execution error."""
105105
with connection.cursor() as c:
106-
with raises(OperationalError) as exc_info:
107-
c.execute("select ]")
108-
109-
assert str(exc_info.value).startswith(
110-
"Error executing query"
111-
), "Invalid SQL error message"
112-
113-
114-
def test_structured_error(connection_system_engine_no_db: Connection) -> None:
115-
"""Connection properly reacts to structured error."""
116-
with connection_system_engine_no_db.cursor() as c:
117-
c.execute("SET advanced_mode=1")
118-
c.execute("SET enable_json_error_output_format=true")
119-
120106
with raises(FireboltStructuredError) as exc_info:
121-
c.execute("select 'dummy'::int")
107+
c.execute("select ]")
122108

123-
assert "Cannot parse string" in str(
124-
exc_info.value
125-
), "Invalid structured error message"
109+
assert "syntax error" in str(exc_info.value), "Invalid SQL error message."

tests/integration/dbapi/sync/V2/test_queries.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,3 +492,23 @@ def test_connection_with_mixed_case_db_and_engine(
492492
cursor.execute('CREATE TABLE "test_table" (id int)')
493493
# This fails if we're not running on a user engine
494494
cursor.execute('INSERT INTO "test_table" VALUES (1)')
495+
496+
497+
def test_select_geography(
498+
connection: Connection,
499+
select_geography_query: str,
500+
select_geography_description: List[Column],
501+
select_geography_response: List[ColType],
502+
):
503+
with connection.cursor() as c:
504+
c.execute(select_geography_query)
505+
assert (
506+
c.description == select_geography_description
507+
), "Invalid description value"
508+
res = c.fetchall()
509+
assert len(res) == 1, "Invalid data length"
510+
assert_deep_eq(
511+
res,
512+
select_geography_response,
513+
"Invalid data returned by fetchall",
514+
)

tests/integration/dbapi/sync/V2/test_system_engine.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from firebolt.common._types import ColType, Column
77
from firebolt.db import Connection
8-
from firebolt.utils.exception import OperationalError
8+
from firebolt.utils.exception import FireboltStructuredError
99
from tests.integration.dbapi.utils import assert_deep_eq
1010

1111
system_error_pattern = re.compile(
@@ -50,14 +50,14 @@ def test_system_engine(
5050

5151
if connection_system_engine.init_parameters.get("database"):
5252
c.execute("show tables")
53-
with raises(OperationalError) as e:
53+
with raises(FireboltStructuredError) as e:
5454
# Either one or another query fails if we're not on a user engine
5555
c.execute('create table if not exists "test_sync"(id int)')
5656
c.execute('insert into "test_sync" values (1)')
5757
assert system_error_pattern.search(str(e.value)), "Invalid error message"
5858
else:
5959
c.execute("show databases")
60-
with raises(OperationalError):
60+
with raises(FireboltStructuredError):
6161
c.execute("show tables")
6262

6363

@@ -90,6 +90,6 @@ def test_system_engine_use_engine(
9090
cursor.execute(f'INSERT INTO "{table_name}" VALUES (1)')
9191
cursor.execute('USE ENGINE "system"')
9292
# Verify we've switched to system by making previous query fail
93-
with raises(OperationalError):
93+
with raises(FireboltStructuredError):
9494
cursor.execute(f'INSERT INTO "{table_name}" VALUES (1)')
9595
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}"')

0 commit comments

Comments
 (0)