Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
175d4d1
Add Firebolt Core connection support
devin-ai-integration[bot] Jul 8, 2025
5d851f2
fix: Update Core connection documentation format
devin-ai-integration[bot] Jul 8, 2025
1178d40
fix: Address pre-commit formatting and line length issues
devin-ai-integration[bot] Jul 8, 2025
faf6e87
fix: Resolve line length violation in documentation comment
devin-ai-integration[bot] Jul 8, 2025
15dad9f
fix: Add firebolt-sdk core-support branch dependency for FireboltCore…
devin-ai-integration[bot] Jul 8, 2025
5b08b27
feat: Add Core connection validation and refactor _determine_auth
devin-ai-integration[bot] Jul 8, 2025
85276bd
feat: Add Core integration test workflow and docker setup
devin-ai-integration[bot] Jul 8, 2025
eb3f428
refactor: Reduce cognitive complexity of create_connect_args method
devin-ai-integration[bot] Jul 8, 2025
3337b02
fix additional parameter parsing
ptiurin Jul 8, 2025
11c6ad2
fix tests
ptiurin Jul 8, 2025
71746ca
add core to integration tests
ptiurin Jul 8, 2025
d57b5e2
register custom mark
ptiurin Jul 9, 2025
01b4eb4
docs: Add clarifying comments about SQLAlchemy URL structure mapping
devin-ai-integration[bot] Jul 9, 2025
ef14caf
refactor: Simplify Core connection validation per PR feedback
devin-ai-integration[bot] Jul 9, 2025
8b2ac55
fix: Apply black formatting to resolve CI code-check failure
devin-ai-integration[bot] Jul 9, 2025
65665b5
feat: Add integration tests for Core SDK validation behavior
devin-ai-integration[bot] Jul 9, 2025
57926be
improve tests
ptiurin Jul 9, 2025
d29ac37
fix: Attempt to resolve async dialect event loop conflicts
devin-ai-integration[bot] Jul 9, 2025
500f848
Merge remote-tracking branch 'origin/devin/1751985773-firebolt-core-s…
devin-ai-integration[bot] Jul 9, 2025
7740e63
fix: Update firebolt-sdk dependency to main branch
devin-ai-integration[bot] Jul 9, 2025
23864fe
bump python version
ptiurin Jul 9, 2025
f8a4ae1
remove hack
ptiurin Jul 9, 2025
2c23796
skip
ptiurin Jul 9, 2025
433c50a
use released version
ptiurin Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/resources/core/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"nodes": [
{
"host": "firebolt-core"
}
]
}
16 changes: 16 additions & 0 deletions .github/resources/core/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: firebolt-core

services:
firebolt-core:
image: ghcr.io/firebolt-db/firebolt-core:${IMAGE_TAG}
container_name: firebolt-core
command: --node 0
privileged: true
restart: no
ulimits:
memlock: 8589934592
ports:
- 3473:3473
volumes:
- ${BASE_DIR}/.github/resources/core/config.json:/firebolt-core/config.json:ro
- ${BASE_DIR}/firebolt-core:/firebolt-core/data
111 changes: 111 additions & 0 deletions .github/workflows/integration-tests-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Core integration tests

on:
workflow_dispatch:
inputs:
tag_version:
description: 'The docker image tag for the firebolt core'
required: false
type: string
default: 'preview-rc'
python_version:
description: 'Python version'
required: false
type: string
default: '3.8'
workflow_call:
inputs:
tag_version:
description: 'The docker image tag for the firebolt core'
required: false
type: string
default: 'preview-rc'
python_version:
description: 'Python version'
required: false
type: string
default: '3.8'

jobs:
run-core-integration-tests:
runs-on: ubuntu-latest
env:
DOCKER_COMPOSE_FILE: ${{ github.workspace }}/.github/resources/core/docker-compose.yml
SERVICE_PORT: 3473
SERVICE_URL: http://localhost:3473
MAX_RETRIES: 30
RETRY_INTERVAL: 2
steps:
- name: Check out code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python_version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ".[dev]"

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Prepare docker-compose.yml
run: |
sed -i "s|\${IMAGE_TAG}|${{ inputs.tag_version }}|g" "$DOCKER_COMPOSE_FILE"
sed -i "s|\${BASE_DIR}|${{ github.workspace }}|g" "$DOCKER_COMPOSE_FILE"

- name: Start service container
run: |
docker compose -f "$DOCKER_COMPOSE_FILE" up -d
docker compose -f "$DOCKER_COMPOSE_FILE" ps

- name: Wait for service to be ready
run: |
for i in $(seq 1 $MAX_RETRIES); do
if curl --silent --fail "$SERVICE_URL" --data-binary "SELECT 1" | grep -q "1"; then
echo "Service is up and responding!"
exit 0
fi
echo "Waiting for service... ($i/$MAX_RETRIES)"
sleep $RETRY_INTERVAL
done
echo "Error: Service failed to start within timeout"
docker compose -f "$DOCKER_COMPOSE_FILE" logs
exit 1

