Skip to content
Merged
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
3 changes: 2 additions & 1 deletion build-hooks/_git.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import git
from _version import PythonicVersion, parser
from _version import PythonicVersion
from _version import parser


@parser("git")
Expand Down
18 changes: 8 additions & 10 deletions build-hooks/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
import enum
import functools
import re
from typing import (
TYPE_CHECKING,
Callable,
Generic,
MutableSequence,
Optional,
Type,
TypeVar,
overload,
)
from typing import TYPE_CHECKING
from typing import Callable
from typing import Generic
from typing import MutableSequence
from typing import Optional
from typing import Type
from typing import TypeVar
from typing import overload

try:
from typing import Self
Expand Down
109 changes: 51 additions & 58 deletions src/giql/generators/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
"""Base generator that outputs standard SQL.

Works with any SQL database that supports:
- Basic comparison operators (<, >, =, AND, OR)
- String literals
- Numeric comparisons

This generator uses only SQL-92 compatible constructs, ensuring compatibility
with virtually all SQL databases.
"""

from typing import Optional

from sqlglot import exp
Expand All @@ -30,16 +19,35 @@


class BaseGIQLGenerator(Generator):
"""Base generator for standard SQL output.
"""Base generator that outputs standard SQL.

Works with any SQL database that supports:

This generator uses only SQL-92 compatible constructs,
ensuring compatibility with virtually all SQL databases.
- Basic comparison operators (<, >, =, AND, OR)
- String literals
- Numeric comparisons

This generator uses only SQL-92 compatible constructs, ensuring
compatibility with virtually all SQL databases.
"""

# Most databases support LATERAL joins (PostgreSQL 9.3+, DuckDB 0.7.0+)
# SQLite does not support LATERAL, so it overrides this to False
SUPPORTS_LATERAL = True

@staticmethod
def _extract_bool_param(param_expr: Optional[exp.Expression]) -> bool:
"""Extract boolean value from a parameter expression.

Handles exp.Boolean, exp.Literal, and string representations.
"""
if not param_expr:
return False
elif isinstance(param_expr, exp.Boolean):
return param_expr.this
else:
return str(param_expr).upper() in ("TRUE", "1", "YES")

def __init__(self, schema_info: Optional[SchemaInfo] = None, **kwargs):
super().__init__(**kwargs)
self.schema_info = schema_info or SchemaInfo()
Expand Down Expand Up @@ -148,11 +156,8 @@ def giqlnearest_sql(self, expression: GIQLNearest) -> str:
max_distance = expression.args.get("max_distance")
max_dist_value = int(str(max_distance)) if max_distance else None

stranded = expression.args.get("stranded")
is_stranded = stranded and str(stranded).lower() in ("true", "1")

signed = expression.args.get("signed")
is_signed = signed and str(signed).lower() in ("true", "1")
is_stranded = self._extract_bool_param(expression.args.get("stranded"))
is_signed = self._extract_bool_param(expression.args.get("signed"))

# Resolve strand columns if stranded mode
ref_strand = None
Expand Down Expand Up @@ -298,27 +303,8 @@ def giqldistance_sql(self, expression: GIQLDistance) -> str:
interval_a = expression.this
interval_b = expression.args.get("expression")

# Extract stranded parameter
stranded_expr = expression.args.get("stranded")
stranded = False
if stranded_expr:
if isinstance(stranded_expr, exp.Boolean):
stranded = stranded_expr.this
elif isinstance(stranded_expr, exp.Literal):
stranded = str(stranded_expr.this).upper() == "TRUE"
else:
stranded = str(stranded_expr).upper() in ("TRUE", "1", "YES")

# Extract signed parameter
signed_expr = expression.args.get("signed")
signed = False
if signed_expr:
if isinstance(signed_expr, exp.Boolean):
signed = signed_expr.this
elif isinstance(signed_expr, exp.Literal):
signed = str(signed_expr.this).upper() == "TRUE"
else:
signed = str(signed_expr).upper() in ("TRUE", "1", "YES")
stranded = self._extract_bool_param(expression.args.get("stranded"))
signed = self._extract_bool_param(expression.args.get("signed"))

# Get SQL representations
interval_a_sql = self.sql(interval_a)
Expand Down Expand Up @@ -408,18 +394,31 @@ def _generate_distance_case(
) -> str:
"""Generate SQL CASE expression for distance calculation.

