pyrsql 0.0.1__py3-none-any.whl

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 (74) hide show
  1. pyrsql/__init__.py +63 -0
  2. pyrsql/adapters/__init__.py +3 -0
  3. pyrsql/adapters/fastapi/__init__.py +15 -0
  4. pyrsql/adapters/fastapi/config.py +76 -0
  5. pyrsql/adapters/fastapi/criteria.py +56 -0
  6. pyrsql/adapters/fastapi/dependency.py +217 -0
  7. pyrsql/adapters/fastapi/errors.py +107 -0
  8. pyrsql/api.py +76 -0
  9. pyrsql/core/__init__.py +45 -0
  10. pyrsql/core/compiler.py +81 -0
  11. pyrsql/core/conversion.py +206 -0
  12. pyrsql/core/custom.py +14 -0
  13. pyrsql/core/field_policy.py +97 -0
  14. pyrsql/core/joins.py +11 -0
  15. pyrsql/core/json/__init__.py +19 -0
  16. pyrsql/core/json/options.py +31 -0
  17. pyrsql/core/json/path.py +35 -0
  18. pyrsql/core/json/query.py +39 -0
  19. pyrsql/core/json/values.py +72 -0
  20. pyrsql/core/options.py +416 -0
  21. pyrsql/core/page.py +115 -0
  22. pyrsql/core/procedure_policy.py +61 -0
  23. pyrsql/core/query.py +160 -0
  24. pyrsql/core/sort.py +153 -0
  25. pyrsql/integrations/__init__.py +8 -0
  26. pyrsql/integrations/fastapi/__init__.py +8 -0
  27. pyrsql/integrations/fastapi/sqlalchemy.py +251 -0
  28. pyrsql/orms/__init__.py +15 -0
  29. pyrsql/orms/base.py +99 -0
  30. pyrsql/orms/sqlalchemy/__init__.py +39 -0
  31. pyrsql/orms/sqlalchemy/coercion.py +57 -0
  32. pyrsql/orms/sqlalchemy/compiled.py +56 -0
  33. pyrsql/orms/sqlalchemy/compiled_page.py +40 -0
  34. pyrsql/orms/sqlalchemy/compiled_sort.py +56 -0
  35. pyrsql/orms/sqlalchemy/custom.py +25 -0
  36. pyrsql/orms/sqlalchemy/errors.py +17 -0
  37. pyrsql/orms/sqlalchemy/introspection.py +107 -0
  38. pyrsql/orms/sqlalchemy/json_path.py +351 -0
  39. pyrsql/orms/sqlalchemy/orm.py +65 -0
  40. pyrsql/orms/sqlalchemy/resolver.py +202 -0
  41. pyrsql/orms/sqlalchemy/sorter.py +173 -0
  42. pyrsql/orms/sqlalchemy/statement.py +40 -0
  43. pyrsql/orms/sqlalchemy/translator.py +560 -0
  44. pyrsql/orms/sqlalchemy/type_inference.py +27 -0
  45. pyrsql/orms/sqlalchemy/types.py +64 -0
  46. pyrsql/parsing/__init__.py +32 -0
  47. pyrsql/parsing/ast.py +61 -0
  48. pyrsql/parsing/errors.py +25 -0
  49. pyrsql/parsing/lexer.py +264 -0
  50. pyrsql/parsing/limits.py +15 -0
  51. pyrsql/parsing/operators.py +201 -0
  52. pyrsql/parsing/parser.py +292 -0
  53. pyrsql/parsing/source.py +48 -0
  54. pyrsql/parsing/tokens.py +30 -0
  55. pyrsql/py.typed +0 -0
  56. pyrsql/selector/__init__.py +30 -0
  57. pyrsql/selector/analyzer.py +58 -0
  58. pyrsql/selector/ast.py +31 -0
  59. pyrsql/selector/parser.py +130 -0
  60. pyrsql/selector/semantic.py +34 -0
  61. pyrsql/semantic/__init__.py +23 -0
  62. pyrsql/semantic/analyzer.py +116 -0
  63. pyrsql/semantic/ast.py +35 -0
  64. pyrsql/semantic/errors.py +40 -0
  65. pyrsql/sorting/__init__.py +26 -0
  66. pyrsql/sorting/analyzer.py +74 -0
  67. pyrsql/sorting/ast.py +46 -0
  68. pyrsql/sorting/errors.py +21 -0
  69. pyrsql/sorting/limits.py +15 -0
  70. pyrsql/sorting/parser.py +150 -0
  71. pyrsql/sorting/semantic.py +15 -0
  72. pyrsql-0.0.1.dist-info/METADATA +72 -0
  73. pyrsql-0.0.1.dist-info/RECORD +74 -0
  74. pyrsql-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,173 @@
