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.
@@ -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
@@ -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.
@@ -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.).