Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First PR :D #1

Merged
merged 5 commits into from
Mar 26, 2024
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
26 changes: 26 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Publish package
on:
release:
types: ["created"]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: 1.8.2
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
cache: poetry
- name: Install plugin
run: poetry self add "poetry-dynamic-versioning[plugin]"
- name: Set PyPI token
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: poetry config pypi-token.pypi $PYPI_TOKEN
- name: Build and publish
run: poetry publish --build
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test
on: ["pull_request", "push"]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", 3.11]
poetry-version: [1.8.2]
steps:
- uses: actions/checkout@v4
- name: Setup Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: ${{ matrix.poetry-version }}
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Install Python dependencies
run: poetry install
- name: Run tests
run: poetry run pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# VS Code
.vscode/
43 changes: 43 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-toml
- id: check-yaml
args:
- --unsafe
- id: end-of-file-fixer
- id: trailing-whitespace

- repo: https://github.com/python-poetry/poetry
rev: 1.8.0
hooks:
- id: poetry-check

- repo: https://github.com/andrei-shabanski/poetry-plugin-sort
rev: v0.2.1
hooks:
- id: poetry-sort

- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
hooks:
- id: pyupgrade
args:
- --py310-plus

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.3.4
hooks:
- id: ruff
args:
- --fix
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
additional_dependencies:
- types-all
165 changes: 164 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,164 @@
# sqlmodel-filters
# sqlmodel-filters

A Lucene query like filter for [SQLModel](https://github.com/tiangolo/sqlmodel).

> [!NOTE]
> This is an alpha level library. Everything is subject to change & there are some known limitations.

## Installation

```bash
pip install sqlmodel_filters
```

## How to Use

Let's say we have the following model & records:

```py
import datetime
from functools import partial
from typing import Optional

from sqlmodel import Field, SQLModel


class Hero(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
secret_name: str
age: Optional[int] = None
created_at: datetime.datetime = Field(
default_factory=partial(datetime.datetime.now, datetime.UTC)
)

hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)


engine = create_engine("sqlite://")


SQLModel.metadata.create_all(engine)

with Session(engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()
```

And let's try querying with this library.

```py
# this library relies on luqum (https://github.com/jurismarches/luqum) for parsing Lucene query
from luqum import parse
from sqlmodel import Session

from sqlmodel_filters import Builder

# parse a Lucene query
parsed = parse('name:Spider')
# build SELECT statement for Hero based on the parsed query
builder = Builder(Hero)
statement = builder(parsed)

# the following is a compiled SQL query
statement.compile(compile_kwargs={"literal_binds": True})
>>> SELECT hero.id, hero.name, hero.secret_name, hero.age, hero.created_at
>>> FROM hero
>>> WHERE hero.name = '%Spider%'

# you can use the statement like this
heros = session.exec(statement).all()
assert len(heros) == 1
assert heros[0].name == "Spider-Boy"
```

Note that a value is automatically casted based on a field definition.

```py
# age: Optional[int]
"age:48"
>>> WHERE hero.age = 48

# created_at: datetime.datetime
"created_at:2020-01-01"
>>> WHERE hero.created_at = '2020-01-01 00:00:00'
```

### `Word` (`Term`)

Double quote a value if you want to use the equal operator.

```py
'name:"Spider-Boy"'
>>> WHERE hero.name = 'Spider-Boy'
```

The `LIKE` operator is used when you don't double quote a value.

```py
"name:Spider"
>>> WHERE hero.name LIKE '%Spider%'
```

Use `?` (a single character wildcard) or `*` (a multiple character wildcard) to control a LIKE operator pattern.

```py
"name:Deadpond?"
>>> WHERE hero.name LIKE 'Deadpond_'

"name:o*"
>>> WHERE hero.name LIKE 'o%'
```

### `FROM` & `TO`

```py
"age:>=40"
>>> WHERE hero.age >= 40

"age:>40"
>>> WHERE hero.age > 40
```

```py
"age:<=40"
>>> WHERE hero.age <= 40

"age:<40"
>>> WHERE hero.age < 40
```

### `RANGE`

```py
"age:{48 TO 60}"
>>> WHERE hero.age < 60 AND hero.age > 48

"age:[48 TO 60]"
>>> WHERE hero.age <= 60 AND hero.age >= 48
```

## `AND`, `OR`, `NOT` and `GROUP` (Grouping)

```py
"name:Rusty AND age:48"
>>> WHERE hero.name LIKE '%Rusty%' AND hero.age = 48

"name:Rusty OR age:47"
>>> WHERE hero.name LIKE '%Rusty%' OR hero.age = 47

"name:Rusty NOT age:47"
>>> WHERE hero.name LIKE '%Rusty%' AND hero.age != 47

"(name:Spider OR age:48) AND name:Rusty"
>>> WHERE (hero.name LIKE '%Spider%' OR hero.age = 48) AND hero.name LIKE '%Rusty%'
```

## Known Limitations / Todos

- Relationship join is not supported
- Filed Grouping is not supported
Loading