databao-context-engine 0.1.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 (135) hide show
  1. databao_context_engine/__init__.py +35 -0
  2. databao_context_engine/build_sources/__init__.py +0 -0
  3. databao_context_engine/build_sources/internal/__init__.py +0 -0
  4. databao_context_engine/build_sources/internal/build_runner.py +111 -0
  5. databao_context_engine/build_sources/internal/build_service.py +77 -0
  6. databao_context_engine/build_sources/internal/build_wiring.py +52 -0
  7. databao_context_engine/build_sources/internal/export_results.py +43 -0
  8. databao_context_engine/build_sources/internal/plugin_execution.py +74 -0
  9. databao_context_engine/build_sources/public/__init__.py +0 -0
  10. databao_context_engine/build_sources/public/api.py +4 -0
  11. databao_context_engine/cli/__init__.py +0 -0
  12. databao_context_engine/cli/add_datasource_config.py +130 -0
  13. databao_context_engine/cli/commands.py +256 -0
  14. databao_context_engine/cli/datasources.py +64 -0
  15. databao_context_engine/cli/info.py +32 -0
  16. databao_context_engine/config/__init__.py +0 -0
  17. databao_context_engine/config/log_config.yaml +16 -0
  18. databao_context_engine/config/logging.py +43 -0
  19. databao_context_engine/databao_context_project_manager.py +92 -0
  20. databao_context_engine/databao_engine.py +85 -0
  21. databao_context_engine/datasource_config/__init__.py +0 -0
  22. databao_context_engine/datasource_config/add_config.py +50 -0
  23. databao_context_engine/datasource_config/check_config.py +131 -0
  24. databao_context_engine/datasource_config/datasource_context.py +60 -0
  25. databao_context_engine/event_journal/__init__.py +0 -0
  26. databao_context_engine/event_journal/writer.py +29 -0
  27. databao_context_engine/generate_configs_schemas.py +92 -0
  28. databao_context_engine/init_project.py +18 -0
  29. databao_context_engine/introspection/__init__.py +0 -0
  30. databao_context_engine/introspection/property_extract.py +202 -0
  31. databao_context_engine/llm/__init__.py +0 -0
  32. databao_context_engine/llm/config.py +20 -0
  33. databao_context_engine/llm/descriptions/__init__.py +0 -0
  34. databao_context_engine/llm/descriptions/ollama.py +21 -0
  35. databao_context_engine/llm/descriptions/provider.py +10 -0
  36. databao_context_engine/llm/embeddings/__init__.py +0 -0
  37. databao_context_engine/llm/embeddings/ollama.py +37 -0
  38. databao_context_engine/llm/embeddings/provider.py +13 -0
  39. databao_context_engine/llm/errors.py +16 -0
  40. databao_context_engine/llm/factory.py +61 -0
  41. databao_context_engine/llm/install.py +227 -0
  42. databao_context_engine/llm/runtime.py +73 -0
  43. databao_context_engine/llm/service.py +159 -0
  44. databao_context_engine/main.py +19 -0
  45. databao_context_engine/mcp/__init__.py +0 -0
  46. databao_context_engine/mcp/all_results_tool.py +5 -0
  47. databao_context_engine/mcp/mcp_runner.py +16 -0
  48. databao_context_engine/mcp/mcp_server.py +63 -0
  49. databao_context_engine/mcp/retrieve_tool.py +22 -0
  50. databao_context_engine/pluginlib/__init__.py +0 -0
  51. databao_context_engine/pluginlib/build_plugin.py +107 -0
  52. databao_context_engine/pluginlib/config.py +37 -0
  53. databao_context_engine/pluginlib/plugin_utils.py +68 -0
  54. databao_context_engine/plugins/__init__.py +0 -0
  55. databao_context_engine/plugins/athena_db_plugin.py +12 -0
  56. databao_context_engine/plugins/base_db_plugin.py +45 -0
  57. databao_context_engine/plugins/clickhouse_db_plugin.py +15 -0
  58. databao_context_engine/plugins/databases/__init__.py +0 -0
  59. databao_context_engine/plugins/databases/athena_introspector.py +101 -0
  60. databao_context_engine/plugins/databases/base_introspector.py +144 -0
  61. databao_context_engine/plugins/databases/clickhouse_introspector.py +162 -0
  62. databao_context_engine/plugins/databases/database_chunker.py +69 -0
  63. databao_context_engine/plugins/databases/databases_types.py +114 -0
  64. databao_context_engine/plugins/databases/duckdb_introspector.py +325 -0
  65. databao_context_engine/plugins/databases/introspection_model_builder.py +270 -0
  66. databao_context_engine/plugins/databases/introspection_scope.py +74 -0
  67. databao_context_engine/plugins/databases/introspection_scope_matcher.py +103 -0
  68. databao_context_engine/plugins/databases/mssql_introspector.py +433 -0
  69. databao_context_engine/plugins/databases/mysql_introspector.py +338 -0
  70. databao_context_engine/plugins/databases/postgresql_introspector.py +428 -0
  71. databao_context_engine/plugins/databases/snowflake_introspector.py +287 -0
  72. databao_context_engine/plugins/duckdb_db_plugin.py +12 -0
  73. databao_context_engine/plugins/mssql_db_plugin.py +12 -0
  74. databao_context_engine/plugins/mysql_db_plugin.py +12 -0
  75. databao_context_engine/plugins/parquet_plugin.py +32 -0
  76. databao_context_engine/plugins/plugin_loader.py +110 -0
  77. databao_context_engine/plugins/postgresql_db_plugin.py +12 -0
  78. databao_context_engine/plugins/resources/__init__.py +0 -0
  79. databao_context_engine/plugins/resources/parquet_chunker.py +23 -0
  80. databao_context_engine/plugins/resources/parquet_introspector.py +154 -0
  81. databao_context_engine/plugins/snowflake_db_plugin.py +12 -0
  82. databao_context_engine/plugins/unstructured_files_plugin.py +68 -0
  83. databao_context_engine/project/__init__.py +0 -0
  84. databao_context_engine/project/datasource_discovery.py +141 -0
  85. databao_context_engine/project/info.py +44 -0
  86. databao_context_engine/project/init_project.py +102 -0
  87. databao_context_engine/project/layout.py +127 -0
  88. databao_context_engine/project/project_config.py +32 -0
  89. databao_context_engine/project/resources/examples/src/databases/example_postgres.yaml +7 -0
  90. databao_context_engine/project/resources/examples/src/files/documentation.md +30 -0
  91. databao_context_engine/project/resources/examples/src/files/notes.txt +20 -0
  92. databao_context_engine/project/runs.py +39 -0
  93. databao_context_engine/project/types.py +134 -0
  94. databao_context_engine/retrieve_embeddings/__init__.py +0 -0
  95. databao_context_engine/retrieve_embeddings/internal/__init__.py +0 -0
  96. databao_context_engine/retrieve_embeddings/internal/export_results.py +12 -0
  97. databao_context_engine/retrieve_embeddings/internal/retrieve_runner.py +34 -0
  98. databao_context_engine/retrieve_embeddings/internal/retrieve_service.py +68 -0
  99. databao_context_engine/retrieve_embeddings/internal/retrieve_wiring.py +29 -0
  100. databao_context_engine/retrieve_embeddings/public/__init__.py +0 -0
  101. databao_context_engine/retrieve_embeddings/public/api.py +3 -0
  102. databao_context_engine/serialisation/__init__.py +0 -0
  103. databao_context_engine/serialisation/yaml.py +35 -0
  104. databao_context_engine/services/__init__.py +0 -0
  105. databao_context_engine/services/chunk_embedding_service.py +104 -0
  106. databao_context_engine/services/embedding_shard_resolver.py +64 -0
  107. databao_context_engine/services/factories.py +88 -0
  108. databao_context_engine/services/models.py +12 -0
  109. databao_context_engine/services/persistence_service.py +61 -0
  110. databao_context_engine/services/run_name_policy.py +8 -0
  111. databao_context_engine/services/table_name_policy.py +15 -0
  112. databao_context_engine/storage/__init__.py +0 -0
  113. databao_context_engine/storage/connection.py +32 -0
  114. databao_context_engine/storage/exceptions/__init__.py +0 -0
  115. databao_context_engine/storage/exceptions/exceptions.py +6 -0
  116. databao_context_engine/storage/migrate.py +127 -0
  117. databao_context_engine/storage/migrations/V01__init.sql +63 -0
  118. databao_context_engine/storage/models.py +51 -0
  119. databao_context_engine/storage/repositories/__init__.py +0 -0
  120. databao_context_engine/storage/repositories/chunk_repository.py +130 -0
  121. databao_context_engine/storage/repositories/datasource_run_repository.py +136 -0
  122. databao_context_engine/storage/repositories/embedding_model_registry_repository.py +87 -0
  123. databao_context_engine/storage/repositories/embedding_repository.py +113 -0
  124. databao_context_engine/storage/repositories/factories.py +35 -0
  125. databao_context_engine/storage/repositories/run_repository.py +157 -0
  126. databao_context_engine/storage/repositories/vector_search_repository.py +63 -0
  127. databao_context_engine/storage/transaction.py +14 -0
  128. databao_context_engine/system/__init__.py +0 -0
  129. databao_context_engine/system/properties.py +13 -0
  130. databao_context_engine/templating/__init__.py +0 -0
  131. databao_context_engine/templating/renderer.py +29 -0
  132. databao_context_engine-0.1.1.dist-info/METADATA +186 -0
  133. databao_context_engine-0.1.1.dist-info/RECORD +135 -0
  134. databao_context_engine-0.1.1.dist-info/WHEEL +4 -0
  135. databao_context_engine-0.1.1.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,338 @@
