pydoptic-sql 0.0.1.post1.dev2__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 (27) hide show
  1. pydoptic_sql-0.0.1.post1.dev2/LICENSE +21 -0
  2. pydoptic_sql-0.0.1.post1.dev2/PKG-INFO +75 -0
  3. pydoptic_sql-0.0.1.post1.dev2/README.md +57 -0
  4. pydoptic_sql-0.0.1.post1.dev2/pyproject.toml +38 -0
  5. pydoptic_sql-0.0.1.post1.dev2/setup.cfg +4 -0
  6. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/__init__.py +49 -0
  7. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/py.typed +0 -0
  8. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_computed.py +93 -0
  9. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_constraint.py +782 -0
  10. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_having.py +728 -0
  11. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_order.py +28 -0
  12. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_query.py +1144 -0
  13. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_service.py +335 -0
  14. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql/sql_table.py +188 -0
  15. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/PKG-INFO +75 -0
  16. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/SOURCES.txt +25 -0
  17. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/dependency_links.txt +1 -0
  18. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/requires.txt +8 -0
  19. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/scm_file_list.json +22 -0
  20. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/scm_version.json +8 -0
  21. pydoptic_sql-0.0.1.post1.dev2/src/pydoptic_sql.egg-info/top_level.txt +1 -0
  22. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_computed.py +258 -0
  23. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_constraint.py +296 -0
  24. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_having.py +237 -0
  25. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_params.py +154 -0
  26. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_query.py +911 -0
  27. pydoptic_sql-0.0.1.post1.dev2/test/test_sql_service.py +657 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Hungerford
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,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydoptic-sql
3
+ Version: 0.0.1.post1.dev2
4
+ Summary: A type-safe SQL query builder built on pydoptic
5
+ Author: John Hungerford
6
+ License-Expression: MIT
7
+ Project-URL: Source, https://github.com/johnhungerford/pydoptic
8
+ Requires-Python: <3.14,>=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: pydoptic
12
+ Requires-Dist: psycopg[binary]==3.2.12
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest~=8.4.2; extra == "test"
15
+ Provides-Extra: types
16
+ Requires-Dist: mypy~=1.18.2; extra == "types"
17
+ Dynamic: license-file
18
+
19
+ # pydoptic-sql
20
+
21
+ A type-safe SQL query builder built on [pydoptic](https://pypi.org/project/pydoptic/).
22
+
23
+ Join arity is tracked in the type system itself, so a WHERE/ON/HAVING clause can't reference a table
24
+ that isn't actually in scope at that point in a join chain -- mypy catches it, not just Postgres at
25
+ runtime.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pip install pydoptic-sql
31
+ ```
32
+
33
+ Includes `psycopg` as a regular dependency, so no extras are needed to execute queries against
34
+ Postgres.
35
+
36
+ ## Quickstart
37
+
38
+ ```python3
39
+ from pydoptic import Prop
40
+ from pydoptic_sql import SqlTable, ColumnType, PrimaryKey, column, SqlQuery, Constraint, PsycoPgSqlClient
41
+ import psycopg
42
+
43
+ class Worker(SqlTable):
44
+ id: Prop['Worker', int] = column(type=ColumnType.BIGINT(), constraints=[PrimaryKey])
45
+ name: Prop['Worker', str] = column(type=ColumnType.TEXT())
46
+ age: Prop['Worker', int] = column(type=ColumnType.INT())
47
+ department_id: Prop['Worker', int] = column(type=ColumnType.BIGINT())
48
+
49
+ query = SqlQuery.from_table(Worker).select(Worker.name).where(Constraint.gte(Worker.age, 18))
50
+
51
+ with psycopg.connect("host=localhost dbname=mydb user=postgres password=password") as conn:
52
+ client = PsycoPgSqlClient(conn)
53
+ with client.open() as tx:
54
+ for worker in tx.execute(query).stream():
55
+ print(Worker.name.get_val_safe(worker))
56
+ ```
57
+
58
+ Joining a second table narrows and widens the query's type together -- `Constraint2` (not
59
+ `Constraint`) is required for a WHERE/ON clause once a second table is in scope, and can reference
60
+ either table directly:
61
+
62
+ ```python3
63
+ class Department(SqlTable):
64
+ id: Prop['Department', int] = column(type=ColumnType.BIGINT(), constraints=[PrimaryKey])
65
+ name: Prop['Department', str] = column(type=ColumnType.TEXT())
66
+
67
+ from pydoptic_sql import Constraint2
68
+
69
+ query = SqlQuery.from_table(Worker).join_inner(
70
+ Department, Constraint2.eq(Worker.department_id, Department.id),
71
+ ).select(Worker.name, Department.name).where(Constraint2.gte(Worker.age, 18))
72
+ ```
73
+
74
+ See the [pydoptic README](https://github.com/johnhungerford/pydoptic/tree/main/packages/pydoptic) for
75
+ the underlying `Prop`/`Select` model this is built on.
@@ -0,0 +1,57 @@
1
+ # pydoptic-sql
2
+
3
+ A type-safe SQL query builder built on [pydoptic](https://pypi.org/project/pydoptic/).
4
+
5
+ Join arity is tracked in the type system itself, so a WHERE/ON/HAVING clause can't reference a table
6
+ that isn't actually in scope at that point in a join chain -- mypy catches it, not just Postgres at
7
+ runtime.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install pydoptic-sql
13
+ ```
14
+
15
+ Includes `psycopg` as a regular dependency, so no extras are needed to execute queries against
16
+ Postgres.
17
+
18
+ ## Quickstart
19
+
20
+ ```python3
21
+ from pydoptic import Prop
22
+ from pydoptic_sql import SqlTable, ColumnType, PrimaryKey, column, SqlQuery, Constraint, PsycoPgSqlClient
23
+ import psycopg
24
+
25
+ class Worker(SqlTable):
26
+ id: Prop['Worker', int] = column(type=ColumnType.BIGINT(), constraints=[PrimaryKey])
27
+ name: Prop['Worker', str] = column(type=ColumnType.TEXT())
28
+ age: Prop['Worker', int] = column(type=ColumnType.INT())
29
+ department_id: Prop['Worker', int] = column(type=ColumnType.BIGINT())
30
+
31
+ query = SqlQuery.from_table(Worker).select(Worker.name).where(Constraint.gte(Worker.age, 18))
32
+
33
+ with psycopg.connect("host=localhost dbname=mydb user=postgres password=password") as conn:
34
+ client = PsycoPgSqlClient(conn)
35
+ with client.open() as tx:
36
+ for worker in tx.execute(query).stream():
37
+ print(Worker.name.get_val_safe(worker))
38
+ ```
39
+
40
+ Joining a second table narrows and widens the query's type together -- `Constraint2` (not
41
+ `Constraint`) is required for a WHERE/ON clause once a second table is in scope, and can reference
42
+ either table directly:
43
+
44
+ ```python3
45
+ class Department(SqlTable):
46
+ id: Prop['Department', int] = column(type=ColumnType.BIGINT(), constraints=[PrimaryKey])
47
+ name: Prop['Department', str] = column(type=ColumnType.TEXT())
48
+
49
+ from pydoptic_sql import Constraint2
50
+
51
+ query = SqlQuery.from_table(Worker).join_inner(
52
+ Department, Constraint2.eq(Worker.department_id, Department.id),
53
+ ).select(Worker.name, Department.name).where(Constraint2.gte(Worker.age, 18))
54
+ ```
55
+
56
+ See the [pydoptic README](https://github.com/johnhungerford/pydoptic/tree/main/packages/pydoptic) for
57
+ the underlying `Prop`/`Select` model this is built on.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pydoptic-sql"
7
+ dynamic = ["version"]
8
+ authors = [{ name = "John Hungerford" }]
9
+ urls = { Source = "https://github.com/johnhungerford/pydoptic" }
10
+ description = "A type-safe SQL query builder built on pydoptic"
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ requires-python = ">=3.8,<3.14"
15
+ dependencies = [
16
+ "pydoptic",
17
+ "psycopg[binary]==3.2.12",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ test = [
22
+ "pytest~=8.4.2",
23
+ ]
24
+ types = [
25
+ "mypy~=1.18.2",
26
+ ]
27
+
28
+ [tool.setuptools_scm]
29
+ # See packages/pydoptic/pyproject.toml -- same repo-wide lockstep version, same reasoning.
30
+ root = "../.."
31
+ version_scheme = "no-guess-dev"
32
+ local_scheme = "no-local-version"
33
+
34
+ [tool.pytest.ini_options]
35
+ markers = [
36
+ "integration: requires a live Postgres instance started via docker-compose.yml; excluded by default, opt in with `-m integration`",
37
+ ]
38
+ addopts = "-m 'not integration'"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,49 @@
1
+ from pydoptic_sql.sql_table import (
2
+ AutoIncrement,
3
+ Check,
4
+ ColumnConstraint,
5
+ ColumnInfo,
6
+ ColumnType,
7
+ Default,
8
+ ForeignKey,
9
+ ManualColumnConstraint,
10
+ PrimaryKey,
11
+ SqlTable,
12
+ Unique,
13
+ column,
14
+ )
15
+ from pydoptic_sql.sql_constraint import Comparison, Constraint, Constraint2, Constraint3, Constraint4
16
+ from pydoptic_sql.sql_order import Direction, OrderBy
17
+ from pydoptic_sql.sql_computed import AggregateFunction, Computed, ComputedResult
18
+ from pydoptic_sql.sql_having import HavingConstraint, HavingConstraint2, HavingConstraint3, HavingConstraint4
19
+ from pydoptic_sql.sql_query import (
20
+ ComputedQuery1,
21
+ ComputedQuery2,
22
+ ComputedQuery3,
23
+ ComputedQuery4,
24
+ CreateQuery,
25
+ DeleteQuery,
26
+ DropQuery,
27
+ InsertQuery,
28
+ JoinType,
29
+ Query1,
30
+ Query2,
31
+ Query3,
32
+ Query4,
33
+ SqlQuery,
34
+ UpdateQuery,
35
+ )
36
+ from pydoptic_sql.sql_service import PsycoPgSqlClient, SqlClient, SqlResponse, SqlTransaction
37
+
38
+ __all__ = [
39
+ 'AutoIncrement', 'Check', 'ColumnConstraint', 'ColumnInfo', 'ColumnType', 'Default', 'ForeignKey',
40
+ 'ManualColumnConstraint', 'PrimaryKey', 'SqlTable', 'Unique', 'column',
41
+ 'Comparison', 'Constraint', 'Constraint2', 'Constraint3', 'Constraint4',
42
+ 'Direction', 'OrderBy',
43
+ 'AggregateFunction', 'Computed', 'ComputedResult',
44
+ 'HavingConstraint', 'HavingConstraint2', 'HavingConstraint3', 'HavingConstraint4',
45
+ 'ComputedQuery1', 'ComputedQuery2', 'ComputedQuery3', 'ComputedQuery4',
46
+ 'CreateQuery', 'DeleteQuery', 'DropQuery', 'InsertQuery', 'JoinType',
47
+ 'Query1', 'Query2', 'Query3', 'Query4', 'SqlQuery', 'UpdateQuery',
48
+ 'PsycoPgSqlClient', 'SqlClient', 'SqlResponse', 'SqlTransaction',
49
+ ]
@@ -0,0 +1,93 @@
1
+
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from typing import Any, Dict, Generic, Mapping, Type, TypeVar
5
+
6
+ from pydoptic.selector import Prop, PropOpt, SelectVal, SelectValue, Selectable
7
+ from pydoptic_sql import SqlTable
8
+
9
+ TC = TypeVar('TC', bound=SqlTable, contravariant=True)
10
+ A = TypeVar('A')
11
+
12
+ class AggregateFunction(Enum):
13
+ SUM = 'SUM'
14
+ COUNT = 'COUNT'
15
+ AVG = 'AVG'
16
+ MIN = 'MIN'
17
+ MAX = 'MAX'
18
+
19
+ class ComputedResult(Selectable['ComputedResult']):
20
+ """
21
+ Dict-backed holder for the computed (aggregate) columns of a query result row, keyed by each
22
+ `Computed`'s alias. Mirrors `PartialModel`'s shape but isn't backed by a model class -- there's
23
+ no schema behind an aggregate expression, just whatever aliases the query selected.
24
+ """
25
+ __slots__ = ['_dict']
26
+ _dict: Dict[str, Any]
27
+
28
+ def __init__(self, **kwargs: Any):
29
+ object.__setattr__(self, '_dict', dict(kwargs))
30
+
31
+ def __repr__(self) -> str:
32
+ return 'ComputedResult(' + ', '.join(f'{k}={v}' for k, v in self._dict.items()) + ')'
33
+
34
+ def __eq__(self, other: object) -> bool:
35
+ return isinstance(other, ComputedResult) and self._dict == other._dict
36
+
37
+ def __getattr__(self, item: str) -> Any:
38
+ try:
39
+ return object.__getattribute__(self, '_dict')[item]
40
+ except KeyError:
41
+ raise AttributeError(item)
42
+
43
+ def as_dict(self) -> Mapping[str, Any]:
44
+ return self._dict
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class Computed(Generic[TC, A], SelectVal[ComputedResult, A]):
48
+ """
49
+ An aggregate expression (SUM/COUNT/AVG/MIN/MAX) over a column, constructed via `SqlQuery.sum`/
50
+ `.count`/etc., selected into a query via `select_computed`/`select_computed_more`, and extracted
51
+ from a `ComputedResult` via `get_val`/`get_val_safe` -- same as a plain `Prop` extracts a value
52
+ from a model. `column` is `None` only for `SqlQuery.count(table)` (`COUNT(*)`), which doesn't
53
+ reference any particular column.
54
+
55
+ `TC` is phantom bookkeeping, not part of the `Select[ComputedResult, A]` shape it's used as --
56
+ it exists purely so `select_computed_more`'s signature can restrict which joined table(s) a given
57
+ `Computed` may reference, the same way `OrderBy`'s `TC` does. Like `OrderBy`, `Computed` has no
58
+ arity variants of its own: a joined query's computed selection is typed as a union of
59
+ `Computed[<each joined table>, Any]`, so an entry set for one table stays valid unchanged as more
60
+ tables get joined in, and qualification (`table.column` vs. bare) is decided by whichever class
61
+ renders it, not by `Computed` itself.
62
+ """
63
+ column: Prop[TC, Any] | PropOpt[TC, Any] | None
64
+ function: AggregateFunction
65
+ label: str
66
+ _target: Type[A]
67
+
68
+ @property
69
+ def origin(self) -> Type[ComputedResult]:
70
+ return ComputedResult
71
+
72
+ @property
73
+ def target(self) -> Type[A]:
74
+ return self._target
75
+
76
+ def get_unsafe(self, value: 'Selectable[ComputedResult] | Dict[str, Any]') -> SelectValue[A]:
77
+ if isinstance(value, dict):
78
+ if self.label not in value:
79
+ raise ValueError(f'Unexpected empty value for computed column {self.label}')
80
+ result = value[self.label]
81
+ else:
82
+ try:
83
+ result = getattr(value, self.label)
84
+ except AttributeError:
85
+ raise ValueError(f'Unexpected empty value for computed column {self.label}')
86
+ return SelectValue(result, False, False)
87
+
88
+ def get(self, value: ComputedResult) -> SelectValue[A]:
89
+ return SelectValue(getattr(value, self.label), False, False)
90
+
91
+ def to_sql(self) -> str:
92
+ col_ref = '*' if self.column is None else self.column.label
93
+ return f'{self.function.value}({col_ref}) AS {self.label}'