Skip to content

Commit f122cdb

Browse files
authored
Extend Github Action Workflow and DB connection for multiple dialects (#387)
1 parent bfdebac commit f122cdb

File tree

43 files changed

+1651
-23
lines changed

Some content is hidden

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

43 files changed

+1651
-23
lines changed

.github/workflows/pr_testing.yml

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@ name: PR Testing
33
on:
44
pull_request:
55
workflow_dispatch:
6+
inputs:
7+
py310:
8+
description: "Python 3.10"
9+
type: boolean
10+
default: true
11+
py311:
12+
description: "Python 3.11"
13+
type: boolean
14+
default: true
15+
py312:
16+
description: "Python 3.12"
17+
type: boolean
18+
default: true
19+
run-python:
20+
description: "Run Main PyDough Tests"
21+
type: boolean
22+
required: false
23+
default: true
24+
run-sf:
25+
description: "Run Snowflake Tests"
26+
type: boolean
27+
required: false
28+
default: false
629

730
# Limit CI to cancel previous runs in the same PR
831
concurrency:
@@ -26,23 +49,70 @@ jobs:
2649
run: |
2750
set -xe pipefail
2851
echo "commitMsg=$(git log -1 --pretty=format:'%s')" >> $GITHUB_OUTPUT
29-
30-
run-tests:
31-
strategy:
32-
matrix:
33-
python-version:
34-
- "3.10"
35-
- "3.11"
36-
- "3.12"
37-
needs: [get-msg]
38-
name: python
52+
# Get Python versions from inputs if workflow_dispatch is used
53+
# Otherwise, use all versions for pull_request events.
54+
get-py-ver-matrix:
55+
name: Get Python Version Matrix
3956
runs-on: ubuntu-latest
57+
outputs:
58+
# Get the matrix output produced by the step below (set-matrix).
59+
# This will be used in the run tests job.
60+
matrix: ${{ steps.set-matrix.outputs.matrix }}
61+
steps:
62+
# Capture the Python versions selected via dispatch as a JSON array output
63+
# For manual runs (workflow_dispatch), it uses the selected
64+
# inputs to determine which versions to run.
65+
# For other events like pull_request, it defaults to all versions.
66+
- name: Set selected Python versions
67+
id: set-matrix
68+
run: |
69+
versions=()
70+
71+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
72+
if [ "${{ inputs.py310 }}" == "true" ]; then
73+
versions+=("\"3.10\"")
74+
fi
75+
if [ "${{ inputs.py311 }}" == "true" ]; then
76+
versions+=("\"3.11\"")
77+
fi
78+
if [ "${{ inputs.py312 }}" == "true" ]; then
79+
versions+=("\"3.12\"")
80+
fi
81+
else
82+
# For pull_request and other events, use all versions by default
83+
versions=( "\"3.10\"" "\"3.11\"" "\"3.12\"" )
84+
fi
85+
# Join the array elements with commas and wrap in brackets to make valid JSON
86+
joined=$(IFS=, ; echo "[${versions[*]}]")
87+
88+
# Output to GitHub Actions expected format
89+
echo "matrix=$joined" >> $GITHUB_OUTPUT
90+
91+
92+
93+
run-python-tests:
94+
name: Main Python Tests
95+
needs: [get-msg, get-py-ver-matrix]
4096
# https://docs.github.com/en/actions/learn-github-actions/expressions#contains
4197
# contains is case-insensitive
42-
if: contains(needs.get-msg.outputs.commitMsg, '[run ci]')
98+
if: |
99+
(github.event_name == 'pull_request' && contains(needs.get-msg.outputs.commitMsg, '[run ci]')) ||
100+
(github.event_name == 'workflow_dispatch' && inputs.run-python)
101+
runs-on: ubuntu-latest
102+
strategy:
103+
matrix:
104+
python-version: ${{ github.event_name == 'workflow_dispatch'
105+
&& fromJSON(needs.get-py-ver-matrix.outputs.matrix)
106+
|| fromJSON('["3.10", "3.11", "3.12"]') }}
107+
43108
steps:
44-
- name: Checkout code
45-
uses: actions/checkout@v4
109+
- uses: actions/checkout@v4
110+
111+
- name: Setup Python ${{ matrix.python-version }}
112+
id: setup-python
113+
uses: actions/setup-python@v4
114+
with:
115+
python-version: ${{ matrix.python-version }}
46116

47117
- name: Install uv
48118
uses: astral-sh/setup-uv@v3
@@ -54,7 +124,23 @@ jobs:
54124
run: ./demos/setup_tpch.sh ./tpch.db
55125

56126
- name: Run Ruff
57-
run: uv run --python ${{ matrix.python-version }} ruff check .
127+
run: uv run ruff check .
58128

59129
- name: Run Tests
60-
run: uv run --python ${{ matrix.python-version }} pytest tests/
130+
run: uv run pytest tests/ -m "not snowflake" -rs
131+
132+
run-sf-tests:
133+
name: Snowflake Tests
134+
needs: [get-msg, get-py-ver-matrix]
135+
if: |
136+
(github.event_name == 'pull_request' && contains(needs.get-msg.outputs.commitMsg, '[run sf]')) ||
137+
(github.event_name == 'workflow_dispatch' && inputs.run-sf)
138+
uses: ./.github/workflows/sf_testing.yml
139+
secrets:
140+
SF_USERNAME: ${{ secrets.SF_USERNAME }}
141+
SF_PASSWORD: ${{ secrets.SF_PASSWORD }}
142+
SF_ACCOUNT: ${{ secrets.SF_ACCOUNT }}
143+
with:
144+
python-versions: ${{ github.event_name == 'workflow_dispatch'
145+
&& needs.get-py-ver-matrix.outputs.matrix
146+
|| '["3.10", "3.11", "3.12"]' }}

.github/workflows/sf_testing.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Run Snowflake Tests
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
python-versions:
7+
description: "JSON string of Python versions"
8+
type: string
9+
required: true
10+
secrets:
11+
SF_USERNAME:
12+
required: true
13+
SF_PASSWORD:
14+
required: true
15+
SF_ACCOUNT:
16+
required: true
17+
18+
jobs:
19+
sf-tests:
20+
name: Snowflake Tests (Python ${{ matrix.python-version }})
21+
runs-on: ubuntu-latest
22+
strategy:
23+
matrix:
24+
python-version: ${{ fromJSON(inputs.python-versions) }}
25+
26+
env:
27+
SF_USERNAME: ${{ secrets.SF_USERNAME }}
28+
SF_PASSWORD: ${{ secrets.SF_PASSWORD }}
29+
SF_ACCOUNT: ${{ secrets.SF_ACCOUNT }}
30+
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- name: Setup Python ${{ matrix.python-version }}
35+
id: setup-python
36+
uses: actions/setup-python@v4
37+
with:
38+
python-version: ${{ matrix.python-version }}
39+
40+
- name: Install uv
41+
uses: astral-sh/setup-uv@v3
42+
with:
43+
version: "0.4.23"
44+
45+
- name: Create virtual environment
46+
# uv requires an existing virtual environment to install packages.
47+
# Running `uv venv` creates the `.venv` directory so that subsequent
48+
# `uv pip install` commands install dependencies inside this environment.
49+
# Without this step, `uv pip install` fails with
50+
# "No virtual environment found".
51+
run: uv venv
52+
53+
- name: Install dependencies
54+
run: uv pip install -e ".[snowflake]"
55+
56+
- name: Confirm Snowflake connector is installed
57+
run: uv run python -c "import snowflake.connector; print(snowflake.connector.__version__)"
58+
59+
- name: Run Snowflake Tests
60+
run: uv run pytest -m snowflake tests/ -rs

documentation/usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ pydough.active_session.database = education_context
351351
shakespeare_context = pydough.active_session.load_database("sqlite", database="db_files/education.db")
352352
```
353353

354-
Notice that both APIs `load_database_context` and `sesion.load_database` take in the name of the databse type first and all the connection keyword arguments, and also return the context object.
354+
Notice that both APIs `load_database_context` and `sesion.load_database` take in the name of the database type first and all the connection keyword arguments, and also return the context object.
355355

356356
It is important to ensure that the correct database context is being used for several reasons:
357357
- It controls what SQL dialect is used when translating from PyDough to SQL.

pydough/database_connectors/database_connector.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
"""
66
# Copyright (C) 2024 Bodo Inc. All rights reserved.
77

8-
import sqlite3
98
from dataclasses import dataclass
109
from enum import Enum
1110

1211
import pandas as pd
1312

13+
from .db_types import DBConnection, DBCursor
14+
1415
__all__ = ["DatabaseConnection", "DatabaseContext", "DatabaseDialect"]
1516

1617

@@ -24,9 +25,9 @@ class DatabaseConnection:
2425
# Database connection that follows DB API 2.0 specification.
2526
# sqlite3 contains the connection specification and is packaged
2627
# with Python.
27-
_connection: sqlite3.Connection
28+
_connection: DBConnection
2829

29-
def __init__(self, connection: sqlite3.Connection) -> None:
30+
def __init__(self, connection: DBConnection) -> None:
3031
self._connection = connection
3132

3233
def execute_query_df(self, sql: str) -> pd.DataFrame:
@@ -42,10 +43,10 @@ def execute_query_df(self, sql: str) -> pd.DataFrame:
4243
Returns:
4344
list[pt.Any]: A list of rows returned by the query.
4445
"""
45-
cursor: sqlite3.Cursor = self._connection.cursor()
46+
cursor: DBCursor = self._connection.cursor()
4647
try:
4748
cursor.execute(sql)
48-
except sqlite3.OperationalError as e:
49+
except Exception as e:
4950
print(f"ERROR WHILE EXECUTING QUERY:\n{sql}")
5051
raise e
5152
column_names: list[str] = [description[0] for description in cursor.description]
@@ -59,13 +60,13 @@ def execute_query_df(self, sql: str) -> pd.DataFrame:
5960
# how this will be available at a user API level.
6061

6162
@property
62-
def connection(self) -> sqlite3.Connection:
63+
def connection(self) -> DBConnection:
6364
"""
6465
Get the database connection. This API may be removed if all
6566
the functionality can be encapsulated in the DatabaseConnection.
6667
6768
Returns:
68-
sqlite3.Connection: The connection PyDough is managing.
69+
The database connection PyDough is managing.
6970
"""
7071
return self._connection
7172

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Type aliases for database connections and cursors used in type hints.
3+
use `if TYPE_CHECKING:` to import database-specific modules in a way
4+
that allows static type checkers to understand the types without triggering
5+
runtime imports. This avoids runtime errors when some optional dependencies
6+
are not installed.
7+
"""
8+
9+
from typing import TYPE_CHECKING, Any, TypeAlias
10+
11+
if TYPE_CHECKING:
12+
# Importing database-specific modules only for type checking
13+
# This allows us to use type hints for SQL dialect connections
14+
# (SQLite, ..etc.)
15+
# without requiring these modules at runtime unless they are actually used.
16+
import sqlite3
17+
18+
SQLiteConn: TypeAlias = sqlite3.Connection
19+
SQLiteCursor: TypeAlias = sqlite3.Cursor
20+
21+
# TBD: Placeholder lines to add other dialects.
22+
# 1. Replace with actual dialect module
23+
# import dialect1_module
24+
# 2. Replace with other dialect connections
25+
# Dialect1_Conn: TypeAlias = dialect1_module.Connection
26+
# 3. Replace with other dialect cursors
27+
# Dialect1_Cursor: TypeAlias = dialect1_module.Cursor
28+
29+
# 4. Define the type aliases for database connections and cursors
30+
DBConnection: TypeAlias = SQLiteConn # | Dialect1_Conn
31+
DBCursor: TypeAlias = SQLiteCursor # | Dialect1_Cursor
32+
else:
33+
DBConnection: TypeAlias = Any
34+
DBCursor: TypeAlias = Any
35+
SQLiteConn: TypeAlias = Any
36+
SQLiteCursor: TypeAlias = Any
37+
# Dialect1_Conn: TypeAlias = Any
38+
# Dialect1_Cursor: TypeAlias = Any
39+
40+
# This allows us to use these type aliases in the rest of the code
41+
# without worrying about whether the specific database modules are available.
42+
__all__ = [
43+
"DBConnection",
44+
"DBCursor",
45+
"SQLiteConn",
46+
"SQLiteCursor",
47+
# "Dialect1_Conn",
48+
# "Dialect1_Cursor",
49+
]
50+
# The type aliases are used to provide a consistent interface for database connections
51+
# and cursors across different database backends, allowing for easier
52+
# type hinting and code readability without requiring the actual database modules.

0 commit comments

Comments
 (0)