sustained 2.3.0__tar.gz → 2.5.0__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.
Files changed (106) hide show
  1. {sustained-2.3.0 → sustained-2.5.0}/CHANGELOG.md +25 -0
  2. sustained-2.5.0/PKG-INFO +131 -0
  3. sustained-2.5.0/README.md +117 -0
  4. {sustained-2.3.0 → sustained-2.5.0}/docs/_layouts/default.html +5 -2
  5. {sustained-2.3.0 → sustained-2.5.0}/docs/assets/css/style.scss +10 -2
  6. {sustained-2.3.0 → sustained-2.5.0}/docs/executing.md +1 -1
  7. {sustained-2.3.0 → sustained-2.5.0}/docs/filtering.md +4 -2
  8. {sustained-2.3.0 → sustained-2.5.0}/docs/grouping.md +18 -9
  9. sustained-2.5.0/docs/index.md +54 -0
  10. {sustained-2.3.0 → sustained-2.5.0}/docs/models.md +11 -1
  11. {sustained-2.3.0 → sustained-2.5.0}/docs/queries.md +14 -2
  12. sustained-2.5.0/docs/reference.md +201 -0
  13. {sustained-2.3.0 → sustained-2.5.0}/docs/relations.md +14 -18
  14. {sustained-2.3.0 → sustained-2.5.0}/docs/schema.md +65 -4
  15. {sustained-2.3.0 → sustained-2.5.0}/pyproject.toml +1 -1
  16. sustained-2.5.0/src/sustained/aio_migrations.py +178 -0
  17. sustained-2.5.0/src/sustained/autogenerate.py +860 -0
  18. sustained-2.5.0/src/sustained/compilers/athena.py +198 -0
  19. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/base.py +103 -0
  20. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/duckdb.py +27 -0
  21. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/mssql.py +44 -0
  22. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/postgres.py +27 -0
  23. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/dialects.py +2 -0
  24. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/functions.py +5 -0
  25. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/migrations.py +98 -12
  26. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/model.py +39 -3
  27. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/schema.py +58 -1
  28. sustained-2.5.0/tests/test_async_migrations.py +88 -0
  29. sustained-2.5.0/tests/test_athena.py +373 -0
  30. sustained-2.5.0/tests/test_autogenerate.py +555 -0
  31. sustained-2.5.0/tests/test_dialect_ddl.py +108 -0
  32. sustained-2.3.0/PKG-INFO +0 -130
  33. sustained-2.3.0/README.md +0 -116
  34. sustained-2.3.0/docs/index.md +0 -35
  35. sustained-2.3.0/src/sustained/autogenerate.py +0 -321
  36. sustained-2.3.0/tests/test_autogenerate.py +0 -204
  37. {sustained-2.3.0 → sustained-2.5.0}/.gitignore +0 -0
  38. {sustained-2.3.0 → sustained-2.5.0}/.pre-commit-config.yaml +0 -0
  39. {sustained-2.3.0 → sustained-2.5.0}/DEVELOPERS.md +0 -0
  40. {sustained-2.3.0 → sustained-2.5.0}/LICENSE +0 -0
  41. {sustained-2.3.0 → sustained-2.5.0}/deploy.py +0 -0
  42. {sustained-2.3.0 → sustained-2.5.0}/docs/CNAME +0 -0
  43. {sustained-2.3.0 → sustained-2.5.0}/docs/_config.yml +0 -0
  44. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/__init__.py +0 -0
  45. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/aio.py +0 -0
  46. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builder.py +0 -0
  47. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builder.pyi +0 -0
  48. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/__init__.py +0 -0
  49. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/__init__.pyi +0 -0
  50. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/conditional_clause_builder.py +0 -0
  51. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/conditional_clause_builder.pyi +0 -0
  52. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/group_by_builder.py +0 -0
  53. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/group_by_builder.pyi +0 -0
  54. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/having_builder.py +0 -0
  55. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/having_builder.pyi +0 -0
  56. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/join_builder.py +0 -0
  57. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/join_builder.pyi +0 -0
  58. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/order_by_builder.py +0 -0
  59. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/order_by_builder.pyi +0 -0
  60. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/select_clause_builder.py +0 -0
  61. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/select_clause_builder.pyi +0 -0
  62. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/where_builder.py +0 -0
  63. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/builders/where_builder.pyi +0 -0
  64. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/__init__.py +0 -0
  65. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/compilers/presto.py +0 -0
  66. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/exceptions.py +0 -0
  67. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/execution.py +0 -0
  68. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/expressions.py +0 -0
  69. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/pool.py +0 -0
  70. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/py.typed +0 -0
  71. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/rendering.py +0 -0
  72. {sustained-2.3.0 → sustained-2.5.0}/src/sustained/types.py +0 -0
  73. {sustained-2.3.0 → sustained-2.5.0}/tests/__init__.py +0 -0
  74. {sustained-2.3.0 → sustained-2.5.0}/tests/test_analyst_sql.py +0 -0
  75. {sustained-2.3.0 → sustained-2.5.0}/tests/test_async.py +0 -0
  76. {sustained-2.3.0 → sustained-2.5.0}/tests/test_builder_ergonomics.py +0 -0
  77. {sustained-2.3.0 → sustained-2.5.0}/tests/test_builder_robustness.py +0 -0
  78. {sustained-2.3.0 → sustained-2.5.0}/tests/test_dialect.py +0 -0
  79. {sustained-2.3.0 → sustained-2.5.0}/tests/test_dialect_behaviors.py +0 -0
  80. {sustained-2.3.0 → sustained-2.5.0}/tests/test_dialect_functions.py +0 -0
  81. {sustained-2.3.0 → sustained-2.5.0}/tests/test_dml.py +0 -0
  82. {sustained-2.3.0 → sustained-2.5.0}/tests/test_duckdb_dialect.py +0 -0
  83. {sustained-2.3.0 → sustained-2.5.0}/tests/test_etl_statements.py +0 -0
  84. {sustained-2.3.0 → sustained-2.5.0}/tests/test_execution.py +0 -0
  85. {sustained-2.3.0 → sustained-2.5.0}/tests/test_expressions.py +0 -0
  86. {sustained-2.3.0 → sustained-2.5.0}/tests/test_functions.py +0 -0
  87. {sustained-2.3.0 → sustained-2.5.0}/tests/test_having_builder.py +0 -0
  88. {sustained-2.3.0 → sustained-2.5.0}/tests/test_join_builder.py +0 -0
  89. {sustained-2.3.0 → sustained-2.5.0}/tests/test_lambda_join_builder.py +0 -0
  90. {sustained-2.3.0 → sustained-2.5.0}/tests/test_migrations.py +0 -0
  91. {sustained-2.3.0 → sustained-2.5.0}/tests/test_model.py +0 -0
  92. {sustained-2.3.0 → sustained-2.5.0}/tests/test_model_features.py +0 -0
  93. {sustained-2.3.0 → sustained-2.5.0}/tests/test_mssql_compiler.py +0 -0
  94. {sustained-2.3.0 → sustained-2.5.0}/tests/test_order_by_builder.py +0 -0
  95. {sustained-2.3.0 → sustained-2.5.0}/tests/test_parameterization.py +0 -0
  96. {sustained-2.3.0 → sustained-2.5.0}/tests/test_pool.py +0 -0
  97. {sustained-2.3.0 → sustained-2.5.0}/tests/test_postgres_compiler.py +0 -0
  98. {sustained-2.3.0 → sustained-2.5.0}/tests/test_predicates.py +0 -0
  99. {sustained-2.3.0 → sustained-2.5.0}/tests/test_query_builder.py +0 -0
  100. {sustained-2.3.0 → sustained-2.5.0}/tests/test_raw_bindings.py +0 -0
  101. {sustained-2.3.0 → sustained-2.5.0}/tests/test_result_formats.py +0 -0
  102. {sustained-2.3.0 → sustained-2.5.0}/tests/test_schema_ddl.py +0 -0
  103. {sustained-2.3.0 → sustained-2.5.0}/tests/test_select_clause_builder.py +0 -0
  104. {sustained-2.3.0 → sustained-2.5.0}/tests/test_transactions.py +0 -0
  105. {sustained-2.3.0 → sustained-2.5.0}/tests/test_upsert.py +0 -0
  106. {sustained-2.3.0 → sustained-2.5.0}/tests/test_where_builder.py +0 -0
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 (2026-08-14)
4
+
5
+ ### Added
6
+
7
+ - AWS Athena dialect (`Dialects.ATHENA`): inherits Presto's query behavior with Athena's differences. `%s` placeholders matching pyathena, MERGE upserts on Iceberg tables, and Athena's type spellings (INT, STRING, DOUBLE, DECIMAL; JSON maps to STRING).
8
+ - `TableOptions(location, partitioned_by, properties)`: storage clauses declared as a model's `tableOptions` and rendered as PARTITIONED BY, LOCATION, and TBLPROPERTIES on Athena. Other dialects raise when options are set. Partition entries pass through as written so Iceberg transforms work.
9
+ - Athena DDL: `ADD COLUMNS` spelling for added columns, `CHANGE COLUMN` for Iceberg type widenings. Constraints (primary key, unique, default, foreign key, NOT NULL, autoincrement) and indexes raise `DialectError` at build time because Athena cannot enforce them. Renames, nullability changes, `type_casts` hints, RETURNING, and temporary CTAS raise with directions.
10
+ - Migrations on engines without transactions: the migrator now consults the dialect and runs each step bare on Athena instead of wrapping it in a transaction, never calling rollback. `Migrator` and `AsyncMigrator` accept `tracking_table_options` for the tracking table's storage clauses, and create it without constraints on constraint-free engines.
11
+ - The function registry recognizes Athena wherever it recognizes Presto, including `NOW()` and the `GETDATE()` translation.
12
+ - Schema diffing normalizes Athena's STRING type, so tables created from models diff clean through `information_schema`.
13
+
14
+ ## 2.4.0 (2026-08-14)
15
+
16
+ ### Added
17
+
18
+ - Constraint-aware introspection: primary keys, unique constraints, foreign keys, column defaults, and indexes are read from SQLite PRAGMA tables or information_schema, with graceful degradation and system schemas filtered.
19
+ - Type and nullability changes now generate migrations: in-place reversible ALTER COLUMN on Postgres (with `type_casts` USING hints), MSSQL, and DuckDB; automatic table rebuild with row copy on SQLite.
20
+ - Rename hints: `renames={'table.old': 'new'}` and `table_renames` produce reversible RENAME statements (sp_rename on MSSQL) instead of destructive drop-plus-add.
21
+ - Declared indexes on models via `Index`; created with the table and diffed for additions, definition changes, and opt-in drops, all reversible.
22
+ - `backfill` on ColumnDef: NOT NULL adds and tightenings emit add-nullable, UPDATE, SET NOT NULL, or fold into the SQLite rebuild.
23
+ - Length and precision changes detected when both sides report them.
24
+ - Constraint notes: PK, FK, unique, and default drift reported in the diff, never auto-migrated.
25
+ - Offline scripts: `migration_sql()` and `Migrator.script()` render the SQL a run would execute for DBA review.
26
+ - `AsyncMigrator`: the migration runner on an AsyncAdapter with transactional application and awaited callable steps.
27
+
3
28
  ## 2.3.0 (2026-08-14)
