Skip to content

Commit 8683071

Browse files
authored
Add error handling class that can be subclassed to customize error messages (#389)
Resolves #388. Also heavily refactors a lot of the error-related code, including moving all of the exception utilities/classes to a common module, and removing the `.with_xxx` patterns (unnecessarily) being used for QDAG nodes.
1 parent cb6e86a commit 8683071

File tree

106 files changed

+1628
-1309
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+1628
-1309
lines changed

pydough/configs/pydough_configs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from enum import Enum
88
from typing import Any, Generic, TypeVar
99

10+
from pydough.errors import PyDoughSessionException
11+
1012
T = TypeVar("T")
1113

1214

@@ -126,5 +128,5 @@ class PyDoughConfigs:
126128

127129
def __setattr__(self, name: str, value: Any) -> None:
128130
if name not in dir(self):
129-
raise AttributeError(f"Unrecognized PyDough config name: {name}")
131+
raise PyDoughSessionException(f"Unrecognized PyDough config name: {name}")
130132
super().__setattr__(name, value)

pydough/configs/session.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- The active metadata graph.
66
- Any PyDough configuration for function behavior.
77
- Backend information (SQL dialect, Database connection, etc.)
8+
- The error builder used to create and format exceptions
89
910
In the future this session will also contain other information
1011
such as any User Defined registration for additional backend
@@ -24,6 +25,7 @@
2425
empty_connection,
2526
load_database_context,
2627
)
28+
from pydough.errors import PyDoughErrorBuilder
2729
from pydough.metadata import GraphMetadata, parse_json_metadata_from_file
2830

2931
from .pydough_configs import PyDoughConfigs
@@ -47,6 +49,7 @@ def __init__(self) -> None:
4749
self._database: DatabaseContext = DatabaseContext(
4850
connection=empty_connection, dialect=DatabaseDialect.ANSI
4951
)
52+
self._error_builder: PyDoughErrorBuilder = PyDoughErrorBuilder()
5053

5154
@property
5255
def metadata(self) -> GraphMetadata | None:
@@ -108,6 +111,26 @@ def database(self, context: DatabaseContext) -> None:
108111
"""
109112
self._database = context
110113

114+
@property
115+
def error_builder(self) -> PyDoughErrorBuilder:
116+
"""
117+
Get the active error builder.
118+
119+
Returns:
120+
The active error builder.
121+
"""
122+
return self._error_builder
123+
124+
@error_builder.setter
125+
def error_builder(self, builder: PyDoughErrorBuilder) -> None:
126+
"""
127+
Set the active error builder context.
128+
129+
Args:
130+
The error builder to set.
131+
"""
132+
self._error_builder = builder
133+
111134
def connect_database(self, database_name: str, **kwargs) -> DatabaseContext:
112135
"""
113136
Create a new DatabaseContext and register it in the session. This returns

