Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
141 changes: 141 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: CI

on:
push:
branches: [ '**' ]
tags: [ 'v*' ]

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
firebird-version: [3, 4, 5]
os: [ubuntu-22.04, windows-latest]
include:
- firebird-version: 3
db-file: fbtest30.fdb
fb-semver: '3.0.13'
- firebird-version: 4
db-file: fbtest40.fdb
fb-semver: '4.0.6'
- firebird-version: 5
db-file: fbtest50.fdb
fb-semver: '5.0.3'

defaults:
run:
shell: pwsh

env:
GITHUB_TOKEN: ${{ github.token }}
ISC_USER: SYSDBA
ISC_PASSWORD: masterkey

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Hatch
run: pip install hatch

- name: Install PSFirebird
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ForceBootstrap -ErrorAction Continue
Install-Module -Name PSFirebird -MinimumVersion 1.2.2 -Force -AllowClobber -Scope CurrentUser -Repository PSGallery

- name: Create Firebird environment
run: |
$tempPath = if ($IsWindows) { $env:TEMP } else { '/tmp' }
$fbPath = Join-Path $tempPath 'firebird-${{ matrix.firebird-version }}'
$fbEnv = New-FirebirdEnvironment -Version '${{ matrix.fb-semver }}' -Path $fbPath
Write-Output $fbEnv
Write-Output "FB_PATH=$fbPath" >> $env:GITHUB_ENV
Write-Output "FB_CLIENT_LIB=$($fbEnv.GetClientLibraryPath())" >> $env:GITHUB_ENV

- name: Configure and start Firebird
run: |
# Configure RemoteAuxPort for Firebird events support
$confPath = Join-Path $env:FB_PATH 'firebird.conf'
Write-FirebirdConfiguration -Path $confPath -Configuration @{ RemoteAuxPort = 3051 }
# Initialize SYSDBA via embedded connection (ISC_USER/ISC_PASSWORD env vars are set at job level)
$initDb = New-FirebirdDatabase -Database (Join-Path $env:FB_PATH 'init.fdb') -Environment $env:FB_PATH
"CREATE USER SYSDBA PASSWORD 'masterkey';" | Invoke-FirebirdIsql -Database $initDb -Environment $env:FB_PATH
Remove-FirebirdDatabase -Database $initDb -Force
# Seed firebird.log so get_log() service API calls return non-empty content
# (Firebird does not write startup messages in this mode; tests assert log is non-empty)
"Firebird`tServer started" | Out-File -FilePath (Join-Path $env:FB_PATH 'firebird.log') -Encoding utf8 -NoNewline:$false
# Start Firebird server
Start-FirebirdInstance -Environment $env:FB_PATH | Write-Output

- name: Run tests
run: hatch test -- --host=localhost --port=3050 "--client-lib=$env:FB_CLIENT_LIB" -v
env:
FIREBIRD_VERSION: ${{ matrix.firebird-version }}

- name: Stop Firebird instance
if: always()
run: Get-FirebirdInstance | Stop-FirebirdInstance

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Hatch
run: pip install hatch

- name: Build package
run: hatch build

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # Required for trusted publishing to PyPI
steps:
- uses: actions/checkout@v4

- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
files: dist/*

# GitHub Packages does not currently support Python packages -- https://github.com/orgs/community/discussions/8542

# - name: Publish to GitHub Packages
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://pypi.pkg.github.com/fdcastel

- name: Publish to PyPI
if: github.repository == 'FirebirdSQL/python3-driver'
uses: pypa/gh-action-pypi-publish@release/v1
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling"]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[project]
Expand Down Expand Up @@ -40,7 +40,7 @@ Funding = "https://github.com/sponsors/pcisar"
Source = "https://github.com/FirebirdSQL/python3-driver"

[tool.hatch.version]
path = "src/firebird/driver/__init__.py"
source = "vcs"

[tool.hatch.build.targets.sdist]
include = ["src"]
Expand Down
6 changes: 5 additions & 1 deletion src/firebird/driver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,8 @@
)

#: Current driver version, SEMVER string.
__VERSION__ = '2.0.2'
try:
from importlib.metadata import version as _get_version
__VERSION__ = _get_version('firebird-driver')
except Exception:
__VERSION__ = 'unknown'
35 changes: 28 additions & 7 deletions src/firebird/driver/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2164,9 +2164,31 @@ def _connect_helper(dsn: str, host: str, port: str, database: str, protocol: Net
if protocol is not None:
dsn = f'{protocol.name.lower()}://'
if host and port:
dsn += f'{host}:{port}/'
dsn += f'{host}:{port}'
elif host:
dsn += f'{host}/'
dsn += host
# Add database path
# When there's a host, URLs need proper path formatting:
# - Unix absolute paths (start with '/') - need double slash to preserve the leading /
# because Firebird URL parsing strips one /
# - Windows absolute paths (contain ':') - concatenate directly without separator
# - Aliases/relative paths - need '/' separator
# When there's no host (loopback), the path is used as-is
if host:
# For URLs with host
if database.startswith('/'):
# Unix absolute path - use double slash so Firebird keeps the leading /
dsn += f'/{database}' # Results in inet://host//absolute/path
elif ':' in database: # Windows path (e.g., C:\...)
dsn += f'/{database}' # Results in inet://host:port/D:\path
else: # Relative/alias
dsn += f'/{database}'
else:
# Loopback - path is used as-is after ://
if database.startswith('/') or ':' in database:
dsn += database
else:
dsn += f'/{database}'
else:
dsn = ''
if host and host.startswith('\\\\'): # Windows Named Pipes
Expand All @@ -2178,7 +2200,7 @@ def _connect_helper(dsn: str, host: str, port: str, database: str, protocol: Net
dsn += f'{host}/{port}:'
elif host:
dsn += f'{host}:'
dsn += database
dsn += database
return dsn

def _is_dsn(value: str) -> bool:
Expand Down Expand Up @@ -2401,10 +2423,9 @@ def create_database(database: str | Path, *, user: str | None=None, password: st
if db_config is None:
db_config = driver_config.db_defaults
srv_config = driver_config.server_defaults
if _is_dsn(database):
dsn = database
database = None
srv_config.host.clear()
dsn = database
database = None
srv_config.host.clear()
else:
database = db_config.database.value
dsn = db_config.dsn.value
Expand Down
12 changes: 8 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def pytest_configure(config):
client_lib = Path(client_lib)
if not client_lib.is_file():
pytest.exit(f"Client library '{client_lib}' not found!")
driver_config.fb_client_library.value = client_lib
driver_config.fb_client_library.value = str(client_lib)
#
if host := config.getoption('host'):
_vars_['host'] = host
Expand Down Expand Up @@ -194,7 +194,8 @@ def tmp_dir(tmp_path_factory):

@pytest.fixture(scope='session', autouse=True)
def db_file(tmp_dir):
test_db_filename: Path = tmp_dir / 'test-db.fdb'
# Always use local tmp_dir - in CI, this will be bind-mounted to the container
test_db_filename = tmp_dir / 'test-db.fdb'
copyfile(_vars_['source_db'], test_db_filename)
if _platform != 'Windows':
test_db_filename.chmod(33206)
Expand All @@ -208,7 +209,9 @@ def dsn(db_file):
if host is None:
result = str(db_file)
else:
result = f'{host}/{port}:{db_file}' if port else f'{host}:{db_file}'
# For remote servers, use the absolute path string of the local file
# which will be the same path inside the container due to bind mount
result = f'{host}/{port}:{str(db_file)}' if port else f'{host}:{str(db_file)}'
yield result

@pytest.fixture()
Expand All @@ -233,8 +236,9 @@ def db_cleanup(db_connection):
cur.execute("delete from t")
cur.execute("delete from t2")
cur.execute("delete from FB4")
db_connection.commit()
db_connection.commit()
except Exception as e:
db_connection.rollback()
# Ignore errors if tables don't exist, log others
if "Table unknown" not in str(e):
print(f"Warning: Error during pre-test cleanup: {e}")
Expand Down
1 change: 1 addition & 0 deletions tests/test_blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def test_stream_blob_basic(db_connection):
def test_stream_blob_extended(db_connection):
blob_content = "Another test blob content." * 5 # Make it slightly longer
with db_connection.cursor() as cur:
cur.execute('delete from T2 where C1 in (1, 2)')
cur.execute('insert into T2 (C1,C9) values (?,?)', [1, StringIO(blob_content)])
cur.execute('insert into T2 (C1,C9) values (?,?)', [2, StringIO(blob_content)])
db_connection.commit()
Expand Down
Loading