1
+ """Sort translation for SQLAlchemy."""
2
+
3
+ from typing import Any, cast
4
+
5
+ import sqlalchemy as sa
6
+ from sqlalchemy.sql.elements import ColumnElement
7
+
8
+ from pyrsql.core.options import SortOptions
9
+ from pyrsql.orms.sqlalchemy.json_path import (
10
+ SQLAlchemyJSONPathExpressionBuilder,
11
+ )
12
+ from pyrsql.orms.sqlalchemy.resolver import SQLAlchemyPathResolver
13
+ from pyrsql.orms.sqlalchemy.type_inference import (
14
+ infer_sql_function_python_type,
15
+ is_string_python_type,
16
+ )
17
+ from pyrsql.orms.sqlalchemy.types import (
18
+ SQLAlchemyJoinPlan,
19
+ SQLAlchemyResolvedPath,
20
+ )
21
+ from pyrsql.selector.semantic import (
22
+ SemanticColumnSelector,
23
+ SemanticLiteralSelector,
24
+ SemanticSelector,
25
+ )
26
+ from pyrsql.sorting.ast import SortDirection
27
+ from pyrsql.sorting.semantic import SemanticSortField
28
+
29
+
30
+ class SQLAlchemySortTranslator:
31
+ """Translates semantic sort fields to SQLAlchemy order clauses."""
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ path_resolver: SQLAlchemyPathResolver | None = None,
37
+ json_path_builder: SQLAlchemyJSONPathExpressionBuilder | None = None,
38
+ ) -> None:
39
+ self._path_resolver = path_resolver or SQLAlchemyPathResolver()
40
+ self._json_path_builder = (
41
+ json_path_builder or SQLAlchemyJSONPathExpressionBuilder()
42
+ )
43
+
44
+ def translate(
45
+ self,
46
+ model: type[Any],
47
+ fields: tuple[SemanticSortField, ...],
48
+ *,
49
+ options: SortOptions | None = None,
50
+ ) -> tuple[
51
+ tuple[SQLAlchemyJoinPlan, ...],
52
+ tuple[ColumnElement[Any], ...],
53
+ ]:
54
+ """Translates semantic sort fields for a mapped model."""
55
+ joins: list[SQLAlchemyJoinPlan] = []
56
+ order_clauses: list[ColumnElement[Any]] = []
57
+ for field in fields:
58
+ selector_joins, expression, python_type = self._translate_selector(
59
+ model,
60
+ field.selector,
61
+ options=options,
62
+ )
63
+ joins.extend(selector_joins)
64
+ order_clauses.append(
65
+ self._build_order_clause(
66
+ expression,
67
+ python_type,
68
+ field,
69
+ )
70
+ )
71
+ return tuple(joins), tuple(order_clauses)
72
+
73
+ def _translate_selector(
74
+ self,
75
+ model: type[Any],
76
+ selector: SemanticSelector,
77
+ *,
78
+ options: SortOptions | None = None,
79
+ ) -> tuple[
80
+ tuple[SQLAlchemyJoinPlan, ...],
81
+ ColumnElement[Any],
82
+ type[Any] | None,
83
+ ]:
84
+ """Translates a semantic selector recursively."""
85
+ if isinstance(selector, SemanticColumnSelector):
86
+ resolved_path = self._path_resolver.resolve(
87
+ model,
88
+ selector.field_path,
89
+ field_policy=options.field_policy if options else None,
90
+ )
91
+ return (
92
+ resolved_path.joins,
93
+ self._resolve_column_expression(resolved_path),
94
+ str if resolved_path.is_json else resolved_path.python_type,
95
+ )
96
+ if isinstance(selector, SemanticLiteralSelector):
97
+ python_type = (
98
+ type(selector.value) if selector.value is not None else None
99
+ )
100
+ return (), sa.literal(selector.value), python_type
101
+
102
+ joins: list[SQLAlchemyJoinPlan] = []
103
+ argument_expressions: list[ColumnElement[Any]] = []
104
+ argument_types: list[type[Any] | None] = []
105
+ for argument in selector.arguments:
106
+ argument_joins, argument_expression, argument_type = (
107
+ self._translate_selector(
108
+ model,
109
+ argument,
110
+ options=options,
111
+ )
112
+ )
113
+ joins.extend(argument_joins)
114
+ argument_expressions.append(argument_expression)
115
+ argument_types.append(argument_type)
116
+ function_expression = getattr(sa.func, selector.function_name)(
117
+ *argument_expressions
118
+ )
119
+ return (
120
+ tuple(joins),
121
+ cast(ColumnElement[Any], function_expression),
122
+ infer_sql_function_python_type(
123
+ selector.function_name,
124
+ tuple(argument_types),
125
+ ),
126
+ )
127
+
128
+ def _resolve_column_expression(
129
+ self,
130
+ resolved_path: SQLAlchemyResolvedPath,
131
+ ) -> ColumnElement[Any]:
132
+ """Builds the effective SQL expression for a resolved path."""
133
+ base_expression = resolved_path.leaf_attribute
134
+ if not resolved_path.is_json:
135
+ return base_expression
136
+ return self._json_path_builder.build_sort_expression(
137
+ base_expression,
138
+ resolved_path.json_path,
139
+ )
140
+
141
+ def _build_order_clause(
142
+ self,
143
+ expression: ColumnElement[Any],
144
+ python_type: type[Any] | None,
145
+ field: SemanticSortField,
146
+ ) -> ColumnElement[Any]:
147
+ """Builds an ORDER BY clause for a resolved sort field."""
148
+ if field.ignore_case and is_string_python_type(python_type):
149
+ expression = cast(ColumnElement[Any], sa.func.lower(expression))
150
+ if field.direction is SortDirection.DESCENDING:
151
+ return cast(ColumnElement[Any], expression.desc())
152
+ return cast(ColumnElement[Any], expression.asc())
153
+
154
+ def _is_string_type(self, python_type: type[Any] | None) -> bool:
155
+ """Returns whether the resolved Python type is string-compatible."""
156
+ if python_type is None:
157
+ return False
158
+ return issubclass(python_type, str)
159
+
160
+ def _infer_function_python_type(
161
+ self,
162
+ function_name: str,
163
+ argument_types: tuple[type[Any] | None, ...],
164
+ ) -> type[Any] | None:
165
+ """Infers the Python type for common SQL functions."""
166
+ normalized_name = function_name.lower()
167
+ if normalized_name in {"lower", "upper", "concat"}:
168
+ return str
169
+ if normalized_name == "coalesce":
170
+ for argument_type in argument_types:
171
+ if argument_type is not None:
172
+ return argument_type
173
+ return None
@@ -0,0 +1,40 @@
1
+ """Helpers for mutating SQLAlchemy Select statements."""
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from sqlalchemy.sql import Select
6
+
7
+ from pyrsql.core.joins import JoinHint
8
+ from pyrsql.orms.sqlalchemy.errors import SQLAlchemyORMError
9
+ from pyrsql.orms.sqlalchemy.types import SQLAlchemyJoinPlan, SQLAlchemySelect
10
+
11
+
12
+ def require_sqlalchemy_select(target: object) -> SQLAlchemySelect:
13
+ """Validates and narrows an ORM target to a SQLAlchemy Select."""
14
+ if not isinstance(target, Select):
15
+ raise TypeError("SQLAlchemy ORM expects a sqlalchemy.sql.Select.")
16
+ return target
17
+
18
+
19
+ def apply_relationship_joins(
20
+ statement: SQLAlchemySelect,
21
+ joins: tuple[SQLAlchemyJoinPlan, ...],
22
+ *,
23
+ join_hints: Mapping[str, JoinHint] | None = None,
24
+ ) -> SQLAlchemySelect:
25
+ """Applies relationship joins once each while preserving order."""
26
+ join_hints = join_hints or {}
27
+ deduplicated_joins = dict.fromkeys(joins)
28
+ updated_statement = statement
29
+ for join_plan in deduplicated_joins:
30
+ resolved_hint = join_hints.get(join_plan.key, join_plan.default_hint)
31
+ if resolved_hint is JoinHint.INNER:
32
+ updated_statement = updated_statement.join(join_plan.attribute)
33
+ continue
34
+ if resolved_hint is JoinHint.LEFT:
35
+ updated_statement = updated_statement.outerjoin(join_plan.attribute)
36
+ continue
37
+ raise SQLAlchemyORMError(
38
+ "SQLAlchemy ORM does not support RIGHT joins via join_hints."
39
+ )
40
+ return updated_statement