1
+ from typing import Any, Mapping
2
+
3
+ import pymysql
4
+ from pydantic import Field
5
+ from pymysql.constants import CLIENT
6
+
7
+ from databao_context_engine.plugins.base_db_plugin import BaseDatabaseConfigFile
8
+ from databao_context_engine.plugins.databases.base_introspector import BaseIntrospector, SQLQuery
9
+ from databao_context_engine.plugins.databases.databases_types import DatabaseSchema, DatabaseTable
10
+ from databao_context_engine.plugins.databases.introspection_model_builder import IntrospectionModelBuilder
11
+
12
+
13
+ class MySQLConfigFile(BaseDatabaseConfigFile):
14
+ connection: dict[str, Any]
15
+ type: str = Field(default="databases/mysql")
16
+
17
+
18
+ class MySQLIntrospector(BaseIntrospector[MySQLConfigFile]):
19
+ _IGNORED_SCHEMAS = {"information_schema", "mysql", "performance_schema", "sys"}
20
+
21
+ supports_catalogs = True
22
+
23
+ def _connect(self, file_config: MySQLConfigFile):
24
+ connection = file_config.connection
25
+ if not isinstance(connection, Mapping):
26
+ raise ValueError("Invalid YAML config: 'connection' must be a mapping of connection parameters")
27
+
28
+ return pymysql.connect(
29
+ **connection,
30
+ cursorclass=pymysql.cursors.DictCursor,
31
+ client_flag=CLIENT.MULTI_STATEMENTS | CLIENT.MULTI_RESULTS,
32
+ )
33
+
34
+ def _connect_to_catalog(self, file_config: MySQLConfigFile, catalog: str):
35
+ cfg = dict(file_config.connection or {})
36
+ cfg["database"] = catalog
37
+ return self._connect(MySQLConfigFile(connection=cfg))
38
+
39
+ def _get_catalogs(self, connection, file_config: MySQLConfigFile) -> list[str]:
40
+ with connection.cursor() as cur:
41
+ cur.execute(
42
+ """
43
+ SELECT schema_name
44
+ FROM information_schema.schemata
45
+ ORDER BY schema_name
46
+ """
47
+ )
48
+ dbs = [row["SCHEMA_NAME"] for row in cur.fetchall()]
49
+ return [d for d in dbs if d.lower() not in self._IGNORED_SCHEMAS]
50
+
51
+ def _sql_list_schemas(self, catalogs: list[str] | None) -> SQLQuery:
52
+ return SQLQuery(
53
+ "SELECT DATABASE() AS schema_name, DATABASE() AS catalog_name",
54
+ None,
55
+ )
56
+
57
+ def collect_catalog_model(self, connection, catalog: str, schemas: list[str]) -> list[DatabaseSchema] | None:
58
+ if not schemas:
59
+ return []
60
+
61
+ comps = self._component_queries()
62
+ results: dict[str, list[dict]] = {name: [] for name in comps}
63
+
64
+ schemas_sql = ", ".join(self._quote_literal(s) for s in schemas)
65
+
66
+ batch = ";\n".join(sql.replace("{SCHEMAS}", schemas_sql).rstrip().rstrip(";") for sql in comps.values())
67
+
68
+ with connection.cursor(pymysql.cursors.DictCursor) as cur:
69
+ cur.execute(batch)
70
+
71
+ for ix, name in enumerate(comps.keys(), start=1):
72
+ raw_rows = cur.fetchall() if cur.description else ()
73
+
74
+ if raw_rows and isinstance(raw_rows[0], dict):
75
+ rows_list = [{k.lower(): v for k, v in row.items()} for row in raw_rows]
76
+ else:
77
+ if cur.description:
78
+ cols = [d[0].lower() for d in cur.description]
79
+ rows_list = [dict(zip(cols, r)) for r in raw_rows]
80
+ else:
81
+ rows_list = []
82
+
83
+ results[name] = rows_list
84
+
85
+ if ix < len(comps):
86
+ ok = cur.nextset()
87
+ if not ok:
88
+ raise RuntimeError(f"MySQL batch ended early after component #{ix} '{name}'")
89
+
90
+ return IntrospectionModelBuilder.build_schemas_from_components(
91
+ schemas=schemas,
92
+ rels=results.get("relations", []),
93
+ cols=results.get("columns", []),
94
+ pk_cols=results.get("pk", []),
95
+ uq_cols=results.get("uq", []),
96
+ checks=results.get("checks", []),
97
+ fk_cols=results.get("fks", []),
98
+ idx_cols=results.get("idx", []),
99
+ )
100
+
101
+ def collect_schema_model(self, connection, catalog: str, schema: str) -> list[DatabaseTable] | None:
102
+ comps = self._component_queries()
103
+ results: dict[str, list[dict]] = {name: [] for name in comps}
104
+
105
+ batch = ";\n".join(
106
+ sql.replace("{SCHEMA}", self._quote_literal(schema)).rstrip().rstrip(";") for sql in comps.values()
107
+ )
108
+
109
+ with connection.cursor(pymysql.cursors.DictCursor) as cur:
110
+ cur.execute(batch)
111
+
112
+ for ix, name in enumerate(comps.keys(), start=1):
113
+ raw_rows = cur.fetchall() if cur.description else ()
114
+
115
+ rows_list: list[dict]
116
+ # TODO: simplify this
117
+ if raw_rows and isinstance(raw_rows[0], dict):
118
+ rows_list = [{k.lower(): v for k, v in row.items()} for row in raw_rows]
119
+ else:
120
+ if cur.description:
121
+ cols = [d[0].lower() for d in cur.description]
122
+ rows_list = [dict(zip(cols, r)) for r in raw_rows]
123
+ else:
124
+ rows_list = []
125
+
126
+ results[name] = rows_list
127
+
128
+ if ix < len(comps):
129
+ ok = cur.nextset()
130
+ if not ok:
131
+ raise RuntimeError(f"MySQL batch ended early after component #{ix} '{name}'")
132
+
133
+ return IntrospectionModelBuilder.build_tables_from_components(
134
+ rels=results.get("relations", []),
135
+ cols=results.get("columns", []),
136
+ pk_cols=results.get("pk", []),
137
+ uq_cols=results.get("uq", []),
138
+ checks=results.get("checks", []),
139
+ fk_cols=results.get("fks", []),
140
+ idx_cols=results.get("idx", []),
141
+ )
142
+
143
+ def _component_queries(self) -> dict[str, str]:
144
+ return {
145
+ "relations": self._sql_relations(),
146
+ "columns": self._sql_columns(),
147
+ "pk": self._sql_primary_keys(),
148
+ "uq": self._sql_uniques(),
149
+ "checks": self._sql_checks(),
150
+ "fks": self._sql_foreign_keys(),
151
+ "idx": self._sql_indexes(),
152
+ }
153
+
154
+ def _sql_relations(self) -> str:
155
+ return r"""
156
+ SELECT
157
+ t.TABLE_SCHEMA AS schema_name,
158
+ t.TABLE_NAME AS table_name,
159
+ CASE t.TABLE_TYPE
160
+ WHEN 'BASE TABLE' THEN 'table'
161
+ WHEN 'VIEW' THEN 'view'
162
+ ELSE LOWER(t.TABLE_TYPE)
163
+ END AS kind,
164
+ CASE t.TABLE_TYPE
165
+ WHEN 'VIEW' THEN NULL
166
+ ELSE NULLIF(t.TABLE_COMMENT, '')
167
+ END AS description
168
+ FROM
169
+ INFORMATION_SCHEMA.TABLES t
170
+ WHERE
171
+ t.TABLE_SCHEMA IN ({SCHEMAS})
172
+ ORDER BY
173
+ t.TABLE_SCHEMA,
174
+ t.TABLE_NAME
175
+ """
176
+
177
+ def _sql_columns(self) -> str:
178
+ return r"""
179
+ SELECT
180
+ c.TABLE_SCHEMA AS schema_name,
181
+ c.TABLE_NAME AS table_name,
182
+ c.COLUMN_NAME AS column_name,
183
+ c.ORDINAL_POSITION AS ordinal_position,
184
+ c.COLUMN_TYPE AS data_type,
185
+ CASE
186
+ WHEN c.IS_NULLABLE = 'YES' THEN TRUE
187
+ ELSE FALSE
188
+ END AS is_nullable,
189
+ CASE
190
+ WHEN c.EXTRA RLIKE '\\b(VIRTUAL|STORED) GENERATED\\b' THEN NULLIF(c.GENERATION_EXPRESSION, '')
191
+ ELSE c.COLUMN_DEFAULT
192
+ END AS default_expression,
193
+ CASE
194
+ WHEN c.EXTRA LIKE '%auto_increment%' THEN 'identity'
195
+ WHEN c.EXTRA RLIKE '\\b(VIRTUAL|STORED) GENERATED\\b' THEN 'computed'
196
+ ELSE NULL
197
+ END AS "generated",
198
+ NULLIF(c.COLUMN_COMMENT, '') AS description
199
+ FROM
200
+ INFORMATION_SCHEMA.COLUMNS c
201
+ WHERE
202
+ c.TABLE_SCHEMA IN ({SCHEMAS})
203
+ ORDER BY
204
+ c.TABLE_SCHEMA,
205
+ c.TABLE_NAME,
206
+ c.ORDINAL_POSITION
207
+ """
208
+
209
+ def _sql_primary_keys(self) -> str:
210
+ return r"""
211
+ SELECT
212
+ tc.TABLE_SCHEMA AS schema_name,
213
+ tc.TABLE_NAME AS table_name,
214
+ tc.CONSTRAINT_NAME AS constraint_name,
215
+ kcu.COLUMN_NAME AS column_name,
216
+ kcu.ORDINAL_POSITION AS position
217
+ FROM
218
+ INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
219
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
220
+ ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA AND kcu.TABLE_NAME = tc.TABLE_NAME
221
+ WHERE
222
+ tc.TABLE_SCHEMA IN ({SCHEMAS})
223
+ AND tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
224
+ ORDER BY
225
+ tc.TABLE_SCHEMA,
226
+ tc.TABLE_NAME,
227
+ tc.CONSTRAINT_NAME,
228
+ kcu.ORDINAL_POSITION
229
+ """
230
+
231
+ def _sql_uniques(self) -> str:
232
+ return r"""
233
+ SELECT
234
+ tc.TABLE_SCHEMA AS schema_name,
235
+ tc.TABLE_NAME AS table_name,
236
+ tc.CONSTRAINT_NAME AS constraint_name,
237
+ kcu.COLUMN_NAME AS column_name,
238
+ kcu.ORDINAL_POSITION AS position
239
+ FROM
240
+ INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
241
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = tc.TABLE_SCHEMA AND kcu.TABLE_NAME = tc.TABLE_NAME
242
+ WHERE
243
+ tc.TABLE_SCHEMA IN ({SCHEMAS})
244
+ AND tc.CONSTRAINT_TYPE = 'UNIQUE'
245
+ ORDER BY
246
+ tc.TABLE_SCHEMA,
247
+ tc.TABLE_NAME,
248
+ tc.CONSTRAINT_NAME,
249
+ kcu.ORDINAL_POSITION
250
+ """
251
+
252
+ def _sql_checks(self) -> str:
253
+ return r"""
254
+ SELECT
255
+ tc.TABLE_SCHEMA AS schema_name,
256
+ tc.TABLE_NAME AS table_name,
257
+ tc.CONSTRAINT_NAME AS constraint_name,
258
+ cc.CHECK_CLAUSE AS expression,
259
+ TRUE AS validated
260
+ FROM
261
+ INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
262
+ JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc ON cc.CONSTRAINT_SCHEMA = tc.TABLE_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
263
+ WHERE
264
+ tc.TABLE_SCHEMA IN ({SCHEMAS})
265
+ AND tc.CONSTRAINT_TYPE = 'CHECK'
266
+ ORDER BY
267
+ tc.TABLE_SCHEMA,
268
+ tc.TABLE_NAME,
269
+ tc.CONSTRAINT_NAME
270
+ """
271
+
272
+ def _sql_foreign_keys(self) -> str:
273
+ return r"""
274
+ SELECT
275
+ kcu.TABLE_SCHEMA AS schema_name,
276
+ kcu.TABLE_NAME AS table_name,
277
+ kcu.CONSTRAINT_NAME AS constraint_name,
278
+ kcu.ORDINAL_POSITION AS position,
279
+ kcu.COLUMN_NAME AS from_column,
280
+ kcu.REFERENCED_TABLE_SCHEMA AS ref_schema,
281
+ kcu.REFERENCED_TABLE_NAME AS ref_table,
282
+ kcu.REFERENCED_COLUMN_NAME AS to_column,
283
+ LOWER(rc.UPDATE_RULE) AS on_update,
284
+ LOWER(rc.DELETE_RULE) AS on_delete,
285
+ TRUE AS enforced,
286
+ TRUE AS validated
287
+ FROM
288
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
289
+ JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND tc.TABLE_NAME = kcu.TABLE_NAME
290
+ JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_SCHEMA = kcu.TABLE_SCHEMA AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
291
+ WHERE
292
+ kcu.TABLE_SCHEMA IN ({SCHEMAS})
293
+ AND tc.CONSTRAINT_TYPE = 'FOREIGN KEY'
294
+ ORDER BY
295
+ kcu.TABLE_SCHEMA,
296
+ kcu.TABLE_NAME,
297
+ kcu.CONSTRAINT_NAME,
298
+ kcu.ORDINAL_POSITION
299
+ """
300
+
301
+ def _sql_indexes(self) -> str:
302
+ return r"""
303
+ SELECT
304
+ s.TABLE_SCHEMA AS schema_name,
305
+ s.TABLE_NAME AS table_name,
306
+ s.INDEX_NAME AS index_name,
307
+ s.SEQ_IN_INDEX AS position,
308
+ COALESCE(s.EXPRESSION, s.COLUMN_NAME) AS expr,
309
+ (s.NON_UNIQUE = 0) AS is_unique,
310
+ s.INDEX_TYPE AS method,
311
+ NULL AS predicate
312
+ FROM
313
+ INFORMATION_SCHEMA.STATISTICS s
314
+ WHERE
315
+ s.TABLE_SCHEMA IN ({SCHEMAS})
316
+ AND s.INDEX_NAME <> 'PRIMARY'
317
+ ORDER BY
318
+ s.TABLE_SCHEMA,
319
+ s.TABLE_NAME,
320
+ s.INDEX_NAME,
321
+ s.SEQ_IN_INDEX
322
+ """
323
+
324
+ def _sql_sample_rows(self, catalog: str, schema: str, table: str, limit: int) -> SQLQuery:
325
+ sql = f"SELECT * FROM `{schema}`.`{table}` LIMIT %s"
326
+ return SQLQuery(sql, (limit,))
327
+
328
+ def _fetchall_dicts(self, connection, sql: str, params) -> list[dict]:
329
+ with connection.cursor(pymysql.cursors.DictCursor) as cur:
330
+ cur.execute(sql, params)
331
+ rows = cur.fetchall()
332
+ return [{k.lower(): v for k, v in row.items()} for row in rows]
333
+
334
+ def _quote_literal(self, value: str) -> str:
335
+ return "'" + str(value).replace("\\", "\\\\").replace("'", "\\'") + "'"
336
+
337
+ def _quote_ident(self, ident: str) -> str:
338
+ return "`" + ident.replace("`", "``") + "`"