4
29
 
5
30
  ### Added
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: sustained
3
+ Version: 2.5.0
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.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Sustained.py
16
+
17
+ A Python query builder and lightweight ORM inspired by [Objection.js](https://vincit.github.io/objection.js/).
18
+
19
+ You describe a query as chained Python methods. Sustained renders the SQL for your dialect, runs it parameterized against any DB-API 2.0 connection or pool, and hydrates the rows into your model classes.
20
+
21
+ ```python
22
+ adults = User.query().where(User.c.age >= 18).orderBy('name').run()
23
+ ```
24
+
25
+ ## What it does
26
+
27
+ - **SQL building** for the default (ANSI), Postgres, MSSQL, Presto, AWS Athena, and DuckDB dialects: joins, CTEs (including recursive), unions, window functions, CASE expressions, and subqueries. Features a dialect lacks raise `DialectError` at build time.
28
+ - **Safe execution**: every statement runs parameterized. Transactions nest through savepoints. `update()` and `delete()` refuse to run without a WHERE clause.
29
+ - **Writes**: `insert()`, `update()`, `delete()`, upserts with `onConflict()`, `INSERT ... SELECT`, CTAS, and RETURNING.
30
+ - **Typed filters**: `User.query().where((User.c.age > 21) & User.c.name.like('A%'))`.
31
+ - **Results as** model instances, dicts, pandas DataFrames, or pyarrow Tables, with `withGraphFetched()` eager loading.
32
+ - **Schema migrations**: models declare typed columns and indexes; `Migrator.sync(models)` diffs the live database, generates the migration, applies it, and `down()` rolls it back. Destructive changes are gated behind explicit opt-ins.
33
+ - **Async**: the same queries run through driver adapters (`asyncpg`, `aiosqlite`, or any sync driver in a worker thread) with `await query.arun()`.
34
+
35
+ ## What it does not do
36
+
37
+ No lazy loading, no dirty tracking or `save()`, no identity map, no result caching, no cross-dialect emulation of missing features, and no guessed migrations: drops, renames, and NOT NULL backfills all require explicit opt-ins or hints. Writes and schema changes only happen when you spell them out.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ python3 -m pip install sustained
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from sustained import Model, RelationType
49
+
50
+ class Person(Model):
51
+ tableName = 'persons'
52
+
53
+ class Animal(Model):
54
+ tableName = 'animals'
55
+ relationMappings = {
56
+ 'owner': {
57
+ 'relation': RelationType.BelongsToOneRelation,
58
+ 'modelClass': Person,
59
+ 'join': {
60
+ 'from': 'animals.ownerId',
61
+ 'to': 'persons.id'
62
+ }
63
+ }
64
+ }
65
+
66
+ # Build a query
67
+ query = Animal.query().select('animals.name', 'persons.name').leftOuterJoinRelated('owner')
68
+
69
+ print(query)
70
+ # SELECT animals.name, persons.name
71
+ # FROM animals
72
+ # LEFT OUTER JOIN persons
73
+ # ON animals.ownerId = persons.id
74
+
75
+
76
+ # Execute against any DB-API 2.0 connection
77
+ import sqlite3
78
+
79
+ conn = sqlite3.connect('app.db')
80
+ Animal.bind(conn)
81
+
82
+ # Parameterized execution with model hydration
83
+ animals = Animal.query().where('species', '=', 'dog').orderBy('name').run()
84
+
85
+ # Or take the SQL and parameters and execute them yourself
86
+ sql, params = Animal.query().where('species', '=', 'dog').to_sql()
87
+ # sql: "SELECT * FROM animals WHERE species = ?"
88
+ # params: ('dog',)
89
+ ```
90
+
91
+ Models can manage their own schema:
92
+
93
+ ```python
94
+ from sustained.migrations import Migrator
95
+ from sustained.schema import Integer, String, Text
96
+
97
+ class User(Model):
98
+ tableName = 'users'
99
+ tableColumns = {
100
+ 'id': Integer(primary_key=True, autoincrement=True),
101
+ 'email': String(120, unique=True, nullable=False),
102
+ }
103
+
104
+ migrator = Migrator(conn, [])
105
+ migrator.sync([User]) # creates the users table
106
+
107
+ User.tableColumns['bio'] = Text()
108
+ migrator.sync([User]) # adds only the bio column
109
+ migrator.down() # rolls it back
110
+ ```
111
+
112
+ ## Documentation
113
+
114
+ The [documentation](https://sustained.tbmh.org/) covers [models](https://sustained.tbmh.org/models), [queries](https://sustained.tbmh.org/queries), [filtering](https://sustained.tbmh.org/filtering), [grouping](https://sustained.tbmh.org/grouping), [relations and joins](https://sustained.tbmh.org/relations), [execution, pooling, and async](https://sustained.tbmh.org/executing), and [schema and migrations](https://sustained.tbmh.org/schema). The [API reference](https://sustained.tbmh.org/reference) lists every public method by task.
115
+
116
+ ## Development
117
+
118
+ To install from source:
119
+
120
+ ```bash
121
+ git clone https://github.com/wetherc/sustained.git
122
+ cd sustained
123
+ python3 -m pip install -e .
124
+ ```
125
+
126
+ This project uses `pre-commit` to format code, lint, type check, and run the test suite before each commit:
127
+
128
+ ```bash
129
+ pip install pre-commit
130
+ pre-commit install
131
+ ```
@@ -0,0 +1,117 @@
1
+ # Sustained.py
2
+
3
+ A Python query builder and lightweight ORM inspired by [Objection.js](https://vincit.github.io/objection.js/).
4
+
5
+ You describe a query as chained Python methods. Sustained renders the SQL for your dialect, runs it parameterized against any DB-API 2.0 connection or pool, and hydrates the rows into your model classes.
6
+
7
+ ```python
8
+ adults = User.query().where(User.c.age >= 18).orderBy('name').run()
9
+ ```
10
+
11
+ ## What it does
12
+
13
+ - **SQL building** for the default (ANSI), Postgres, MSSQL, Presto, AWS Athena, and DuckDB dialects: joins, CTEs (including recursive), unions, window functions, CASE expressions, and subqueries. Features a dialect lacks raise `DialectError` at build time.
14
+ - **Safe execution**: every statement runs parameterized. Transactions nest through savepoints. `update()` and `delete()` refuse to run without a WHERE clause.
15
+ - **Writes**: `insert()`, `update()`, `delete()`, upserts with `onConflict()`, `INSERT ... SELECT`, CTAS, and RETURNING.
16
+ - **Typed filters**: `User.query().where((User.c.age > 21) & User.c.name.like('A%'))`.
17
+ - **Results as** model instances, dicts, pandas DataFrames, or pyarrow Tables, with `withGraphFetched()` eager loading.
18
+ - **Schema migrations**: models declare typed columns and indexes; `Migrator.sync(models)` diffs the live database, generates the migration, applies it, and `down()` rolls it back. Destructive changes are gated behind explicit opt-ins.
19
+ - **Async**: the same queries run through driver adapters (`asyncpg`, `aiosqlite`, or any sync driver in a worker thread) with `await query.arun()`.
20
+
21
+ ## What it does not do
22
+
23
+ No lazy loading, no dirty tracking or `save()`, no identity map, no result caching, no cross-dialect emulation of missing features, and no guessed migrations: drops, renames, and NOT NULL backfills all require explicit opt-ins or hints. Writes and schema changes only happen when you spell them out.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ python3 -m pip install sustained
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from sustained import Model, RelationType
35
+
36
+ class Person(Model):
37
+ tableName = 'persons'
38
+
39
+ class Animal(Model):
40
+ tableName = 'animals'
41
+ relationMappings = {
42
+ 'owner': {
43
+ 'relation': RelationType.BelongsToOneRelation,
44
+ 'modelClass': Person,
45
+ 'join': {
46
+ 'from': 'animals.ownerId',
47
+ 'to': 'persons.id'
48
+ }
49
+ }
50
+ }
51
+
52
+ # Build a query
53
+ query = Animal.query().select('animals.name', 'persons.name').leftOuterJoinRelated('owner')
54
+
55
+ print(query)
56
+ # SELECT animals.name, persons.name
57
+ # FROM animals
58
+ # LEFT OUTER JOIN persons
59
+ # ON animals.ownerId = persons.id
60
+
61
+
62
+ # Execute against any DB-API 2.0 connection
63
+ import sqlite3
64
+
65
+ conn = sqlite3.connect('app.db')
66
+ Animal.bind(conn)
67
+
68
+ # Parameterized execution with model hydration
69
+ animals = Animal.query().where('species', '=', 'dog').orderBy('name').run()
70
+
71
+ # Or take the SQL and parameters and execute them yourself
72
+ sql, params = Animal.query().where('species', '=', 'dog').to_sql()
73
+ # sql: "SELECT * FROM animals WHERE species = ?"
74
+ # params: ('dog',)
75
+ ```
76
+
77
+ Models can manage their own schema:
78
+
79
+ ```python
80
+ from sustained.migrations import Migrator
81
+ from sustained.schema import Integer, String, Text
82
+
83
+ class User(Model):
84
+ tableName = 'users'
85
+ tableColumns = {
86
+ 'id': Integer(primary_key=True, autoincrement=True),
87
+ 'email': String(120, unique=True, nullable=False),
88
+ }
89
+
90
+ migrator = Migrator(conn, [])
91
+ migrator.sync([User]) # creates the users table
92
+
93
+ User.tableColumns['bio'] = Text()
94
+ migrator.sync([User]) # adds only the bio column
95
+ migrator.down() # rolls it back
96
+ ```
97
+
98
+ ## Documentation
99
+
100
+ The [documentation](https://sustained.tbmh.org/) covers [models](https://sustained.tbmh.org/models), [queries](https://sustained.tbmh.org/queries), [filtering](https://sustained.tbmh.org/filtering), [grouping](https://sustained.tbmh.org/grouping), [relations and joins](https://sustained.tbmh.org/relations), [execution, pooling, and async](https://sustained.tbmh.org/executing), and [schema and migrations](https://sustained.tbmh.org/schema). The [API reference](https://sustained.tbmh.org/reference) lists every public method by task.
101
+
102
+ ## Development
103
+
104
+ To install from source:
105
+
106
+ ```bash
107
+ git clone https://github.com/wetherc/sustained.git
108
+ cd sustained
109
+ python3 -m pip install -e .
110
+ ```
111
+
112
+ This project uses `pre-commit` to format code, lint, type check, and run the test suite before each commit:
113
+
114
+ ```bash
115
+ pip install pre-commit
116
+ pre-commit install
117
+ ```
@@ -23,14 +23,17 @@
23
23
 
24
24
  <p>{{ site.description | default: site.github.project_tagline }}</p>
25
25
 
26
- <span>
26
+ <nav>
27
27
  <ul>
28
28
  <li><a href="index">Home</a></li>
29
29
  <li><a href="models">Models</a></li>
30
30
  <li><a href="queries">Building Queries</a></li>
31
31
  <li><a href="filtering">Filtering Queries</a></li>
32
- <li><a href="relations">Relations</a></li>
33
32
  <li><a href="grouping">Grouping</a></li>
33
+ <li><a href="relations">Relations and Joins</a></li>
34
+ <li><a href="executing">Executing Queries</a></li>
35
+ <li><a href="schema">Schema and Migrations</a></li>
36
+ <li><a href="reference">API Reference</a></li>
34
37
  </ul>
35
38
  </nav>
36
39
 
@@ -25,8 +25,8 @@ nav ul {
25
25
  }
26
26
 
27
27
  nav ul li {
28
- display: inline-block;
29
- margin-right: 15px;
28
+ display: block;
29
+ margin-bottom: 5px;
30
30
  }
31
31
 
32
32
  nav ul li a {
@@ -38,3 +38,11 @@ nav ul li a {
38
38
  nav ul li a:hover {
39
39
  text-decoration: underline;
40
40
  }
41
+
42
+ /* The minimal theme bolds links on hover. Bold text is wider, so table
43
+ columns jitter as the pointer moves over them. Underline instead. */
44
+ section a:hover,
45
+ section a:focus {
46
+ font-weight: normal;
47
+ text-decoration: underline;
48
+ }
@@ -5,7 +5,7 @@ title: Executing Queries
5
5
 
6
6
  Sustained can execute the queries it builds. It works with any DB-API 2.0 connection, such as `sqlite3`, `psycopg`, or `pyodbc`. Every statement runs parameterized: user values travel as parameters, never as text inside the SQL.
7
7
 
8
- The connection's parameter style must match the dialect. The default and MSSQL dialects use `?` (qmark). The Postgres dialect uses `%s` (format).
8
+ The connection's parameter style must match the dialect. The default and MSSQL dialects use `?` (qmark). The Postgres and Athena dialects use `%s` (format), matching psycopg and pyathena.
9
9
 
10
10
  ## Binding a Connection
11
11
 
@@ -156,7 +156,8 @@ You can nest these groups as deeply as you need.
156
156
  # (genre = 'Drama' AND rating > 8)
157
157
  # )
158
158
 
159
- query = Movie.query()
159
+ query = (
160
+ Movie.query()
160
161
  .where("status", "=", "available")
161
162
  .andWhere(lambda q: (
162
163
  q.where(lambda group1: (
@@ -165,7 +166,8 @@ query = Movie.query()
165
166
  group2.where("genre", "=", "Drama").andWhere("rating", ">", 8)
166
167
  ))
167
168
  ))
168
-
169
+ )
170
+ ```
169
171
 
170
172
  ## `whereLike` and `whereILike`
171
173
 
@@ -43,10 +43,12 @@ print(query)
43
43
  # GROUP BY customer_id
44
44
  # HAVING COUNT(id) > 5
45
45
  # AND SUM(total_amount) > 100
46
- query = Order.query()
46
+ query = (
47
+ Order.query()
47
48
  .groupBy("customer_id")
48
49
  .having("COUNT(id)", ">", 5)
49
50
  .andHaving("SUM(total_amount)", ">", 100)
51
+ )
50
52
  print(query)
51
53
 
52
54
  # Use `orHaving` to add an alternative condition
@@ -54,10 +56,12 @@ print(query)
54
56
  # GROUP BY customer_id
55
57
  # HAVING AVG(item_price) < 50
56
58
  # OR MAX(quantity) > 10
57
- query = Order.query()
59
+ query = (
60
+ Order.query()
58
61
  .groupBy("customer_id")
59
62
  .having("AVG(item_price)", "<", 50)
60
63
  .orHaving("MAX(quantity)", ">", 10)
64
+ )
61
65
  print(query)
62
66
  ```
63
67
  > **Note:** The first `having` call in a chain cannot be an `orHaving` or `andHaving`. It must be a plain `having`.
@@ -75,10 +79,9 @@ This generates a `HAVING col IN (...)` clause, useful for checking if an aggrega
75
79
  # GROUP BY order_status
76
80
  # HAVING order_status IN ('completed', 'shipped')
77
81
  query = (
78
- Order
79
- .query()
80
- .groupBy("order_status")
81
- .havingIn("order_status", ["completed", "shipped"]
82
+ Order.query()
83
+ .groupBy("order_status")
84
+ .havingIn("order_status", ["completed", "shipped"])
82
85
  )
83
86
  print(query)
84
87
 
@@ -87,10 +90,12 @@ print(query)
87
90
  # GROUP BY product_category
88
91
  # HAVING SUM(sales_count) > 100
89
92
  # AND product_category IN ('Electronics', 'Books')
90
- query = Order.query()
93
+ query = (
94
+ Order.query()
91
95
  .groupBy("product_category")
92
96
  .having("SUM(sales_count)", ">", 100)
93
97
  .andHavingIn("product_category", ["Electronics", "Books"])
98
+ )
94
99
  print(query)
95
100
  ```
96
101
 
@@ -162,13 +167,15 @@ This is useful for creating conditions like `... AND (condition A OR condition B
162
167
  # HAVING COUNT(id) > 5
163
168
  # AND (SUM(total_amount) < 100 OR MAX(quantity) <= 2)
164
169
 
165
- query = Order.query()
170
+ query = (
171
+ Order.query()
166
172
  .groupBy("customer_id")
167
173
  .having("COUNT(id)", ">", 5)
168
174
  .andHaving(lambda q: (
169
175
  q.having("SUM(total_amount)", "<", 100)
170
176
  .orHaving("MAX(quantity)", "<=", 2)
171
177
  ))
178
+ )
172
179
  print(query)
173
180
  ```
174
181
 
@@ -185,7 +192,8 @@ You can nest these groups as deeply as needed to construct intricate `HAVING` lo
185
192
  # (inventory_count < 10 AND product_type = 'clearance')
186
193
  # )
187
194
 
188
- query = Order.query()
195
+ query = (
196
+ Order.query()
189
197
  .groupBy("product_type")
190
198
  .having("total_revenue", ">", 10000)
191
199
  .andHaving(lambda q: (
@@ -197,5 +205,6 @@ query = Order.query()
197
205
  .andHaving("product_type", "=", "clearance")
198
206
  ))
199
207
  ))
208
+ )
200
209
  print(query)
201
210
  ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ layout: default
3
+ title: Sustained Documentation
4
+ ---
5
+
6
+ Sustained is a Python query builder and lightweight ORM inspired by [Objection.js](https://vincit.github.io/objection.js/). You describe a query as chained Python methods; Sustained renders the SQL for your dialect, runs it parameterized, and hydrates the rows into your model classes.
7
+
8
+ ```python
9
+ adults = User.query().where(User.c.age >= 18).orderBy('name').run()
10
+ ```
11
+
12
+ ## What Sustained does
13
+
14
+ - **Builds SQL programmatically.** Selects, aggregates, window functions, CASE expressions, every join type, CTEs (including recursive), unions, INTERSECT and EXCEPT, subqueries in SELECT, FROM, WHERE, and JOIN clauses.
15
+ - **Targets six dialects.** ANSI (default), PostgreSQL, MSSQL, Presto, AWS Athena, and DuckDB. Quoting, placeholders, upsert syntax, LIMIT/OFFSET spelling, and function names all follow the dialect. Unsupported features raise `DialectError` at build time instead of failing in the database.
16
+ - **Executes queries safely.** Every statement runs parameterized against any DB-API 2.0 connection or a `ConnectionPool`. Transactions nest through savepoints. `update()` and `delete()` refuse to run without a WHERE clause.
17
+ - **Writes data.** `insert()`, `update()`, `delete()`, upserts through `onConflict()`, `INSERT ... SELECT`, CREATE TABLE AS, and RETURNING.
18
+ - **Hydrates results.** Rows become model instances, plain dicts, pandas DataFrames, or pyarrow Tables. Relations eager load with `withGraphFetched()`.
19
+ - **Manages schema.** Models declare typed columns and indexes. `Migrator.sync()` diffs the live database against your models, generates a migration, applies it, and `down()` rolls it back. Destructive changes are opt-in.
20
+ - **Runs async.** The same queries run through driver adapters with `await query.arun()`, including asyncpg and aiosqlite, plus an `AsyncMigrator`.
21
+
22
+ ## What Sustained does not do
23
+
24
+ These are design decisions, not roadmap gaps. Knowing them up front saves you a search later.
25
+
26
+ - **No lazy loading or identity map.** A hydrated model instance is a plain object. Accessing an unloaded relation does not trigger a query; use `withGraphFetched()` or a join.
27
+ - **No dirty tracking or `save()`.** Writes are explicit statements: `User.query().update({...}).where(...)`. Sustained never writes anything you did not spell out.
28
+ - **No connection management beyond the pool.** You create connections (or a factory for the pool); Sustained never reads connection strings or config files.
29
+ - **No cross-dialect emulation of missing features.** If MSSQL has no RETURNING, `returning()` raises `DialectError` there. Sustained tells you at build time rather than emulating with extra queries.
30
+ - **No guessed migrations.** Autogeneration refuses to invent anything lossy. Drops need `allow_drops=True`, renames need explicit hints, and NOT NULL changes need a `backfill` value. Constraint drift is reported, never silently migrated.
31
+ - **No query result caching.** Every `run()` hits the database.
32
+
33
+ ## Finding your way
34
+
35
+ | You want to | Read |
36
+ | --- | --- |
37
+ | Map classes to tables, catch column typos | [Models](./models) |
38
+ | Build a SELECT: columns, functions, CTEs, unions, pagination | [Queries](./queries) |
39
+ | Filter rows: where methods and typed predicates | [Filtering](./filtering) |
40
+ | Aggregate and filter groups | [Grouping](./grouping) |
41
+ | Join tables and define relations | [Relations and Joins](./relations) |
42
+ | Run queries, write data, transactions, pooling, async | [Executing Queries](./executing) |
43
+ | Create tables, generate and roll back migrations | [Schema and Migrations](./schema) |
44
+ | Look up any method by name | [API Reference](./reference) |
45
+
46
+ New to Sustained? Read the pages in the order above. Each builds on the one before it.
47
+
48
+ ## Installing
49
+
50
+ ```bash
51
+ python3 -m pip install sustained
52
+ ```
53
+
54
+ Sustained has no required dependencies. pandas and pyarrow are optional, only needed for `to_df()` and `to_arrow()`.
@@ -119,7 +119,17 @@ Person.id # "persons.id"
119
119
  Person.firstNam # AttributeError
120
120
  ```
121
121
 
122
- The `create_model` function accepts the same declaration through its `columns` argument.
122
+ The `create_model` function accepts the same declaration through its `columns` argument. Models that declare typed columns in `tableColumns` (see [Schema and Migrations](./schema)) get the strict column set derived automatically.
123
+
124
+ ## Typed Column References
125
+
126
+ Beyond plain name strings, every model exposes a typed column namespace at `Model.c`. Python comparison operators on these references build `Predicate` objects for `where()` and `having()`:
127
+
128
+ ```python
129
+ User.query().where((User.c.age > 21) & User.c.name.like('A%'))
130
+ ```
131
+
132
+ See [Filtering](./filtering#typed-predicates) for the full operator set.
123
133
 
124
134
  ## The Model Registry
125
135
 
@@ -214,6 +214,17 @@ query = User.query().select_func(
214
214
 
215
215
  A string argument that is not a plain column name raises a `ValueError` when the query renders. This protects you from a literal that silently becomes a column, or the reverse.
216
216
 
217
+ Every registered function is also available as a method of its own name, so these two lines build the same query:
218
+
219
+ ```python
220
+ User.query().select_func('COALESCE', 'nickname', 'first_name', alias='d')
221
+ User.query().coalesce('nickname', 'first_name', alias='d')
222
+ ```
223
+
224
+ The shortcuts include `lower()`, `upper()`, `coalesce()`, `concat()`, `substring()`, `trim()`, `length()`, `round()`, `abs()`, `ceiling()`, `floor()`, `mod()`, `now()`, and `getdate()`.
225
+
226
+ Some functions translate per dialect: `now()` renders as `GETDATE()` on MSSQL, `getdate()` renders as `NOW()` on Postgres, and `length()` renders as `LEN()` on MSSQL. You write the name you know, and the dialect gets the spelling it needs. A registered function raises `DialectError` on a dialect that has no spelling for it, so `now()` fails on the default dialect at build time instead of in the database.
227
+
217
228
  ##### Dialect Validation
218
229
 
219
230
  A key feature of `.select_func()` is its runtime dialect validation. The method checks the function name against an internal `FunctionRegistry`.
@@ -256,7 +267,7 @@ While `.select_func()` can be used for any function, the `FunctionRegistry` incl
256
267
  You can use a `Subquery` object to embed a subquery directly into your `SELECT` list.
257
268
 
258
269
  ```python
259
- from sustained import Subquery
270
+ from sustained.expressions import Subquery
260
271
 
261
272
  # SELECT
262
273
  # id,
@@ -463,6 +474,7 @@ Currently, the following dialects are supported:
463
474
 
464
475
  * **`Dialects.DEFAULT`**: Standard ANSI SQL (Default)
465
476
  * **`Dialects.PRESTO`**: SQL dialect for the Presto query engine.
477
+ * **`Dialects.ATHENA`**: SQL dialect for AWS Athena (Trino-based). Inherits Presto's query behavior with Athena's DDL, `%s` placeholders for pyathena, and MERGE upserts on Iceberg tables.
466
478
  * **`Dialects.MSSQL`**: SQL dialect for Microsoft SQL Server.
467
479
  * **`Dialects.POSTGRES`**: SQL dialect for PostgreSQL.
468
480
  * **`Dialects.DUCKDB`**: SQL dialect for DuckDB.
@@ -529,6 +541,6 @@ sql, params = User.query().select('name').where('id', '=', 1).to_sql()
529
541
  cursor.execute(sql, params)
530
542
  ```
531
543
 
532
- The placeholder style follows the dialect: `?` by default and for MSSQL, `%s` for Postgres.
544
+ The placeholder style follows the dialect: `?` by default and for MSSQL, `%s` for Postgres and Athena.
533
545
 
534
546
  Sustained can also execute queries for you and hydrate the results into model instances. See [Executing Queries](./executing) for `Model.bind()`, `run()`, `first()`, and eager loading, and for the `insert()`, `update()`, and `delete()` statement builders.