:param chrom_a: Chromosome column for interval A
:param start_a: Start column for interval A
:param end_a: End column for interval A
:param strand_a: Strand column for interval A (None if not stranded)
:param chrom_b: Chromosome column for interval B
:param start_b: Start column for interval B
:param end_b: End column for interval B
:param strand_b: Strand column for interval B (None if not stranded)
:param stranded: Whether to use strand-aware distance calculation
:param add_one_for_gap: Whether to add 1 to non-overlapping distance (bedtools compatibility)
:param signed: Whether to return signed distance (negative for upstream, positive for downstream)
:return: SQL CASE expression
:param chrom_a:
Chromosome column for interval A
:param start_a:
Start column for interval A
:param end_a:
End column for interval A
:param strand_a:
Strand column for interval A (None if not stranded)
:param chrom_b:
Chromosome column for interval B
:param start_b:
Start column for interval B
:param end_b:
End column for interval B
:param strand_b:
Strand column for interval B (None if not stranded)
:param stranded:
Whether to use strand-aware distance calculation
:param add_one_for_gap:
Whether to add 1 to non-overlapping distance (bedtools compatibility)
:param signed:
Whether to return signed distance (negative for upstream, positive for
downstream)
:return:
SQL CASE expression
"""
# Distance adjustment for non-overlapping intervals
gap_adj = " + 1" if add_one_for_gap else ""
Expand Down Expand Up @@ -829,12 +828,6 @@ def _resolve_target_table(
# Try to extract as string
table_name = str(target)

# Look up table in schema
if not self.schema_info:
raise ValueError(
f"Cannot resolve target table '{table_name}': schema_info not available"
)

table_schema = self.schema_info.get_table(table_name)
if not table_schema:
raise ValueError(
Expand Down
17 changes: 1 addition & 16 deletions src/giql/generators/duckdb.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
"""DuckDB-specific generator with optimizations.

This module provides DuckDB-specific optimizations for GIQL query generation.
"""

from sqlglot.dialects.duckdb import DuckDB

from giql.generators.base import BaseGIQLGenerator


class GIQLDuckDBGenerator(BaseGIQLGenerator, DuckDB.Generator):
"""DuckDB-specific optimizations.

Can leverage:
- Efficient list operations
- STRUCT types
- Columnar optimizations
"""

def __init__(self, schema_info=None, **kwargs):
BaseGIQLGenerator.__init__(self, schema_info=schema_info, **kwargs)
DuckDB.Generator.__init__(self, **kwargs)
"""DuckDB-specific generator with optimizations."""
27 changes: 13 additions & 14 deletions src/giql/generators/sqlite.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
"""SQLite-specific generator.

This module provides SQLite-specific SQL generation for GIQL queries.
SQLite does not support LATERAL joins, so NEAREST uses window functions instead.
"""
from typing import Final

from sqlglot.dialects.sqlite import SQLite

Expand All @@ -12,14 +8,17 @@
class GIQLSQLiteGenerator(BaseGIQLGenerator, SQLite.Generator):
"""SQLite-specific SQL generator.

Key differences from other dialects:
- No LATERAL join support - uses window functions for NEAREST
- Window functions available since SQLite 3.25.0 (2018-09-15)
"""
SQLite does not support LATERAL joins, so correlated NEAREST queries
(without explicit reference) will raise an error. Use standalone mode
with an explicit reference parameter instead.

Example::

# SQLite does not support LATERAL joins
SUPPORTS_LATERAL = False
-- This works (standalone mode with explicit reference):
SELECT * FROM NEAREST(genes, reference='chr1:1000-2000', k=3)

-- This fails (correlated mode requires LATERAL):
SELECT * FROM peaks CROSS JOIN LATERAL NEAREST(genes, k=3)
"""

def __init__(self, schema_info=None, **kwargs):
BaseGIQLGenerator.__init__(self, schema_info=schema_info, **kwargs)
SQLite.Generator.__init__(self, **kwargs)
SUPPORTS_LATERAL: Final = False
Empty file added tests/generators/__init__.py
Empty file.
Loading