- name: Run Core integration tests
env:
CORE_URL: "http://localhost:3473"
run: |
pytest -o log_cli=true -o log_cli_level=INFO tests/integration -k "core" --alluredir=allure-results

- name: Stop container
if: always()
run: |
docker compose -f "$DOCKER_COMPOSE_FILE" down

# Need to pull the pages branch in order to fetch the previous runs
- name: Get Allure history
uses: actions/checkout@v4
if: always()
continue-on-error: true
with:
ref: gh-pages
path: gh-pages

- name: Allure Report
uses: firebolt-db/action-allure-report@v1
if: always()
with:
github-key: ${{ secrets.GITHUB_TOKEN }}
test-type: integration
allure-dir: allure-results
pages-branch: gh-pages
repository-name: firebolt-sqlalchemy
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests-v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
FIREBOLT_BASE_URL: "api.staging.firebolt.io"
ACCOUNT_NAME: "firebolt"
run: |
pytest --last-failed -o log_cli=true -o log_cli_level=INFO tests/integration
pytest --last-failed -o log_cli=true -o log_cli_level=INFO tests/integration -k "not core"

- name: Save failed tests
id: cache-tests-save
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
FIREBOLT_BASE_URL: "api.staging.firebolt.io"
ACCOUNT_NAME: ${{ vars.FIREBOLT_ACCOUNT }}
run: |
pytest --last-failed -o log_cli=true -o log_cli_level=INFO tests/integration --alluredir=allure-results
pytest --last-failed -o log_cli=true -o log_cli_level=INFO tests/integration -k "not core" --alluredir=allure-results

- name: Save failed tests
id: cache-tests-save
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/python-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ jobs:
integration-test-v2:
uses: ./.github/workflows/integration-tests-v2.yml
secrets: inherit
integration-test-core:
uses: ./.github/workflows/integration-tests-core.yml
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
trio_mode = true
markers =
core: mark test to run only on Firebolt Core.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ project_urls =
[options]
packages = find:
install_requires =
firebolt-sdk>=1.5.0
firebolt-sdk@git+https://github.com/firebolt-db/firebolt-python-sdk.git@core-support
sqlalchemy>=1.0.0
python_requires = >=3.8
package_dir =
Expand Down
99 changes: 83 additions & 16 deletions src/firebolt_db/firebolt_dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

import firebolt.db as dbapi
import sqlalchemy.types as sqltypes
from firebolt.client.auth import Auth, ClientCredentials, UsernamePassword
from firebolt.client.auth import (
Auth,
ClientCredentials,
FireboltCore,
UsernamePassword,
)
from firebolt.db import Cursor
from sqlalchemy.engine import Connection as AlchemyConnection
from sqlalchemy.engine import ExecutionContext, default
Expand Down Expand Up @@ -145,36 +150,93 @@ def create_connect_args(self, url: URL) -> Tuple[List, Dict]:
"""
Build firebolt-sdk compatible connection arguments.
URL format : firebolt://id:secret@host:port/db_name
For Core: firebolt://db_name?url=http://localhost:8080
(full URL including scheme, host, port in url parameter)
"""
parameters = dict(url.query)
# parameters are all passed as a string, we need to convert
# bool flag to boolean for SDK compatibility
token_cache_flag = bool(strtobool(parameters.pop("use_token_cache", "True")))
auth = _determine_auth(url.username, url.password, token_cache_flag)
is_core_connection = "url" in parameters

if is_core_connection:
self._validate_core_connection(url, parameters)

token_cache_flag = self._parse_token_cache_flag(parameters)
auth = _determine_auth(url, token_cache_flag)
kwargs = self._build_connection_kwargs(
url, parameters, auth, is_core_connection
)

return ([], kwargs)

def _validate_core_connection(self, url: URL, parameters: Dict[str, str]) -> None:
"""Validate that Core connection parameters are correct."""
if url.username or url.password:
raise ArgumentError(
"Core connections do not support username/password authentication"
)
if url.database:
raise ArgumentError("Core connections do not support engine_name parameter")
if "account_name" in parameters:
raise ArgumentError(
"Core connections do not support account_name parameter"
)

def _parse_token_cache_flag(self, parameters: Dict[str, str]) -> bool:
"""Parse and remove token cache flag from parameters."""
return bool(strtobool(parameters.pop("use_token_cache", "True")))

def _build_connection_kwargs(
self, url: URL, parameters: Dict[str, str], auth: Auth, is_core_connection: bool
) -> Dict[str, Union[str, Auth, Dict[str, Any], None]]:
"""Build connection kwargs for the SDK."""
kwargs: Dict[str, Union[str, Auth, Dict[str, Any], None]] = {
"database": url.host or None,
"auth": auth,
"engine_name": url.database,
"additional_parameters": {},
}
additional_parameters = {}

if is_core_connection:
kwargs["url"] = parameters.pop("url")

self._handle_account_name(parameters, auth, kwargs)
self._handle_environment_config(kwargs)
kwargs["additional_parameters"] = self._build_additional_parameters(parameters)
self._set_parameters = parameters

