google-cloud-db-context-engineering 0.5.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.
- google/cloud/db_context_enrichment/__init__.py +1 -0
- google/cloud/db_context_enrichment/bootstrap/__init__.py +1 -0
- google/cloud/db_context_enrichment/bootstrap/bootstrap_generator.py +74 -0
- google/cloud/db_context_enrichment/common/__init__.py +0 -0
- google/cloud/db_context_enrichment/common/config.py +10 -0
- google/cloud/db_context_enrichment/common/context_mutator.py +132 -0
- google/cloud/db_context_enrichment/common/parameterizer.py +192 -0
- google/cloud/db_context_enrichment/dataset/__init__.py +0 -0
- google/cloud/db_context_enrichment/dataset/dataset_generator.py +44 -0
- google/cloud/db_context_enrichment/evaluate/__init__.py +3 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/__init__.py +0 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/alloydb.py +72 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/base.py +76 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/mysql.py +69 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/postgres.py +69 -0
- google/cloud/db_context_enrichment/evaluate/db_generators/spanner.py +62 -0
- google/cloud/db_context_enrichment/evaluate/evaluate_generator.py +239 -0
- google/cloud/db_context_enrichment/evaluate/result_reader.py +181 -0
- google/cloud/db_context_enrichment/facet/__init__.py +0 -0
- google/cloud/db_context_enrichment/facet/facet_generator.py +68 -0
- google/cloud/db_context_enrichment/main.py +450 -0
- google/cloud/db_context_enrichment/model/__init__.py +0 -0
- google/cloud/db_context_enrichment/model/context.py +86 -0
- google/cloud/db_context_enrichment/prompts/__init__.py +9 -0
- google/cloud/db_context_enrichment/prompts/targeted_facets.py +56 -0
- google/cloud/db_context_enrichment/prompts/targeted_templates.py +57 -0
- google/cloud/db_context_enrichment/prompts/targeted_value_search.py +93 -0
- google/cloud/db_context_enrichment/template/__init__.py +0 -0
- google/cloud/db_context_enrichment/template/template_generator.py +67 -0
- google/cloud/db_context_enrichment/value_search/__init__.py +0 -0
- google/cloud/db_context_enrichment/value_search/generator.py +91 -0
- google/cloud/db_context_enrichment/value_search/match_templates.py +267 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/METADATA +160 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/RECORD +38 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/WHEEL +5 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/entry_points.txt +2 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/licenses/LICENSE +202 -0
- google_cloud_db_context_engineering-0.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import textwrap
|
|
2
|
+
|
|
3
|
+
from google.cloud.db_context_enrichment.value_search.match_templates import (
|
|
4
|
+
MatchFunction,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
_SUPPORTED_DB_ENGINE = ["alloydb", "postgres", "mysql", "spanner"]
|
|
8
|
+
|
|
9
|
+
GENERATE_TARGETED_VALUE_SEARCH_PROMPT = textwrap.dedent(
|
|
10
|
+
"""
|
|
11
|
+
**Workflow for Generating Targeted Value Search**
|
|
12
|
+
|
|
13
|
+
1. **Database Configuration:**
|
|
14
|
+
- Ask the user for the **Database Engine (Product name) and optionally version**. Supported database engines: {supported_db_engines}
|
|
15
|
+
- **Understand the difference between Database Engine and Dialect:**
|
|
16
|
+
- **Database Engine** is the product name (e.g., `alloydb`, `postgres`, `mysql`, `spanner`). This is what the user provides.
|
|
17
|
+
- **Dialect** is the underlying SQL dialect/protocol used by tools (e.g., `postgresql` for `alloydb`/`postgres`, `googlesql` for `spanner`). You MUST infer this dialect from the engine.
|
|
18
|
+
- Using the database engine provided, infer the dialect:
|
|
19
|
+
- **alloydb** or **postgres** -> `postgresql`
|
|
20
|
+
- **mysql** -> `mysql`
|
|
21
|
+
- **spanner** -> `googlesql`
|
|
22
|
+
- Confirm the database engine provided is one of the supported engines. If not, notify the user this database engine is not supported yet and end the workflow.
|
|
23
|
+
|
|
24
|
+
2. **Fetch Capabilities:**
|
|
25
|
+
- **Immediately after** receiving the Database Engine (and Version if provided), call the `list_match_functions` tool.
|
|
26
|
+
- **Important:** Pass the **inferred dialect** (as defined in step 1) to the `dialect` parameter of the tool.
|
|
27
|
+
- If the tool returns an error (e.g., unsupported version), present the error to the user (which includes the list of supported versions) and end the workflow.
|
|
28
|
+
- Otherwise, present the available match functions to the user, strictly including the function name, its Description, and an Example of when it should be used, using the information returned by the tool.
|
|
29
|
+
|
|
30
|
+
3. **User Input Loop:**
|
|
31
|
+
- Ask the user to provide the following details for a value search:
|
|
32
|
+
- **Table Name**
|
|
33
|
+
- **Column Name**
|
|
34
|
+
- **Concept Type** (e.g., "City", "Product ID")
|
|
35
|
+
- **Match Function** (Must be one of the function names retrieved in Step 2)
|
|
36
|
+
- **Dialect Specific Parameters** (Ask for these if the chosen function and dialect require them):
|
|
37
|
+
- For **Spanner (googlesql)** + `{trigram_match}`: Ask for **Column Tokens** column name.
|
|
38
|
+
- For **MySQL** + `{semantic_match}`: Ask for **Column Embedding** column name.
|
|
39
|
+
- **Description** (optional): A description of the value search.
|
|
40
|
+
- After capturing the details, check if the input is valid, especially whether Match Function is a valid string from the returned values from list_match_functions, if not ask the user to do a correction, if valid, ask the user if they would like to add another one.
|
|
41
|
+
- Continue this loop until the user indicates they have no more value searches to add.
|
|
42
|
+
|
|
43
|
+
4. **Review and Confirmation:**
|
|
44
|
+
- Present the complete list of user-provided value search definitions for confirmation.
|
|
45
|
+
- **Use the following format for each value search:**
|
|
46
|
+
**Index [Number]**
|
|
47
|
+
**Table:** [Table Name]
|
|
48
|
+
**Column:** [Column Name]
|
|
49
|
+
**Concept:** [Concept Type]
|
|
50
|
+
**Function:** [Match Function]
|
|
51
|
+
**Engine Specific Parameters:** [Column Tokens / Column Embedding, if provided]
|
|
52
|
+
**Description:** [Description]
|
|
53
|
+
- Ask if any modifications are needed. If so, work with the user to refine the list.
|
|
54
|
+
|
|
55
|
+
5. **Final Generation:**
|
|
56
|
+
- Once approved, call the `generate_value_searches` tool with the list of value search definitions.
|
|
57
|
+
- Mapping for `generate_value_searches` input (JSON):
|
|
58
|
+
- `column_tokens` (for Column Tokens)
|
|
59
|
+
- `column_embedding` (for Column Embedding)
|
|
60
|
+
- **Important:** Pass the **inferred dialect** (from step 1) and `db_version` to the tool.
|
|
61
|
+
- Combine all generated Value Search configurations into a single JSON structure (ContextSet).
|
|
62
|
+
|
|
63
|
+
6. **Save Value Search:**
|
|
64
|
+
- Ask the user to choose one of the following options:
|
|
65
|
+
1. Create a new context set file.
|
|
66
|
+
2. Append value search to an existing context set file.
|
|
67
|
+
|
|
68
|
+
- **If creating a new file:**
|
|
69
|
+
- You will need to ask the user for the database instance and database name to create the filename.
|
|
70
|
+
- Call the `save_context_set` tool. You will need to provide the database instance, database name, the JSON content from the previous step, and the root directory where the Gemini CLI is running.
|
|
71
|
+
|
|
72
|
+
- **If appending to an existing file:**
|
|
73
|
+
- Ask the user to provide the path to the existing context set file.
|
|
74
|
+
- Call the `attach_context_set` tool with the JSON content and the absolute file path.
|
|
75
|
+
|
|
76
|
+
7. **Generate Upload URL (Optional):**
|
|
77
|
+
- After the file is saved, ask the user if they want to generate a URL to upload the context set file.
|
|
78
|
+
- If the user confirms, you must collect the necessary database context from them. This includes:
|
|
79
|
+
- **Database Type:** 'alloydb', 'cloudsql', or 'spanner'.
|
|
80
|
+
- **Project ID:** The Google Cloud project ID.
|
|
81
|
+
- **And depending on the database type:**
|
|
82
|
+
- For 'alloydb': Location and Cluster ID.
|
|
83
|
+
- For 'cloudsql': Instance ID.
|
|
84
|
+
- For 'spanner': Instance ID and Database ID.
|
|
85
|
+
- Once you have the required information, call the `generate_upload_url` tool to provide the upload URL to the user.
|
|
86
|
+
|
|
87
|
+
Start the workflow.
|
|
88
|
+
"""
|
|
89
|
+
).format(
|
|
90
|
+
supported_db_engines=_SUPPORTED_DB_ENGINE,
|
|
91
|
+
trigram_match=MatchFunction.TRIGRAM_STRING_MATCH.value,
|
|
92
|
+
semantic_match=MatchFunction.SEMANTIC_SIMILARITY_MATCH.value,
|
|
93
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from google.cloud.db_context_enrichment.common import parameterizer
|
|
4
|
+
from google.cloud.db_context_enrichment.model import context
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def generate_templates(
|
|
8
|
+
template_inputs_json: str, sql_dialect: str = "postgresql"
|
|
9
|
+
) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Generates the final, detailed templates based on user-approved items.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
# Convert the string to the Enum member
|
|
15
|
+
db_dialect = parameterizer.SQLDialect(sql_dialect)
|
|
16
|
+
except ValueError:
|
|
17
|
+
return f'{{"error": "Invalid database dialect specified: {sql_dialect}"}}'
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
# The input is now expected to be a direct list of items
|
|
21
|
+
item_list = json.loads(template_inputs_json)
|
|
22
|
+
if not isinstance(item_list, list):
|
|
23
|
+
raise json.JSONDecodeError("Input is not a list.", template_inputs_json, 0)
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
return '{"error": "Invalid JSON format for approved items. Expected a JSON array."}'
|
|
26
|
+
|
|
27
|
+
final_templates = []
|
|
28
|
+
|
|
29
|
+
for item in item_list:
|
|
30
|
+
question = item["question"]
|
|
31
|
+
sql = item["sql"]
|
|
32
|
+
intent = item.get(
|
|
33
|
+
"intent", question
|
|
34
|
+
) # Use provided intent or fallback to question
|
|
35
|
+
|
|
36
|
+
# 1. Extract value phrases from the intent
|
|
37
|
+
phrases = await parameterizer.extract_value_phrases(nl_query=intent)
|
|
38
|
+
|
|
39
|
+
# 2. Generate the manifest
|
|
40
|
+
manifest = intent
|
|
41
|
+
# Sort keys by length descending to replace longer phrases first
|
|
42
|
+
sorted_phrases = sorted(phrases.keys(), key=len, reverse=True)
|
|
43
|
+
for phrase in sorted_phrases:
|
|
44
|
+
# Use the first identified type for the manifest
|
|
45
|
+
phrase_type = phrases[phrase][0] if phrases[phrase] else "value"
|
|
46
|
+
manifest = manifest.replace(phrase, f"a given {phrase_type}")
|
|
47
|
+
|
|
48
|
+
# 3. Parameterize the SQL and Intent
|
|
49
|
+
parameterized_result = parameterizer.parameterize_sql_and_intent(
|
|
50
|
+
phrases, sql, intent, db_dialect=db_dialect
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 4. Assemble the final template object
|
|
54
|
+
template = context.Template(
|
|
55
|
+
nl_query=question,
|
|
56
|
+
sql=sql,
|
|
57
|
+
intent=intent,
|
|
58
|
+
manifest=manifest,
|
|
59
|
+
parameterized=context.ParameterizedTemplate(
|
|
60
|
+
parameterized_sql=parameterized_result["sql"],
|
|
61
|
+
parameterized_intent=parameterized_result["intent"],
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
final_templates.append(template)
|
|
65
|
+
|
|
66
|
+
context_set = context.ContextSet(templates=final_templates, facets=None)
|
|
67
|
+
return context_set.model_dump_json(indent=2, exclude_none=True)
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from google.cloud.db_context_enrichment.model import context
|
|
5
|
+
from google.cloud.db_context_enrichment.value_search import match_templates
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_value_searches(
|
|
9
|
+
value_search_inputs_json: str,
|
|
10
|
+
dialect: str,
|
|
11
|
+
db_version: str | None = None,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Generates a list of Value Search configurations based on a JSON input list.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
value_search_inputs_json: A JSON string representing a list of dictionaries.
|
|
18
|
+
Each dictionary must contain:
|
|
19
|
+
- table_name (str)
|
|
20
|
+
- column_name (str)
|
|
21
|
+
- concept_type (str)
|
|
22
|
+
- match_function (str)
|
|
23
|
+
- description (str, optional)
|
|
24
|
+
dialect: The database dialect (e.g., 'postgresql').
|
|
25
|
+
db_version: The specific database version (optional).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A JSON string representation of a ContextSet containing all generated value searches.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
inputs: list[dict[str, Any]] = json.loads(value_search_inputs_json)
|
|
32
|
+
except json.JSONDecodeError as e:
|
|
33
|
+
return json.dumps({"error": f"Invalid JSON format: {str(e)}"})
|
|
34
|
+
|
|
35
|
+
value_searches = []
|
|
36
|
+
|
|
37
|
+
for index, item in enumerate(inputs):
|
|
38
|
+
required_fields = [
|
|
39
|
+
"table_name",
|
|
40
|
+
"column_name",
|
|
41
|
+
"concept_type",
|
|
42
|
+
"match_function",
|
|
43
|
+
]
|
|
44
|
+
for field in required_fields:
|
|
45
|
+
if not item.get(field):
|
|
46
|
+
return json.dumps(
|
|
47
|
+
{"error": f"Field '{field}' is missing at index {index}"}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
table_name = item.get("table_name")
|
|
51
|
+
column_name = item.get("column_name")
|
|
52
|
+
concept_type = item.get("concept_type")
|
|
53
|
+
match_function = item.get("match_function")
|
|
54
|
+
description = item.get("description")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
template_def = match_templates.get_match_template(
|
|
58
|
+
dialect=dialect,
|
|
59
|
+
function_name=match_function,
|
|
60
|
+
version=db_version,
|
|
61
|
+
)
|
|
62
|
+
raw_sql = template_def["sql_template"]
|
|
63
|
+
|
|
64
|
+
# Prepare formatting arguments with defaults for optional fields
|
|
65
|
+
format_args = {
|
|
66
|
+
"table": table_name,
|
|
67
|
+
"column": column_name,
|
|
68
|
+
"concept_type": concept_type,
|
|
69
|
+
"column_tokens": item.get("column_tokens", ""),
|
|
70
|
+
"column_embedding": item.get("column_embedding", ""),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
value_search_query = raw_sql.format(**format_args)
|
|
74
|
+
|
|
75
|
+
vs = context.ValueSearch(
|
|
76
|
+
concept_type=concept_type,
|
|
77
|
+
query=value_search_query,
|
|
78
|
+
description=description,
|
|
79
|
+
)
|
|
80
|
+
value_searches.append(vs)
|
|
81
|
+
|
|
82
|
+
except ValueError as e:
|
|
83
|
+
return json.dumps(
|
|
84
|
+
{
|
|
85
|
+
"error": f"Error while processing value search at index {index}: {str(e)}"
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return context.ContextSet(value_searches=value_searches).model_dump_json(
|
|
90
|
+
indent=2, exclude_none=True
|
|
91
|
+
)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Dialect(StrEnum):
|
|
6
|
+
"""Supported database dialects."""
|
|
7
|
+
|
|
8
|
+
POSTGRESQL = "postgresql"
|
|
9
|
+
MYSQL = "mysql"
|
|
10
|
+
GOOGLE_SQL = "googlesql"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MatchFunction(StrEnum):
|
|
14
|
+
"""Available match functions."""
|
|
15
|
+
|
|
16
|
+
EXACT_MATCH_STRINGS = "EXACT_MATCH_STRINGS"
|
|
17
|
+
TRIGRAM_STRING_MATCH = "TRIGRAM_STRING_MATCH"
|
|
18
|
+
SEMANTIC_SIMILARITY_MATCH = "SEMANTIC_SIMILARITY_MATCH"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_MATCH_CONFIG: dict[Dialect, dict[str, Any]] = {
|
|
22
|
+
Dialect.POSTGRESQL: {
|
|
23
|
+
"min_version": "13",
|
|
24
|
+
# Default templates
|
|
25
|
+
"defaults": {
|
|
26
|
+
MatchFunction.EXACT_MATCH_STRINGS.value: {
|
|
27
|
+
"description": "Exact match for strings (Standard SQL).",
|
|
28
|
+
"example": "Use when finding a specific state code (e.g., 'CA'), order ID, or exact product name where precise spelling is required.",
|
|
29
|
+
"sql_template": (
|
|
30
|
+
"SELECT $value as value, '{table}.{column}' as columns, "
|
|
31
|
+
"'{concept_type}' as concept_type, 0 as distance, "
|
|
32
|
+
'\'\' as context FROM "{table}" T WHERE T."{column}" = $value'
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
MatchFunction.TRIGRAM_STRING_MATCH.value: {
|
|
36
|
+
"description": "Fuzzy text match using trigram similarity (Requires extension: pg_trgm).",
|
|
37
|
+
"example": "Use when searching for names, addresses, or plain text where users might have typos, misspellings, or partial matches.",
|
|
38
|
+
"sql_template": (
|
|
39
|
+
"WITH TrigramMetrics AS ("
|
|
40
|
+
' SELECT T."{column}" AS original_value, '
|
|
41
|
+
' (T."{column}" <-> $value::text) AS normalized_dist '
|
|
42
|
+
' FROM "{table}" T '
|
|
43
|
+
' WHERE T."{column}" % $value::text'
|
|
44
|
+
") "
|
|
45
|
+
"SELECT original_value AS value, '{table}.{column}' AS columns, "
|
|
46
|
+
"'{concept_type}' AS concept_type, normalized_dist AS distance, "
|
|
47
|
+
"''::text AS context FROM TrigramMetrics"
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
MatchFunction.SEMANTIC_SIMILARITY_MATCH.value: {
|
|
51
|
+
"description": "Semantic similarity search using Gemini text embeddings (Requires extensions: vector, google_ml_integration).",
|
|
52
|
+
"example": "Use when searching for concepts, descriptions, themes, or abstract text where the exact words might differ but the underlying meaning is similar.",
|
|
53
|
+
"sql_template": (
|
|
54
|
+
"WITH SemanticMetrics AS ("
|
|
55
|
+
' SELECT T."{column}" AS original_value, ('
|
|
56
|
+
" (google_ml.embedding('gemini-embedding-001', $value)::vector <=> "
|
|
57
|
+
" google_ml.embedding('gemini-embedding-001', T.\"{column}\")::vector) / 2.0"
|
|
58
|
+
" ) AS normalized_dist "
|
|
59
|
+
' FROM "{table}" T '
|
|
60
|
+
' WHERE T."{column}" IS NOT NULL'
|
|
61
|
+
") "
|
|
62
|
+
"SELECT original_value AS value, '{table}.{column}' AS columns, "
|
|
63
|
+
"'{concept_type}' AS concept_type, normalized_dist AS distance, "
|
|
64
|
+
"''::text AS context FROM SemanticMetrics"
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"overrides": {},
|
|
69
|
+
},
|
|
70
|
+
Dialect.GOOGLE_SQL: {
|
|
71
|
+
"min_version": "1",
|
|
72
|
+
"defaults": {
|
|
73
|
+
MatchFunction.EXACT_MATCH_STRINGS.value: {
|
|
74
|
+
"description": "Exact match for strings in Spanner.",
|
|
75
|
+
"example": "Use for exact IDs or state codes in Spanner.",
|
|
76
|
+
"sql_template": (
|
|
77
|
+
"SELECT CAST($value AS STRING) AS value, '{column}' AS `columns`, "
|
|
78
|
+
"'{concept_type}' AS concept_type, 0 AS distance, "
|
|
79
|
+
"JSON '{{}}' AS context "
|
|
80
|
+
"FROM `{table}` AS T "
|
|
81
|
+
"WHERE CAST(T.`{column}` AS STRING) = CAST($value AS STRING) "
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
MatchFunction.TRIGRAM_STRING_MATCH.value: {
|
|
85
|
+
"description": "String similarity using Spanner Search Indexes.",
|
|
86
|
+
"example": "Use for typos/misspellings in Spanner using SEARCH_NGRAMS.",
|
|
87
|
+
"sql_template": (
|
|
88
|
+
"SELECT CAST(T.`{column}` AS STRING) AS value, '{column}' AS `columns`, "
|
|
89
|
+
"'{concept_type}' AS concept_type, "
|
|
90
|
+
"1 - SCORE_NGRAMS(T.`{column_tokens}`, CAST($value AS STRING)) AS distance, "
|
|
91
|
+
"JSON '{{}}' AS context "
|
|
92
|
+
"FROM `{table}` AS T "
|
|
93
|
+
"WHERE SEARCH_NGRAMS(T.`{column_tokens}`, CAST($value AS STRING)) "
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
"overrides": {},
|
|
98
|
+
},
|
|
99
|
+
Dialect.MYSQL: {
|
|
100
|
+
"min_version": "8",
|
|
101
|
+
"defaults": {
|
|
102
|
+
MatchFunction.EXACT_MATCH_STRINGS.value: {
|
|
103
|
+
"description": "Exact match for strings in MySQL.",
|
|
104
|
+
"example": "Use for exact matching in MySQL.",
|
|
105
|
+
"sql_template": (
|
|
106
|
+
"SELECT $value AS value, '{column}' AS `columns`, "
|
|
107
|
+
"'{concept_type}' AS concept_type, 0 AS distance, "
|
|
108
|
+
"JSON_OBJECT() AS context "
|
|
109
|
+
"FROM `{table}` AS T WHERE T.`{column}` = $value"
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
MatchFunction.TRIGRAM_STRING_MATCH.value: {
|
|
113
|
+
"description": "Trigram fuzzy match in MySQL using FULLTEXT index with score normalization.",
|
|
114
|
+
"example": "Use for fuzzy matching in MySQL (requires FULLTEXT index with ngram).",
|
|
115
|
+
"sql_template": (
|
|
116
|
+
"SELECT * FROM ("
|
|
117
|
+
" WITH TrigramMetrics AS ("
|
|
118
|
+
" SELECT T.`{column}` AS original_value, "
|
|
119
|
+
" MATCH(T.`{column}`) AGAINST($value IN NATURAL LANGUAGE MODE) AS raw_score "
|
|
120
|
+
" FROM `{table}` AS T "
|
|
121
|
+
" WHERE MATCH(T.`{column}`) AGAINST($value IN NATURAL LANGUAGE MODE) > 0 "
|
|
122
|
+
" ORDER BY raw_score DESC LIMIT 10"
|
|
123
|
+
" ), "
|
|
124
|
+
" NormalizationParams AS ("
|
|
125
|
+
" SELECT MAX(raw_score) AS max_score "
|
|
126
|
+
" FROM TrigramMetrics"
|
|
127
|
+
" ) "
|
|
128
|
+
" SELECT original_value AS value, '{column}' AS `columns`, "
|
|
129
|
+
" '{concept_type}' AS concept_type, "
|
|
130
|
+
" (CASE WHEN n.max_score > 0 THEN (1 - (m.raw_score / n.max_score)) ELSE 0 END) AS distance, "
|
|
131
|
+
" JSON_OBJECT() AS context "
|
|
132
|
+
" FROM TrigramMetrics m, NormalizationParams n"
|
|
133
|
+
") AS wrapped_query "
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
MatchFunction.SEMANTIC_SIMILARITY_MATCH.value: {
|
|
137
|
+
"description": "Semantic match in MySQL using Vertex AI embedding.",
|
|
138
|
+
"example": "Use for semantic matching (requires mysql.ml_embedding).",
|
|
139
|
+
"sql_template": (
|
|
140
|
+
"SELECT * FROM ("
|
|
141
|
+
" WITH search_embedding AS ("
|
|
142
|
+
" SELECT mysql.ml_embedding('text-embedding-005', $value) AS val"
|
|
143
|
+
" ) "
|
|
144
|
+
" SELECT T.`{column}` AS value, '{column}' AS `columns`, "
|
|
145
|
+
" '{concept_type}' AS concept_type, "
|
|
146
|
+
" COSINE_DISTANCE(T.`{column_embedding}`, search_embedding.val) AS distance, "
|
|
147
|
+
" JSON_OBJECT() AS context "
|
|
148
|
+
" FROM `{table}` AS T, search_embedding "
|
|
149
|
+
" WHERE T.`{column_embedding}` IS NOT NULL"
|
|
150
|
+
") AS wrapped_query "
|
|
151
|
+
),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"overrides": {},
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_version_supported(version: str, min_version: str) -> bool:
|
|
160
|
+
"""Helper to compare version strings (e.g. '13.2' >= '13')."""
|
|
161
|
+
|
|
162
|
+
def parse(v: str):
|
|
163
|
+
return tuple(map(int, v.split(".")))
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
return parse(version) >= parse(min_version)
|
|
167
|
+
except ValueError:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_match_template(
|
|
172
|
+
dialect: str, function_name: str, version: str | None = None
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""
|
|
175
|
+
Retrieves a match template with a default-fallback strategy.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
dialect: The database dialect string (e.g., 'postgresql').
|
|
179
|
+
function_name: The name of the match function.
|
|
180
|
+
version: The specific database version (optional).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A dictionary containing the template definition.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError:
|
|
187
|
+
- If dialect is invalid.
|
|
188
|
+
- If version is provided but unsupported.
|
|
189
|
+
- If function_name is not found (lists available templates).
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
dialect_enum = Dialect(dialect.lower())
|
|
193
|
+
except ValueError:
|
|
194
|
+
supported = [d.value for d in Dialect]
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Dialect '{dialect}' not supported. Supported dialects: {supported}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
engine_config = _MATCH_CONFIG.get(dialect_enum)
|
|
200
|
+
if not engine_config:
|
|
201
|
+
raise ValueError(f"Dialect '{dialect}' has no configuration registered.")
|
|
202
|
+
|
|
203
|
+
defaults = engine_config.get("defaults", {})
|
|
204
|
+
min_version = engine_config.get("min_version")
|
|
205
|
+
overrides = engine_config.get("overrides", {})
|
|
206
|
+
|
|
207
|
+
if version and min_version:
|
|
208
|
+
version = str(version)
|
|
209
|
+
if not _is_version_supported(version, min_version):
|
|
210
|
+
raise ValueError(
|
|
211
|
+
f"Version '{version}' is not supported for dialect '{dialect}'. "
|
|
212
|
+
f"Minimum required version: {min_version}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Identify specific overrides for this version (if any)
|
|
216
|
+
# Note: Overrides currently use exact version matches in the keys
|
|
217
|
+
version_overrides = overrides.get(version, {}) if version else {}
|
|
218
|
+
|
|
219
|
+
effective_templates = defaults | version_overrides
|
|
220
|
+
template = effective_templates.get(function_name)
|
|
221
|
+
|
|
222
|
+
if not template:
|
|
223
|
+
supported_templates = list(defaults.keys())
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"Match function '{function_name}' not found. "
|
|
226
|
+
f"Supported match templates: {supported_templates}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return template
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_available_functions(
|
|
233
|
+
dialect: str, version: str | None = None
|
|
234
|
+
) -> dict[str, dict[str, str]]:
|
|
235
|
+
"""
|
|
236
|
+
Returns a dictionary of available match function names with their descriptions and examples for a given dialect.
|
|
237
|
+
Validates both the dialect and the version (if provided).
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
dialect_enum = Dialect(dialect.lower())
|
|
241
|
+
except ValueError:
|
|
242
|
+
supported = [d.value for d in Dialect]
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"Dialect '{dialect}' not supported. Supported engine: {supported}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
engine_config = _MATCH_CONFIG.get(dialect_enum, {})
|
|
248
|
+
|
|
249
|
+
if version:
|
|
250
|
+
min_version = engine_config.get("min_version")
|
|
251
|
+
version = str(version)
|
|
252
|
+
if min_version and not _is_version_supported(version, min_version):
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"Version '{version}' is not supported for dialect '{dialect}'. "
|
|
255
|
+
f"Minimum required version: {min_version}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
defaults = engine_config.get("defaults", {})
|
|
259
|
+
version_overrides = (
|
|
260
|
+
engine_config.get("overrides", {}).get(version, {}) if version else {}
|
|
261
|
+
)
|
|
262
|
+
effective_templates = defaults | version_overrides
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
k: {"description": v.get("description", ""), "example": v.get("example", "")}
|
|
266
|
+
for k, v in effective_templates.items()
|
|
267
|
+
}
|