sustained 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sustained-0.0.1/.gitignore +137 -0
- sustained-0.0.1/.pre-commit-config.yaml +36 -0
- sustained-0.0.1/LICENSE +21 -0
- sustained-0.0.1/PKG-INFO +89 -0
- sustained-0.0.1/README.md +75 -0
- sustained-0.0.1/docs/filtering.md +91 -0
- sustained-0.0.1/docs/index.md +28 -0
- sustained-0.0.1/docs/models.md +100 -0
- sustained-0.0.1/docs/queries.md +124 -0
- sustained-0.0.1/docs/relations.md +116 -0
- sustained-0.0.1/pyproject.toml +26 -0
- sustained-0.0.1/src/sustained/__init__.py +39 -0
- sustained-0.0.1/src/sustained/builder.py +479 -0
- sustained-0.0.1/src/sustained/model.py +145 -0
- sustained-0.0.1/src/sustained/types.py +74 -0
- sustained-0.0.1/tests/__init__.py +0 -0
- sustained-0.0.1/tests/test_join_builder.py +170 -0
- sustained-0.0.1/tests/test_lambda_join_builder.py +88 -0
- sustained-0.0.1/tests/test_query_builder.py +122 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a parent script to a folder in a temporary
|
|
31
|
+
# directory. Only add PyInstaller folders to .gitignore if you are sure this is
|
|
32
|
+
# the only course of action.
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to the Pipenv docs, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# from different sources is a concern, you may want to ignore it.
|
|
94
|
+
# Pipfile.lock
|
|
95
|
+
|
|
96
|
+
# PEP 582; used by poetry and pdm
|
|
97
|
+
__pypackages__/
|
|
98
|
+
|
|
99
|
+
# Celery stuff
|
|
100
|
+
celerybeat-schedule
|
|
101
|
+
celerybeat.pid
|
|
102
|
+
|
|
103
|
+
# SageMath parsed files
|
|
104
|
+
*.sage.py
|
|
105
|
+
|
|
106
|
+
# Environments
|
|
107
|
+
.env
|
|
108
|
+
.venv
|
|
109
|
+
env/
|
|
110
|
+
venv/
|
|
111
|
+
ENV/
|
|
112
|
+
env.bak/
|
|
113
|
+
venv.bak/
|
|
114
|
+
|
|
115
|
+
# Spyder project settings
|
|
116
|
+
.spyderproject
|
|
117
|
+
.spyproject
|
|
118
|
+
|
|
119
|
+
# Rope project settings
|
|
120
|
+
.ropeproject
|
|
121
|
+
|
|
122
|
+
# mkdocs documentation
|
|
123
|
+
/site
|
|
124
|
+
|
|
125
|
+
# mypy
|
|
126
|
+
.mypy_cache/
|
|
127
|
+
.dmypy.json
|
|
128
|
+
dmypy.json
|
|
129
|
+
|
|
130
|
+
# Pyre type checker
|
|
131
|
+
.pyre/
|
|
132
|
+
|
|
133
|
+
# pytype static type analyzer
|
|
134
|
+
.pytype/
|
|
135
|
+
|
|
136
|
+
# Cython debug symbols
|
|
137
|
+
cython_debug/
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# See https://pre-commit.com for more information
|
|
2
|
+
# See https://pre-commit.com/hooks.html for more hooks
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
5
|
+
rev: v4.6.0
|
|
6
|
+
hooks:
|
|
7
|
+
- id: trailing-whitespace
|
|
8
|
+
- id: end-of-file-fixer
|
|
9
|
+
- id: check-yaml
|
|
10
|
+
- id: check-added-large-files
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/pycqa/isort
|
|
13
|
+
rev: 5.13.2
|
|
14
|
+
hooks:
|
|
15
|
+
- id: isort
|
|
16
|
+
name: isort (python)
|
|
17
|
+
|
|
18
|
+
- repo: https://github.com/psf/black
|
|
19
|
+
rev: 24.4.2
|
|
20
|
+
hooks:
|
|
21
|
+
- id: black
|
|
22
|
+
|
|
23
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
24
|
+
rev: "v1.10.1"
|
|
25
|
+
hooks:
|
|
26
|
+
- id: mypy
|
|
27
|
+
|
|
28
|
+
- repo: local
|
|
29
|
+
hooks:
|
|
30
|
+
- id: unit-tests
|
|
31
|
+
name: Run unit tests
|
|
32
|
+
entry: python3 -m unittest discover -s tests
|
|
33
|
+
language: system
|
|
34
|
+
types: [python]
|
|
35
|
+
pass_filenames: false
|
|
36
|
+
always_run: true
|
sustained-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sustained-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sustained
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Python query builder inspired by Objection.js
|
|
5
|
+
Project-URL: Homepage, https://github.com/wetherc/sustained
|
|
6
|
+
Project-URL: Issues, https://github.com/wetherc/sustained/issues
|
|
7
|
+
Author-email: Christopher Wetherill <git@tbmh.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Sustained.py
|
|
16
|
+
|
|
17
|
+
A Python query builder inspired by [Objection.js](https://vincit.github.io/objection.js/).
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
This package is not available on PyPI and must be installed from source.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from sustained import Model, RelationType, create_model
|
|
31
|
+
|
|
32
|
+
class Person(Model):
|
|
33
|
+
database = 'my_db'
|
|
34
|
+
tableSchema = 'public'
|
|
35
|
+
tableName = 'persons'
|
|
36
|
+
|
|
37
|
+
class Animal(Model):
|
|
38
|
+
tableName = 'animals'
|
|
39
|
+
|
|
40
|
+
relationMappings = {
|
|
41
|
+
'owner': {
|
|
42
|
+
'relation': RelationType.BelongsToOneRelation,
|
|
43
|
+
'modelClass': Person,
|
|
44
|
+
'join': {
|
|
45
|
+
'from': 'animals.ownerId',
|
|
46
|
+
'to': 'persons.id'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Build a query
|
|
52
|
+
query = Animal.query().select('animals.name', 'persons.name').leftOuterJoinRelated('owner')
|
|
53
|
+
|
|
54
|
+
print(query)
|
|
55
|
+
# SELECT animals.name, persons.name FROM animals LEFT OUTER JOIN persons ON animals.ownerId = persons.id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Build a more complex query with a CTE and a raw join
|
|
59
|
+
active_owners = Person.query().select('id').where('status', '=', 'active')
|
|
60
|
+
|
|
61
|
+
query = (
|
|
62
|
+
Animal.query()
|
|
63
|
+
.with_('active_owners', active_owners)
|
|
64
|
+
.join('active_owners', 'animals.ownerId', '=', 'active_owners.id')
|
|
65
|
+
.select('animals.name')
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
print(query)
|
|
69
|
+
# WITH active_owners AS (SELECT id FROM persons WHERE status = 'active') SELECT animals.name FROM animals JOIN active_owners ON animals.ownerId = active_owners.id
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
This project uses `pre-commit` to enforce code quality and run tests before committing code.
|
|
75
|
+
|
|
76
|
+
### Pre-commit Hooks Setup
|
|
77
|
+
|
|
78
|
+
1. **Install pre-commit:**
|
|
79
|
+
```bash
|
|
80
|
+
pip install pre-commit
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Install the Git hooks:**
|
|
84
|
+
From the root of the project directory, run:
|
|
85
|
+
```bash
|
|
86
|
+
pre-commit install
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Now, the pre-commit hooks (including `black`, `isort`, `mypy`, and unit tests) will run automatically on every commit.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Sustained.py
|
|
2
|
+
|
|
3
|
+
A Python query builder inspired by [Objection.js](https://vincit.github.io/objection.js/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This package is not available on PyPI and must be installed from source.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from sustained import Model, RelationType, create_model
|
|
17
|
+
|
|
18
|
+
class Person(Model):
|
|
19
|
+
database = 'my_db'
|
|
20
|
+
tableSchema = 'public'
|
|
21
|
+
tableName = 'persons'
|
|
22
|
+
|
|
23
|
+
class Animal(Model):
|
|
24
|
+
tableName = 'animals'
|
|
25
|
+
|
|
26
|
+
relationMappings = {
|
|
27
|
+
'owner': {
|
|
28
|
+
'relation': RelationType.BelongsToOneRelation,
|
|
29
|
+
'modelClass': Person,
|
|
30
|
+
'join': {
|
|
31
|
+
'from': 'animals.ownerId',
|
|
32
|
+
'to': 'persons.id'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Build a query
|
|
38
|
+
query = Animal.query().select('animals.name', 'persons.name').leftOuterJoinRelated('owner')
|
|
39
|
+
|
|
40
|
+
print(query)
|
|
41
|
+
# SELECT animals.name, persons.name FROM animals LEFT OUTER JOIN persons ON animals.ownerId = persons.id
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Build a more complex query with a CTE and a raw join
|
|
45
|
+
active_owners = Person.query().select('id').where('status', '=', 'active')
|
|
46
|
+
|
|
47
|
+
query = (
|
|
48
|
+
Animal.query()
|
|
49
|
+
.with_('active_owners', active_owners)
|
|
50
|
+
.join('active_owners', 'animals.ownerId', '=', 'active_owners.id')
|
|
51
|
+
.select('animals.name')
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
print(query)
|
|
55
|
+
# WITH active_owners AS (SELECT id FROM persons WHERE status = 'active') SELECT animals.name FROM animals JOIN active_owners ON animals.ownerId = active_owners.id
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
This project uses `pre-commit` to enforce code quality and run tests before committing code.
|
|
61
|
+
|
|
62
|
+
### Pre-commit Hooks Setup
|
|
63
|
+
|
|
64
|
+
1. **Install pre-commit:**
|
|
65
|
+
```bash
|
|
66
|
+
pip install pre-commit
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. **Install the Git hooks:**
|
|
70
|
+
From the root of the project directory, run:
|
|
71
|
+
```bash
|
|
72
|
+
pre-commit install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Now, the pre-commit hooks (including `black`, `isort`, `mypy`, and unit tests) will run automatically on every commit.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Filtering Queries
|
|
2
|
+
|
|
3
|
+
Sustained provides a dynamic API for adding `WHERE` clauses to your queries.
|
|
4
|
+
|
|
5
|
+
[<-- Back to Index](./index.md)
|
|
6
|
+
|
|
7
|
+
## Basic `where` Methods
|
|
8
|
+
|
|
9
|
+
The most common way to filter is by using the `where`, `andWhere`, and `orWhere` methods. These are handled dynamically, so you can chain them as needed.
|
|
10
|
+
|
|
11
|
+
A `where` clause takes three arguments: a column, an operator, and a value.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from my_project import Movie
|
|
15
|
+
|
|
16
|
+
# SELECT * FROM movies WHERE director = 'Quentin Tarantino'
|
|
17
|
+
Movie.query().where('director', '=', 'Quentin Tarantino')
|
|
18
|
+
|
|
19
|
+
# Chain multiple `where` clauses
|
|
20
|
+
# SELECT * FROM movies WHERE release_year > 2000 AND rating > 8.5
|
|
21
|
+
Movie.query().where('release_year', '>', 2000).andWhere('rating', '>', 8.5)
|
|
22
|
+
|
|
23
|
+
# Use `orWhere` to add an alternative condition
|
|
24
|
+
# SELECT * FROM movies WHERE genre = 'Sci-Fi' OR genre = 'Fantasy'
|
|
25
|
+
Movie.query().where('genre', '=', 'Sci-Fi').orWhere('genre', '=', 'Fantasy')
|
|
26
|
+
```
|
|
27
|
+
> **Note:** The first `where` call in a chain cannot be an `orWhere`. It must be a plain `where` or `andWhere`.
|
|
28
|
+
|
|
29
|
+
## `whereIn` and `whereNotIn`
|
|
30
|
+
|
|
31
|
+
To filter against a list of values, use the `whereIn` and `whereNotIn` methods.
|
|
32
|
+
|
|
33
|
+
### `whereIn`
|
|
34
|
+
|
|
35
|
+
This generates a `WHERE col IN (...)` clause.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# SELECT * FROM movies WHERE id IN (10, 25, 30)
|
|
39
|
+
Movie.query().whereIn('id', [10, 25, 30])
|
|
40
|
+
|
|
41
|
+
# You can also use `andWhereIn` and `orWhereIn`
|
|
42
|
+
# SELECT * FROM movies WHERE release_year = 1999 AND genre IN ('Action', 'Sci-Fi')
|
|
43
|
+
Movie.query().where('release_year', '=', 1999).andWhereIn('genre', ['Action', 'Sci-Fi'])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `whereNotIn`
|
|
47
|
+
|
|
48
|
+
This generates a `WHERE col NOT IN (...)` clause.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# SELECT * FROM movies WHERE rating NOT IN (1, 2, 3)
|
|
52
|
+
Movie.query().whereNotIn('rating', [1, 2, 3])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Grouped `where` Clauses
|
|
56
|
+
|
|
57
|
+
For complex logical groupings, you can pass a callable (like a lambda function) to any `where` method. The function will receive a temporary `QueryBuilder` instance that you can use to build the grouped condition.
|
|
58
|
+
|
|
59
|
+
This is useful for creating conditions wrapped in parentheses, like `... AND (condition A OR condition B)`.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# SELECT * FROM movies
|
|
63
|
+
# WHERE genre = 'Action' AND (release_year < 1990 OR rating > 9.0)
|
|
64
|
+
|
|
65
|
+
query = Movie.query().where('genre', '=', 'Action').andWhere(lambda q: (
|
|
66
|
+
q.where('release_year', '<', 1990).orWhere('rating', '>', 9.0)
|
|
67
|
+
))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Complex Grouping
|
|
71
|
+
|
|
72
|
+
You can nest these groups as deeply as you need.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
# SELECT * FROM movies
|
|
76
|
+
# WHERE status = 'available'
|
|
77
|
+
# AND (
|
|
78
|
+
# (genre = 'Comedy' AND rating > 7) OR
|
|
79
|
+
# (genre = 'Drama' AND rating > 8)
|
|
80
|
+
# )
|
|
81
|
+
|
|
82
|
+
query = Movie.query()
|
|
83
|
+
.where('status', '=', 'available')
|
|
84
|
+
.andWhere(lambda q: (
|
|
85
|
+
q.where(lambda group1: (
|
|
86
|
+
group1.where('genre', '=', 'Comedy').andWhere('rating', '>', 7)
|
|
87
|
+
)).orWhere(lambda group2: (
|
|
88
|
+
group2.where('genre', '=', 'Drama').andWhere('rating', '>', 8)
|
|
89
|
+
))
|
|
90
|
+
))
|
|
91
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Sustained Documentation
|
|
2
|
+
|
|
3
|
+
Welcome to the documentation for Sustained, a Python query builder inspired by [Objection.js](https://vincit.github.io/objection.js/).
|
|
4
|
+
|
|
5
|
+
This documentation provides a detailed guide on how to use the library to define models and build complex SQL queries in a programmatic way.
|
|
6
|
+
|
|
7
|
+
## Getting Started
|
|
8
|
+
|
|
9
|
+
If you are new to Sustained, it's recommended to read the guides in the following order:
|
|
10
|
+
|
|
11
|
+
1. **[Models](./models.md):** Learn how to define models that map to your database tables.
|
|
12
|
+
2. **[Queries](./queries.md):** Understand how to start queries and select data.
|
|
13
|
+
3. **[Filtering](./filtering.md):** Dive into the various `where` methods for filtering your results.
|
|
14
|
+
4. **[Relations and Joins](./relations.md):** Learn how to define relationships between models and join them in your queries.
|
|
15
|
+
|
|
16
|
+
## API Reference
|
|
17
|
+
|
|
18
|
+
The docstrings in the source code provide a comprehensive API reference. You can use Python's built-in `help()` function to get detailed information about any class or method.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from sustained import Model, QueryBuilder
|
|
22
|
+
|
|
23
|
+
# Get help on the Model class
|
|
24
|
+
help(Model)
|
|
25
|
+
|
|
26
|
+
# Get help on the QueryBuilder class
|
|
27
|
+
help(QueryBuilder)
|
|
28
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Defining Models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
The `Model` class is the foundation of sustained.py. Each model you create represents a database table.
|
|
5
|
+
|
|
6
|
+
[<-- Back to Index](./index.md)
|
|
7
|
+
|
|
8
|
+
## Basic Setup
|
|
9
|
+
|
|
10
|
+
To define a model, create a class that inherits from `sustained.Model` and give it a `tableName`.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from sustained import Model
|
|
14
|
+
|
|
15
|
+
class Person(Model):
|
|
16
|
+
# This is the only required property.
|
|
17
|
+
tableName = 'persons'
|
|
18
|
+
|
|
19
|
+
class Animal(Model):
|
|
20
|
+
tableName = 'animals'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Namespace Properties
|
|
24
|
+
|
|
25
|
+
For fully qualified table names, you can also specify `database` and `tableSchema`.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
class User(Model):
|
|
29
|
+
database = 'my_db'
|
|
30
|
+
tableSchema = 'public'
|
|
31
|
+
tableName = 'users'
|
|
32
|
+
|
|
33
|
+
# This model will produce queries like:
|
|
34
|
+
# SELECT * FROM my_db.public.users
|
|
35
|
+
print(User.query())
|
|
36
|
+
```
|
|
37
|
+
These properties are used by the `QueryBuilder` to construct the `FROM` clause of your SQL queries.
|
|
38
|
+
|
|
39
|
+
## Dynamic Model Creation
|
|
40
|
+
|
|
41
|
+
In some cases, you might need to create models at runtime. The `create_model` function is provided for this purpose. It takes the desired class name and table name as arguments.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from sustained import create_model, RelationType
|
|
45
|
+
|
|
46
|
+
# A simple dynamic model
|
|
47
|
+
Vehicle = create_model('Vehicle', 'vehicles')
|
|
48
|
+
|
|
49
|
+
# You can use it immediately
|
|
50
|
+
query = Vehicle.query().select('id', 'license_plate')
|
|
51
|
+
print(query)
|
|
52
|
+
# SELECT id, license_plate FROM vehicles
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# You can also define relations for dynamic models
|
|
56
|
+
Person = create_model('Person', 'persons')
|
|
57
|
+
|
|
58
|
+
Animal = create_model(
|
|
59
|
+
'Animal',
|
|
60
|
+
'animals',
|
|
61
|
+
mappings={
|
|
62
|
+
'owner': {
|
|
63
|
+
'relation': RelationType.BelongsToOneRelation,
|
|
64
|
+
'modelClass': Person,
|
|
65
|
+
'join': {'from': 'animals.ownerId', 'to': 'persons.id'}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# This works just like a statically defined model
|
|
71
|
+
query = Animal.query().innerJoinRelated('owner')
|
|
72
|
+
print(query)
|
|
73
|
+
# SELECT * FROM animals INNER JOIN persons ON animals.ownerId = persons.id
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Column Name Access
|
|
77
|
+
|
|
78
|
+
Model instances provide a convenient way to get fully-qualified column names for use in queries, which helps avoid ambiguity.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
person = Person()
|
|
82
|
+
|
|
83
|
+
# Accessing an attribute on a model instance returns the qualified column name
|
|
84
|
+
print(person.id)
|
|
85
|
+
# "persons.id"
|
|
86
|
+
|
|
87
|
+
# Use it in a select statement
|
|
88
|
+
query = Person.query().select(person.firstName, person.lastName)
|
|
89
|
+
print(query)
|
|
90
|
+
# SELECT persons.firstName, persons.lastName FROM persons
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the model has a `database` or `tableSchema` defined, they will be included in the qualified name.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
user = User() # The model with `database` and `tableSchema`
|
|
97
|
+
print(user.id)
|
|
98
|
+
# "my_db.public.users.id"
|
|
99
|
+
```
|
|
100
|
+
This feature is particularly useful for writing complex joins and where clauses while ensuring you're always referencing the correct column.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Building Queries
|
|
2
|
+
|
|
3
|
+
Once you have defined your models, you can start building queries using the `QueryBuilder`.
|
|
4
|
+
|
|
5
|
+
[<-- Back to Index](./index.md)
|
|
6
|
+
|
|
7
|
+
## Starting a Query
|
|
8
|
+
|
|
9
|
+
All queries begin with the `query()` class method on a `Model` subclass. This returns a new `QueryBuilder` instance, which you can use to chain methods.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from my_project import User
|
|
13
|
+
|
|
14
|
+
# Get a query builder for the User model
|
|
15
|
+
query_builder = User.query()
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Selecting Columns
|
|
19
|
+
|
|
20
|
+
The `select()` method allows you to specify which columns your query should return.
|
|
21
|
+
|
|
22
|
+
### Selecting Specific Columns
|
|
23
|
+
|
|
24
|
+
Pass any number of column name strings to `select()`.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# Builds: SELECT id, name, email FROM users
|
|
28
|
+
query = User.query().select('id', 'name', 'email')
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If `select()` is never called, the query will default to selecting all columns (`SELECT *`).
|
|
32
|
+
|
|
33
|
+
### Using Column Name Access
|
|
34
|
+
|
|
35
|
+
For clarity and to avoid ambiguity in joins, it's often a good idea to use the model's column access feature to get fully-qualified column names.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
user = User()
|
|
39
|
+
person = Person()
|
|
40
|
+
|
|
41
|
+
# Builds: SELECT users.id, persons.firstName FROM users...
|
|
42
|
+
query = User.query().select(user.id, person.firstName)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Joining Tables
|
|
46
|
+
|
|
47
|
+
For simple joins where you don't have or need a pre-defined relation on your model, you can use the raw join methods. These methods are generated dynamically for each join type (`join`, `innerJoin`, `leftJoin`, `rightJoin`, etc.).
|
|
48
|
+
|
|
49
|
+
They accept four arguments:
|
|
50
|
+
1. The table to join to.
|
|
51
|
+
2. The first column for the `ON` condition.
|
|
52
|
+
3. The operator for the `ON` condition.
|
|
53
|
+
4. The second column for the `ON` condition.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Builds: SELECT persons.*, animals.name FROM persons LEFT JOIN animals ON persons.id = animals.ownerId
|
|
57
|
+
query = Person.query().leftJoin('animals', 'persons.id', '=', 'animals.ownerId')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Complex Joins with Lambdas
|
|
61
|
+
|
|
62
|
+
For joins that require multiple or complex `ON` conditions, you can pass a lambda function as the second argument to any of the `join` methods. This lambda receives a `JoinBuilder` object that you can use to construct the join conditions.
|
|
63
|
+
|
|
64
|
+
The `JoinBuilder` has the following methods:
|
|
65
|
+
* `on(col1, op, col2)`: Adds the initial `ON` condition.
|
|
66
|
+
* `andOn(col1, op, col2)`: Adds an `AND` condition to the join.
|
|
67
|
+
* `orOn(col1, op, col2)`: Adds an `OR` condition to the join.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Builds:
|
|
71
|
+
# SELECT * FROM users
|
|
72
|
+
# JOIN accounts ON accounts.id = users.account_id AND accounts.enabled = 1 OR accounts.owner_id = users.id
|
|
73
|
+
query = User.query().join(
|
|
74
|
+
'accounts',
|
|
75
|
+
lambda j: j.on('accounts.id', '=', 'users.account_id')
|
|
76
|
+
.andOn('accounts.enabled', '=', '1')
|
|
77
|
+
.orOn('accounts.owner_id', '=', 'users.id'),
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
For more complex joins based on your data model, see the [Relations documentation](./relations.md).
|
|
82
|
+
|
|
83
|
+
## Common Table Expressions (CTEs)
|
|
84
|
+
|
|
85
|
+
You can add CTEs to your query using the `.with_()` method. Note the trailing underscore, which is necessary to avoid conflicting with Python's `with` keyword.
|
|
86
|
+
|
|
87
|
+
The `.with_()` method takes two arguments:
|
|
88
|
+
1. An alias (string) for the CTE.
|
|
89
|
+
2. A `QueryBuilder` instance for the CTE's subquery.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Build a CTE for active users
|
|
93
|
+
active_users_cte = User.query().select('id').where('status', '=', 'active')
|
|
94
|
+
|
|
95
|
+
# Use the CTE in a main query to get their posts
|
|
96
|
+
# (Assumes a Post model exists)
|
|
97
|
+
posts_query = (
|
|
98
|
+
Post.query()
|
|
99
|
+
.with_('active_users', active_users_cte)
|
|
100
|
+
.join('active_users', 'posts.user_id', '=', 'active_users.id')
|
|
101
|
+
.select('posts.title')
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Builds:
|
|
105
|
+
# WITH active_users AS (SELECT id FROM users WHERE status = 'active')
|
|
106
|
+
# SELECT posts.title FROM posts JOIN active_users ON posts.user_id = active_users.id
|
|
107
|
+
print(posts_query)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Retrieving the SQL
|
|
111
|
+
|
|
112
|
+
The `QueryBuilder` does not execute the query. It only builds the SQL string. To get the final SQL, simply convert the builder instance to a string.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
query = User.query().select('name').where('id', '=', 1)
|
|
116
|
+
|
|
117
|
+
# Get the SQL string
|
|
118
|
+
sql_string = str(query)
|
|
119
|
+
|
|
120
|
+
print(sql_string)
|
|
121
|
+
# "SELECT name FROM users WHERE id = 1"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This design allows you to use Sustained with any database driver. You build the query with Sustained, and then execute the resulting SQL string with your preferred library (e.g., `psycopg2`, `pyodbc`, etc.).
|