return kwargs

def _handle_account_name(
self,
parameters: Dict[str, str],
auth: Auth,
kwargs: Dict[str, Union[str, Auth, Dict[str, Any], None]],
) -> None:
"""Handle account_name parameter and validation."""
if "account_name" in parameters:
kwargs["account_name"] = parameters.pop("account_name")
elif isinstance(auth, ClientCredentials):
# account_name is required for client credentials authentication
raise ArgumentError(
"account_name parameter must be provided to authenticate"
)
self._set_parameters = parameters
# If URL override is not provided leave it to the sdk to determine the endpoint

def _handle_environment_config(
self, kwargs: Dict[str, Union[str, Auth, Dict[str, Any], None]]
) -> None:
"""Handle environment-based configuration."""
if "FIREBOLT_BASE_URL" in os.environ:
kwargs["api_endpoint"] = os.environ["FIREBOLT_BASE_URL"]
# Tracking information

def _build_additional_parameters(
self, parameters: Dict[str, str]
) -> Dict[str, Any]:
"""Build additional parameters including tracking information."""
additional_parameters: Dict[str, Any] = {}

if "user_clients" in parameters or "user_drivers" in parameters:
additional_parameters["user_drivers"] = parameters.pop("user_drivers", [])
additional_parameters["user_clients"] = parameters.pop("user_clients", [])
kwargs["additional_parameters"] = additional_parameters
return ([], kwargs)

return additional_parameters

def get_schema_names(
self, connection: AlchemyConnection, **kwargs: Any
Expand Down Expand Up @@ -366,8 +428,13 @@ def get_is_nullable(column_is_nullable: int) -> bool:
return column_is_nullable == 1


def _determine_auth(key: str, secret: str, token_cache_flag: bool = True) -> Auth:
if "@" in key:
return UsernamePassword(key, secret, token_cache_flag)
def _determine_auth(url: URL, token_cache_flag: bool = True) -> Auth:
parameters = dict(url.query)
is_core_connection = "url" in parameters

if is_core_connection:
return FireboltCore()
elif "@" in (url.username or ""):
return UsernamePassword(url.username, url.password, token_cache_flag)
else:
return ClientCredentials(key, secret, token_cache_flag)
return ClientCredentials(url.username, url.password, token_cache_flag)
18 changes: 17 additions & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def dimension_table_name() -> str:
return "test_alchemy_dimension"


@fixture(scope="class", autouse=True)
@fixture(scope="class")
def setup_test_tables(
connection: Connection,
engine: Engine,
Expand Down Expand Up @@ -233,3 +233,19 @@ def setup_test_tables(
assert not engine.dialect.has_table(connection, fact_table_name)
assert not engine.dialect.has_table(connection, dimension_table_name)
assert not engine.dialect.has_table(connection, type_table_name)


@fixture(scope="session")
def core_url() -> str:
return environ.get("CORE_URL", "http://localhost:3473")


@fixture(scope="session")
def core_engine(core_url: str) -> Engine:
return create_engine(f"firebolt://firebolt?url={core_url}")


@fixture(scope="session")
def core_connection(core_engine: Engine) -> Connection:
with core_engine.connect() as c:
yield c
36 changes: 36 additions & 0 deletions tests/integration/test_core_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
from firebolt.client.auth import FireboltCore
from sqlalchemy import text
from sqlalchemy.engine.base import Connection, Engine


@pytest.mark.core
class TestFireboltCoreIntegration:
def test_core_connection(self, core_connection: Connection):
"""Test that Core connection can be established."""
result = core_connection.execute(text("SELECT 1"))
assert result.fetchall() == [(1,)]

def test_core_engine_auth(self, core_engine: Engine):
"""Test that Core engine uses FireboltCore authentication."""
connect_args = core_engine.dialect.create_connect_args(core_engine.url)
auth = connect_args[1]["auth"]
assert isinstance(auth, FireboltCore)

def test_core_simple_query(self, core_connection: Connection):
"""Test executing a simple query against Core."""
result = core_connection.execute(text("SELECT 'Hello Core' as message"))
rows = result.fetchall()
assert len(rows) == 1
assert rows[0][0] == "Hello Core"

def test_core_no_credentials_required(self, core_engine: Engine):
"""Test that Core connection doesn't require traditional credentials."""
connect_args = core_engine.dialect.create_connect_args(core_engine.url)
result_dict = connect_args[1]

assert "url" in result_dict
assert result_dict["url"] == "http://localhost:3473"
assert isinstance(result_dict["auth"], FireboltCore)
assert result_dict["engine_name"] is None
assert "account_name" not in result_dict
2 changes: 2 additions & 0 deletions tests/integration/test_sqlalchemy_async_integration.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Dict, List

import pytest
from sqlalchemy import inspect, text
from sqlalchemy.engine.base import Connection, Engine


@pytest.mark.usefixtures("setup_test_tables")
class TestAsyncFireboltDialect:
async def test_create_ex_table(
self,
Expand Down
Loading