pydough/conversion/hybrid_translator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pydough.pydough_operators as pydop
1010
from pydough.configs import PyDoughConfigs
1111
from pydough.database_connectors import DatabaseDialect
12+
from pydough.errors import PyDoughSQLException
1213
from pydough.metadata import (
1314
CartesianProductMetadata,
1415
GeneralJoinMetadata,
@@ -810,7 +811,7 @@ def rewrite_quantile_call(
810811
or not isinstance(expr.args[1].literal.value, (int, float))
811812
or not (0.0 <= float(expr.args[1].literal.value) <= 1.0)
812813
):
813-
raise ValueError(
814+
raise PyDoughSQLException(
814815
f"Expected second argument to QUANTILE to be a numeric literal between 0 and 1, instead received {expr.args[1]!r}"
815816
)
816817

pydough/conversion/hybrid_tree.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ def add_successor(self, successor: "HybridTree") -> None:
605605
`successor`: the HybridTree to be marked as one level below `self`.
606606
"""
607607
if self._successor is not None:
608-
raise Exception("Duplicate successor")
608+
raise ValueError("Duplicate successor")
609609
self._successor = successor
610610
successor._parent = self
611611
# Shift the aggregation keys and rhs of join keys back by 1 level to

pydough/conversion/projection_pullup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ def simplify_agg(
455455
# Otherwise, FUNC(key) -> key
456456
return key_ref, None
457457

458-
# If running a selection aggregation on a literal, can just return the
459-
# input.
458+
# If running a selection aggregation on a literal or a grouping key, can
459+
# just return the input.
460460
if (
461461
agg.op
462462
in (
@@ -470,7 +470,7 @@ def simplify_agg(
470470
and len(agg.inputs) >= 1
471471
):
472472
arg = agg.inputs[0]
473-
if isinstance(arg, LiteralExpression):
473+
if isinstance(arg, LiteralExpression) or arg in reverse_keys:
474474
return arg, None
475475
# In all other cases, we just return the aggregation as is.
476476
return out_ref, agg

pydough/conversion/relational_converter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,8 +1346,7 @@ def preprocess_root(
13461346
column_typ: PyDoughType = node.get_expr(column).pydough_type
13471347
final_terms.append((column, Reference(node, column, column_typ)))
13481348
children: list[PyDoughCollectionQDAG] = []
1349-
final_calc: Calculate = Calculate(node, children).with_terms(final_terms)
1350-
return final_calc
1349+
return Calculate(node, children, final_terms)
13511350

13521351

13531352
def make_relational_ordering(

pydough/database_connectors/builtin_databases.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sqlite3
77
import time
88

9+
from pydough.errors import PyDoughSessionException
10+
911
from .database_connector import DatabaseConnection, DatabaseContext, DatabaseDialect
1012

1113
__all__ = [
@@ -43,7 +45,7 @@ def load_database_context(database_name: str, **kwargs) -> DatabaseContext:
4345
connection = load_mysql_connection(**kwargs)
4446
dialect = DatabaseDialect.MYSQL
4547
case _:
46-
raise ValueError(
48+
raise PyDoughSessionException(
4749
f"Unsupported database: {database_name}. The supported databases are: {supported_databases}."
4850
"Any other database must be created manually by specifying the connection and dialect."
4951
)
@@ -59,7 +61,7 @@ def load_sqlite_connection(**kwargs) -> DatabaseConnection:
5961
A database connection object for SQLite.
6062
"""
6163
if "database" not in kwargs:
62-
raise ValueError("SQLite connection requires a database path.")
64+
raise PyDoughSessionException("SQLite connection requires a database path.")
6365
connection: sqlite3.Connection = sqlite3.connect(**kwargs)
6466
return DatabaseConnection(connection)
6567

pydough/database_connectors/database_connector.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
by leveraging PEP 249 (Python Database API Specification v2.0).
44
https://peps.python.org/pep-0249/
55
"""
6-
# Copyright (C) 2024 Bodo Inc. All rights reserved.
6+
7+
__all__ = ["DatabaseConnection", "DatabaseContext", "DatabaseDialect"]
78

89
from dataclasses import dataclass
910
from enum import Enum
1011
from typing import TYPE_CHECKING, cast
1112

1213
import pandas as pd
1314

14-
from .db_types import DBConnection, DBCursor, SnowflakeCursor
15+
import pydough
16+
from pydough.errors import PyDoughSessionException
1517

16-
__all__ = ["DatabaseConnection", "DatabaseContext", "DatabaseDialect"]
18+
from .db_types import DBConnection, DBCursor, SnowflakeCursor
1719

1820

1921
class DatabaseConnection:
@@ -51,7 +53,9 @@ def execute_query_df(self, sql: str) -> pd.DataFrame:
5153
self.cursor.execute(sql)
5254
except Exception as e:
5355
print(f"ERROR WHILE EXECUTING QUERY:\n{sql}")
54-
raise e
56+
raise pydough.active_session.error_builder.sql_runtime_failure(
57+
sql, e, True
58+
) from e
5559

5660
# This is only for MyPy to pass and know about fetch_pandas_all()
5761
# NOTE: Code does not run in type checking mode, so we need to
@@ -119,7 +123,7 @@ def from_string(dialect: str) -> "DatabaseDialect":
119123
if dialect in DatabaseDialect.__members__:
120124
return DatabaseDialect.__members__[dialect]
121125
else:
122-
raise ValueError(f"Unsupported dialect: {dialect}")
126+
raise PyDoughSessionException(f"Unsupported dialect: {dialect}")
123127

124128

125129
@dataclass

pydough/database_connectors/empty_connection.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
__all__ = ["empty_connection"]
1111

12+
from pydough.errors import PyDoughSessionException
13+
1214
from .database_connector import DatabaseConnection
1315

1416

@@ -22,16 +24,16 @@ def __init__(self):
2224
pass
2325

2426
def commit(self):
25-
raise ValueError("No SQL Database is specified.")
27+
raise PyDoughSessionException("No SQL Database is specified.")
2628

2729
def close(self):
28-
raise ValueError("No SQL Database is specified.")
30+
raise PyDoughSessionException("No SQL Database is specified.")
2931

3032
def rollback(self):
31-
raise ValueError("No SQL Database is specified.")
33+
raise PyDoughSessionException("No SQL Database is specified.")
3234

3335
def cursor(self, *args, **kwargs):
34-
raise ValueError("No SQL Database is specified.")
36+
raise PyDoughSessionException("No SQL Database is specified.")
3537

3638

3739
empty_connection: DatabaseConnection = DatabaseConnection(EmptyConnection())

pydough/errors/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Module for error handling in PyDough.
3+
"""
4+
5+
__all__ = [
6+
"PyDoughErrorBuilder",
7+
"PyDoughException",
8+
"PyDoughMetadataException",
9+
"PyDoughQDAGException",
10+
"PyDoughSQLException",
11+
"PyDoughSessionException",
12+
"PyDoughTestingException",
13+
"PyDoughTypeException",
14+
"PyDoughUnqualifiedException",
15+
]
16+
17+
from .error_types import (
18+
PyDoughException,
19+
PyDoughMetadataException,
20+
PyDoughQDAGException,
21+
PyDoughSessionException,
22+
PyDoughSQLException,
23+
PyDoughTestingException,
24+
PyDoughTypeException,
25+
PyDoughUnqualifiedException,
26+
)
27+
from .pydough_error_builder import PyDoughErrorBuilder

0 commit comments

Comments
 (0)