pyoq-sql 1.0.2__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 (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,72 @@
1
+ """MySQL data source name parsing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from urllib.parse import SplitResult, unquote, urlsplit
7
+
8
+ from pyoq.errors import ConfigurationValidationError
9
+
10
+ DEFAULT_PORT = 3306
11
+ SCHEME = "mysql"
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class MySQLConnectionSettings:
16
+ host: str
17
+ port: int
18
+ user: str
19
+ password: str
20
+ database: str
21
+
22
+
23
+ def parse_data_source_name(dsn: str) -> MySQLConnectionSettings:
24
+ parts = urlsplit(dsn)
25
+ if parts.scheme != SCHEME:
26
+ message = f"MySQL data source names must use the {SCHEME} scheme"
27
+ raise ConfigurationValidationError(message)
28
+ if parts.query or parts.fragment:
29
+ message = (
30
+ "MySQL data source names do not accept query or fragment options; "
31
+ "supply host, port, user, password, and database only"
32
+ )
33
+ raise ConfigurationValidationError(message)
34
+ return MySQLConnectionSettings(
35
+ host=parts.hostname or "localhost",
36
+ port=_port(parts),
37
+ user=_user(parts.username),
38
+ password=unquote(parts.password or ""),
39
+ database=_database(parts.path),
40
+ )
41
+
42
+
43
+ def _port(parts: SplitResult) -> int:
44
+ try:
45
+ port = parts.port
46
+ except ValueError as error:
47
+ message = f"MySQL data source name has an invalid port: {error}"
48
+ raise ConfigurationValidationError(message) from error
49
+ return DEFAULT_PORT if port is None else port
50
+
51
+
52
+ def _user(value: str | None) -> str:
53
+ user = unquote(value or "")
54
+ if user == "":
55
+ message = "MySQL data source names must name a user"
56
+ raise ConfigurationValidationError(message)
57
+ return user
58
+
59
+
60
+ def _database(path: str) -> str:
61
+ if not path.startswith("/") or path.count("/") != 1 or path == "/":
62
+ message = "MySQL data source names must name exactly one database"
63
+ raise ConfigurationValidationError(message)
64
+ return unquote(path[1:])
65
+
66
+
67
+ __all__ = (
68
+ "DEFAULT_PORT",
69
+ "SCHEME",
70
+ "MySQLConnectionSettings",
71
+ "parse_data_source_name",
72
+ )
@@ -0,0 +1,354 @@
1
+ """Typed MySQL information schema row records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pyoq.errors import SchemaInspectionError
8
+ from pyoq.schema.models import (
9
+ GeneratedKind,
10
+ GeneratedValue,
11
+ Identifier,
12
+ IndexTerm,
13
+ KeyKind,
14
+ ObjectReference,
15
+ ReferentialAction,
16
+ SqlExpression,
17
+ SqlType,
18
+ TypeKind,
19
+ )
20
+ from pyoq.schema.mysql.types import mysql_enum_values, mysql_type
21
+
22
+ _KEY_KINDS = {"PRIMARY KEY": KeyKind.PRIMARY, "UNIQUE": KeyKind.UNIQUE}
23
+ _FOREIGN_KEY = "FOREIGN KEY"
24
+ _REFERENTIAL_ACTIONS = {
25
+ "NO ACTION": ReferentialAction.NO_ACTION,
26
+ "RESTRICT": ReferentialAction.RESTRICT,
27
+ "CASCADE": ReferentialAction.CASCADE,
28
+ "SET NULL": ReferentialAction.SET_NULL,
29
+ "SET DEFAULT": ReferentialAction.SET_DEFAULT,
30
+ }
31
+ _BASE_TABLE = "BASE TABLE"
32
+ _VIEW_COMMENT = "VIEW"
33
+ _AUTO_INCREMENT = "AUTO_INCREMENT"
34
+ _GENERATED = "GENERATED"
35
+ _STORED = "STORED"
36
+ _EXPRESSION_DEFAULT = "DEFAULT_GENERATED"
37
+ _DESCENDING_COLLATION = "D"
38
+ _UNQUOTED_DEFAULT_KINDS = frozenset(
39
+ {
40
+ TypeKind.BOOLEAN,
41
+ TypeKind.SMALL_INTEGER,
42
+ TypeKind.INTEGER,
43
+ TypeKind.BIG_INTEGER,
44
+ TypeKind.DECIMAL,
45
+ TypeKind.REAL,
46
+ TypeKind.DOUBLE,
47
+ }
48
+ )
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class ObjectRecord:
53
+ name: str
54
+ table_type: str
55
+ comment_text: str
56
+ definition_text: str | None
57
+
58
+ @property
59
+ def is_table(self) -> bool:
60
+ return self.table_type == _BASE_TABLE
61
+
62
+ @property
63
+ def comment(self) -> str | None:
64
+ if not self.is_table or self.comment_text == "":
65
+ return None
66
+ return self.comment_text
67
+
68
+ @property
69
+ def definition(self) -> SqlExpression | None:
70
+ text = "" if self.definition_text is None else self.definition_text.strip()
71
+ return None if text == "" else SqlExpression(text)
72
+
73
+
74
+ @dataclass(frozen=True, slots=True)
75
+ class ColumnRecord:
76
+ table: str
77
+ name: str
78
+ column_type: str
79
+ nullable_text: str
80
+ default_text: str | None
81
+ extra: str
82
+ generation_expression: str
83
+ comment_text: str
84
+
85
+ @property
86
+ def enum_values(self) -> tuple[str, ...]:
87
+ """The labels this column's enum lists, or none if it is not one."""
88
+ return mysql_enum_values(self.column_type)
89
+
90
+ @property
91
+ def enum_name(self) -> Identifier:
92
+ """What to call an enum MySQL keeps on a column rather than by name.
93
+
94
+ MySQL has no enum type of its own, so the column it belongs to is the
95
+ only thing that identifies it.
96
+ """
97
+ return Identifier(f"{self.table}_{self.name}")
98
+
99
+ @property
100
+ def data_type(self) -> SqlType:
101
+ values = self.enum_values
102
+ if not values:
103
+ return mysql_type(self.column_type)
104
+ return SqlType(
105
+ TypeKind.ENUM,
106
+ self.column_type,
107
+ named_type=ObjectReference(self.enum_name),
108
+ )
109
+
110
+ @property
111
+ def nullable(self) -> bool:
112
+ return self.nullable_text.upper() == "YES"
113
+
114
+ @property
115
+ def comment(self) -> str | None:
116
+ return None if self.comment_text == "" else self.comment_text
117
+
118
+ @property
119
+ def generated(self) -> GeneratedValue | None:
120
+ markers = frozenset(self.extra.upper().split())
121
+ if _AUTO_INCREMENT in markers:
122
+ return GeneratedValue(GeneratedKind.IDENTITY_BY_DEFAULT)
123
+ if _GENERATED not in markers:
124
+ return None
125
+ kind = (
126
+ GeneratedKind.COMPUTED_STORED
127
+ if _STORED in markers
128
+ else GeneratedKind.COMPUTED_VIRTUAL
129
+ )
130
+ return GeneratedValue(kind, SqlExpression(self.generation_expression))
131
+
132
+ @property
133
+ def default(self) -> SqlExpression | None:
134
+ if self.default_text is None or self.generated is not None:
135
+ return None
136
+ if _EXPRESSION_DEFAULT in frozenset(self.extra.upper().split()):
137
+ return SqlExpression(self.default_text)
138
+ if self.data_type.kind in _UNQUOTED_DEFAULT_KINDS:
139
+ return SqlExpression(self.default_text)
140
+ return SqlExpression(_quoted(self.default_text))
141
+
142
+
143
+ @dataclass(frozen=True, slots=True)
144
+ class ConstraintRecord:
145
+ table: str
146
+ name: str
147
+ constraint_type: str
148
+ column: str
149
+ target_schema: str | None
150
+ target_table: str | None
151
+ target_column: str | None
152
+ on_update: str | None
153
+ on_delete: str | None
154
+
155
+ @property
156
+ def key_kind(self) -> KeyKind | None:
157
+ return _KEY_KINDS.get(self.constraint_type)
158
+
159
+ @property
160
+ def is_foreign_key(self) -> bool:
161
+ return self.constraint_type == _FOREIGN_KEY
162
+
163
+ @property
164
+ def target_reference(self) -> ObjectReference | None:
165
+ if self.target_schema is None or self.target_table is None:
166
+ return None
167
+ return ObjectReference(
168
+ Identifier(self.target_table),
169
+ Identifier(self.target_schema),
170
+ )
171
+
172
+ @property
173
+ def target_identifier(self) -> Identifier:
174
+ if self.target_column is None:
175
+ message = f"MySQL foreign key {self.name!r} has no referenced column"
176
+ raise SchemaInspectionError(message)
177
+ return Identifier(self.target_column)
178
+
179
+ @property
180
+ def update_action(self) -> ReferentialAction:
181
+ return _referential_action(self.on_update)
182
+
183
+ @property
184
+ def delete_action(self) -> ReferentialAction:
185
+ return _referential_action(self.on_delete)
186
+
187
+
188
+ @dataclass(frozen=True, slots=True)
189
+ class CheckRecord:
190
+ table: str
191
+ name: str
192
+ clause: str
193
+
194
+
195
+ @dataclass(frozen=True, slots=True)
196
+ class IndexRecord:
197
+ table: str
198
+ name: str
199
+ non_unique: int
200
+ column: str | None
201
+ collation: str | None
202
+ expression: str | None
203
+ index_type: str
204
+
205
+ @property
206
+ def unique(self) -> bool:
207
+ return self.non_unique == 0
208
+
209
+ @property
210
+ def method(self) -> str:
211
+ return self.index_type.lower()
212
+
213
+ @property
214
+ def term(self) -> IndexTerm:
215
+ descending = self.collation == _DESCENDING_COLLATION
216
+ if self.column is not None:
217
+ return IndexTerm(Identifier(self.column), None, descending)
218
+ if self.expression is None:
219
+ message = f"MySQL index {self.name!r} has an unreadable term"
220
+ raise SchemaInspectionError(message)
221
+ return IndexTerm(None, SqlExpression(self.expression), descending)
222
+
223
+
224
+ def read_object(row: tuple[object, ...]) -> ObjectRecord:
225
+ values = _row(row, 4)
226
+ return ObjectRecord(
227
+ _text(values[0]),
228
+ _text(values[1]),
229
+ _text(values[2]),
230
+ _optional_text(values[3]),
231
+ )
232
+
233
+
234
+ def read_column(row: tuple[object, ...]) -> ColumnRecord:
235
+ values = _row(row, 8)
236
+ return ColumnRecord(
237
+ _text(values[0]),
238
+ _text(values[1]),
239
+ _text(values[2]),
240
+ _text(values[3]),
241
+ _optional_text(values[4]),
242
+ _text(values[5]),
243
+ _text(values[6]),
244
+ _text(values[7]),
245
+ )
246
+
247
+
248
+ def read_constraint(row: tuple[object, ...]) -> ConstraintRecord:
249
+ values = _row(row, 9)
250
+ return ConstraintRecord(
251
+ _text(values[0]),
252
+ _text(values[1]),
253
+ _text(values[2]),
254
+ _text(values[3]),
255
+ _optional_text(values[4]),
256
+ _optional_text(values[5]),
257
+ _optional_text(values[6]),
258
+ _optional_text(values[7]),
259
+ _optional_text(values[8]),
260
+ )
261
+
262
+
263
+ def read_check(row: tuple[object, ...]) -> CheckRecord:
264
+ values = _row(row, 3)
265
+ return CheckRecord(_text(values[0]), _text(values[1]), _text(values[2]))
266
+
267
+
268
+ def read_index(row: tuple[object, ...]) -> IndexRecord:
269
+ values = _row(row, 7)
270
+ return IndexRecord(
271
+ _text(values[0]),
272
+ _text(values[1]),
273
+ _number(values[2]),
274
+ _optional_text(values[3]),
275
+ _optional_text(values[4]),
276
+ _optional_text(values[5]),
277
+ _text(values[6]),
278
+ )
279
+
280
+
281
+ def _referential_action(rule: str | None) -> ReferentialAction:
282
+ action = None if rule is None else _REFERENTIAL_ACTIONS.get(rule.upper())
283
+ if action is None:
284
+ message = f"unsupported MySQL referential action: {rule!r}"
285
+ raise SchemaInspectionError(message)
286
+ return action
287
+
288
+
289
+ def _quoted(value: str) -> str:
290
+ escaped = value.replace("'", "''")
291
+ return f"'{escaped}'"
292
+
293
+
294
+ def _row(row: tuple[object, ...], width: int) -> tuple[object, ...]:
295
+ if len(row) != width:
296
+ message = "MySQL catalog row has an unexpected shape"
297
+ raise SchemaInspectionError(message)
298
+ return row
299
+
300
+
301
+ def _text(value: object) -> str:
302
+ if not isinstance(value, str):
303
+ message = f"MySQL catalog value is not text: {value!r}"
304
+ raise SchemaInspectionError(message)
305
+ return value
306
+
307
+
308
+ def _optional_text(value: object) -> str | None:
309
+ return None if value is None else _text(value)
310
+
311
+
312
+ def _number(value: object) -> int:
313
+ if isinstance(value, bool) or not isinstance(value, int):
314
+ message = f"MySQL catalog value is not a number: {value!r}"
315
+ raise SchemaInspectionError(message)
316
+ return value
317
+
318
+
319
+ __all__ = (
320
+ "CheckRecord",
321
+ "ColumnRecord",
322
+ "ConstraintRecord",
323
+ "IndexRecord",
324
+ "ObjectRecord",
325
+ "read_check",
326
+ "read_column",
327
+ "read_constraint",
328
+ "read_index",
329
+ "read_object",
330
+ )
331
+
332
+
333
+ @dataclass(frozen=True, slots=True)
334
+ class RoutineRecord:
335
+ name: str
336
+ kind: str
337
+ returns: str | None
338
+ position: int | None
339
+ parameter_name: str | None
340
+ mode: str | None
341
+ parameter_type: str | None
342
+
343
+
344
+ def read_routine(row: tuple[object, ...]) -> RoutineRecord:
345
+ values = _row(row, 7)
346
+ return RoutineRecord(
347
+ _text(values[0]),
348
+ _text(values[1]),
349
+ _optional_text(values[2]),
350
+ None if values[3] is None else _number(values[3]),
351
+ _optional_text(values[4]),
352
+ _optional_text(values[5]),
353
+ _optional_text(values[6]),
354
+ )