-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from baluyotraf/initial
Initial alpha version
- Loading branch information
Showing
8 changed files
with
465 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
repos: | ||
- repo: https://github.com/pre-commit/pre-commit-hooks | ||
rev: v2.3.0 | ||
hooks: | ||
- id: check-yaml | ||
- id: end-of-file-fixer | ||
- id: trailing-whitespace | ||
|
||
- repo: https://github.com/pre-commit/mirrors-prettier | ||
rev: "v3.1.0" | ||
hooks: | ||
- id: prettier | ||
args: [--prose-wrap, always] | ||
types_or: [ markdown ] | ||
|
||
- repo: https://github.com/astral-sh/ruff-pre-commit | ||
rev: v0.3.3 | ||
hooks: | ||
- id: ruff | ||
args: [ --fix ] | ||
- id: ruff-format | ||
|
||
- repo: https://github.com/RobertCraigie/pyright-python | ||
rev: v1.1.354 | ||
hooks: | ||
- id: pyright | ||
language: system | ||
types: [python] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,139 @@ | ||
# altqq | ||
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) | ||
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) | ||
[![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/) | ||
|
||
# Alternative Queries | ||
|
||
Alternative queries is a library created to help with handcrafted SQL queries. | ||
It works by providing a class that represent the queries, with parameters type | ||
checked by `Pydantic`. | ||
|
||
The library is currently still in development and has an alpha release. | ||
|
||
## Installation | ||
|
||
The library is available in PyPI | ||
|
||
```bash | ||
pip install altqq | ||
``` | ||
|
||
## Basic Usage | ||
|
||
To use the library, you can define a class that represents a query. Then this | ||
query can be converted to plain text or `pyodbc` usable query. | ||
|
||
```python | ||
import altqq | ||
|
||
class SelectUserByFirstName(altqq.Query): | ||
__query__ = """ | ||
SELECT * FROM "Users" | ||
WHERE first_name = {first_name} | ||
""" | ||
first_name: str | ||
|
||
q = altqq.to_pyodbc(SelectUserByFirstName(first_name="arietta")) | ||
print(q.query) | ||
print(q.parameters) | ||
``` | ||
|
||
Running the code above should give the result below: | ||
|
||
```bash | ||
|
||
SELECT * FROM "Users" | ||
WHERE first_name = ? | ||
|
||
['arietta'] | ||
``` | ||
|
||
## Templating Non-Parameters | ||
|
||
By default, the class properties are treated as parameters. If there's a need | ||
for more customization, they can be declared as `altqq.NonParameter`. | ||
|
||
```python | ||
import altqq | ||
|
||
class SelectByFirstName(altqq.Query): | ||
__query__ = """ | ||
SELECT * FROM "{table}" | ||
WHERE first_name = {first_name} | ||
""" | ||
first_name: str | ||
table: altqq.NonParameter[str] | ||
|
||
q = altqq.to_pyodbc(SelectByFirstName( | ||
first_name="arietta", | ||
table="Users" | ||
)) | ||
print(q.query) | ||
print(q.parameters) | ||
``` | ||
|
||
Running the code above should give the result below: | ||
|
||
```bash | ||
|
||
SELECT * FROM "Users" | ||
WHERE first_name = ? | ||
|
||
['arietta'] | ||
``` | ||
|
||
## Nested Queries | ||
|
||
Queries can also use other queries. When passed to the functions for conversion, | ||
other queries will also be converted. | ||
|
||
```python | ||
import altqq | ||
|
||
class SelectUserByFirstName(altqq.Query): | ||
__query__ = """ | ||
SELECT * FROM "Users" | ||
WHERE first_name = {first_name} | ||
""" | ||
first_name: str | ||
|
||
|
||
class SelectSubqueryByAge(altqq.Query): | ||
__query__ = """ | ||
SELECT * FROM ({subquery}) AS tbl | ||
WHERE tbl.age = {age} | ||
""" | ||
age: int | ||
subquery: altqq.Query | ||
|
||
q = altqq.to_pyodbc(SelectSubqueryByAge( | ||
age=20, | ||
subquery=SelectUserByFirstName( | ||
first_name="arietta" | ||
) | ||
)) | ||
print(q.query) | ||
print(q.parameters) | ||
``` | ||
|
||
Running the code above should give the result below: | ||
|
||
```bash | ||
|
||
SELECT * FROM ( | ||
SELECT * FROM "Users" | ||
WHERE first_name = ? | ||
) AS tbl | ||
WHERE tbl.age = ? | ||
|
||
['arietta', 20] | ||
``` | ||
|
||
## Road Map | ||
|
||
Below is the list of things planned for the library | ||
|
||
- Documentation Page | ||
- Tests | ||
- Expansion of Supported Version | ||
- Support for other Python Database Tooling |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
[tool.pyright] | ||
include = ["src"] | ||
strict = ["src"] | ||
|
||
[tool.ruff.lint] | ||
select = ["E4", "E7", "E9", "F", "I", "D"] | ||
ignore = ["D107"] | ||
|
||
[tool.ruff.lint.pydocstyle] | ||
convention = "google" | ||
|
||
[tool.poetry] | ||
name = "altqq" | ||
version = "0.0.1a1" | ||
description = "Alternative Queries: Typed and Composable Tool for Handwritten SQL" | ||
authors = ["baluyotraf <baluyotraf@outlook.com>"] | ||
maintainers = ["baluyotraf <baluyotraf@outlook.com>"] | ||
license = "MIT" | ||
readme = "README.md" | ||
repository = "https://github.com/baluyotraf/altqq" | ||
keywords = ["database", "SQL"] | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.9" | ||
pydantic = "^2.6.4" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
pyright = "^1.1.354" | ||
|
||
[build-system] | ||
requires = ["poetry-core"] | ||
build-backend = "poetry.core.masonry.api" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
"""Main entry point for the altqq library.""" | ||
|
||
from altqq.structs import Query | ||
from altqq.translators.plain_text import PlainTextTranslator | ||
from altqq.translators.pyodbc import PyODBCQuery, PyODBCTranslator | ||
from altqq.types import NonParameter as NonParameter | ||
|
||
|
||
class Translators: | ||
"""Definition of available translators.""" | ||
|
||
PYODBC = PyODBCTranslator() | ||
PLAIN_TEXT = PlainTextTranslator() | ||
|
||
|
||
def to_pyodbc(query: Query) -> PyODBCQuery: | ||
"""Converts a `Query` to its corresponding `PyODBCQuery` object. | ||
Args: | ||
query (Query): Query to translate to PyODBC | ||
Returns: | ||
PyODBCQuery: Equivalent query for PyODBC usage. | ||
""" | ||
return Translators.PYODBC(query) | ||
|
||
|
||
def to_plain_text(query: Query) -> str: | ||
"""Converts a `Query` to a plain text SQL. | ||
The conversion to plain text also handles some of the data types. None | ||
is converted to `NULL`, numeric values are written as they are and | ||
string values and other object types are escaped using `'`. | ||
Args: | ||
query (Query): Query to convert. | ||
Returns: | ||
str: Query as plain text. | ||
""" | ||
return Translators.PLAIN_TEXT(query) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
"""Structures used for defining queries.""" | ||
|
||
from typing import Any, ClassVar, Dict, Tuple | ||
|
||
import pydantic.dataclasses as dc | ||
from typing_extensions import dataclass_transform | ||
|
||
QUERY_ATTRIB = "__query__" | ||
|
||
|
||
@dataclass_transform() | ||
class QueryMeta(type): | ||
"""Metaclass for generating Query objects that the library supports. | ||
Classes using this metaclass are automatically converted to Pydantic | ||
Dataclasses for validation support. Also, the `__query__` attribute is | ||
verified to be provided either as a value or as a type hint. | ||
Raises: | ||
ValueError: When the `__query__` attribute is not defined. | ||
Returns: | ||
_type_: A new type compatible for the library query functionalities. | ||
""" | ||
|
||
@staticmethod | ||
def _check_query_attribute(dataclass: "QueryMeta", dct: Dict[str, Any]): | ||
try: | ||
if isinstance(getattr(dataclass, QUERY_ATTRIB), str): | ||
return True | ||
except AttributeError: | ||
return QUERY_ATTRIB in dct["__annotations__"] | ||
|
||
return False | ||
|
||
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]): | ||
"""Creates a new class of the metaclass. | ||
This wraps the newly created class with Pydantic Dataclass and checks | ||
the `__query__` attribute. | ||
""" | ||
dataclass = super().__new__(cls, name, bases, dct) | ||
if not cls._check_query_attribute(dataclass, dct): | ||
raise ValueError(f"A string {QUERY_ATTRIB} must be provided") | ||
return dc.dataclass(dataclass) | ||
|
||
|
||
class Query(metaclass=QueryMeta): | ||
"""Base class for query definitions. | ||
This class can be inherited instead of providing the `QueryMeta` as the | ||
class metaclass. | ||
""" | ||
|
||
__query__: ClassVar[str] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
"""Module for converting Query objects to plain text SQL.""" | ||
|
||
import dataclasses as dc | ||
import typing | ||
from typing import Any | ||
|
||
from altqq.structs import Query | ||
from altqq.types import QueryValueTypes, T | ||
from typing_extensions import Annotated | ||
|
||
|
||
class PlainTextTranslator: | ||
"""Converts a `Query` to a plain text SQL.""" | ||
|
||
def _resolve_value(self, query: Query, field: dc.Field[T]) -> Any: | ||
value = getattr(query, field.name) | ||
if field.type == Query: | ||
return self.__call__(value) | ||
|
||
if typing.get_origin(field.type) == Annotated: | ||
# Pyright can't match the __metadata__ attribute to Annotated | ||
if QueryValueTypes.NON_PARAMETER in field.type.__metadata__: # type: ignore | ||
return value | ||
|
||
# Numeric types are not escaped | ||
if isinstance(value, (int, float)): | ||
return value | ||
|
||
# None is written as NULL | ||
if value is None: | ||
return "NULL" | ||
|
||
# All other types fall down to strings and are escaped | ||
return f"'{value}'" | ||
|
||
def __call__(self, query: Query) -> str: | ||
"""Converts a `Query` to a plain text SQL. | ||
The conversion to plain text also handles some of the data types. None | ||
is converted to `NULL`, numeric values are written as they are and | ||
string values and other object types are escaped using `'`. | ||
Args: | ||
query (Query): Query to convert. | ||
Returns: | ||
str: Query as plain text. | ||
""" | ||
fields = dc.fields(query) | ||
format_dict = {f.name: self._resolve_value(query, f) for f in fields} | ||
return query.__query__.format(**format_dict) |
Oops, something went wrong.