Coverage for app / translator.py: 100%
54 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 13:09 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-16 13:09 +0000
1import re
2from typing import Callable
5def parse_query_delimiters(sql: str) -> list[str | None]:
6 delimiters = re.findall(r"\s*;\s*", sql)
7 return delimiters
10def merge_sql_strings(queries: list[str], delimiters: list[str | None]) -> str:
11 if not queries:
12 return ""
14 if not delimiters:
15 return queries[0]
17 n_queries = len(queries)
18 n_delimiters = len(delimiters)
19 diff = n_queries - n_delimiters
21 if n_delimiters != n_queries and n_delimiters != (n_queries - 1):
22 raise ValueError(
23 f"The number of delimiters ({n_delimiters}) must be equal to or one less than the "
24 f"number of queries ({n_queries})."
25 )
27 result = "".join(
28 [f"{query}{delimiter}" for query, delimiter in zip(queries, delimiters)]
29 )
31 if diff == 1:
32 result += queries[-1]
34 return result
37def get_casing(words: list[str]) -> Callable[[str], str]:
38 first_alpha = next((char for char in "".join(words) if char.isalpha()), "a")
39 return str.upper if first_alpha.isupper() else str.lower
42def restore_casing(original: str, transpiled: str) -> str:
43 """
44 SQLGlot doesn't preserve the original casing of the original sql string.
45 This function does a 'best effort' job of trying to restore the orignal casing.
47 This is done by comparing each 'word' from the original sql string to the
48 transpiled sql string. It looks for exact matches based on a case insensitive
49 comparison.
51 We then figure out "implied" matches based on any words it skips.
53 Once the range of original words maps to the range of transpiled words
54 we apply the casing of the original word based on the first letter.
55 """
56 original_words = re.split(r"(\s+)", original)
57 transpiled_words_lower = re.split(r"(\s+)", transpiled.lower())
58 original_mapping = []
59 transpiled_mapping = []
60 last_original_match_idx = 0
61 last_transpiled_match_idx = 0
62 for original_idx, original_word in enumerate(original_words):
63 if original_word.isspace():
64 continue
66 original_word_lower = original_word.lower()
67 transpiled_words_subset = transpiled_words_lower[last_transpiled_match_idx:]
69 if original_word_lower in transpiled_words_subset:
70 transpiled_index = transpiled_words_subset.index(original_word_lower)
71 transpiled_index += last_transpiled_match_idx
73 if (original_idx - last_original_match_idx) > 0:
74 original_mapping.append(
75 get_casing(original_words[last_original_match_idx:original_idx])
76 )
77 original_mapping.append(
78 get_casing(original_words[original_idx : (original_idx + 1)])
79 )
80 last_original_match_idx = original_idx + 1
82 if (transpiled_index - last_transpiled_match_idx) > 0:
83 transpiled_mapping.append((last_transpiled_match_idx, transpiled_index))
84 transpiled_mapping.append((transpiled_index, transpiled_index + 1))
85 last_transpiled_match_idx = transpiled_index + 1
87 if last_original_match_idx < len(original_words):
88 original_mapping.append(
89 get_casing(original_words[last_original_match_idx : len(original_words)])
90 )
92 if last_transpiled_match_idx < len(transpiled_words_lower):
93 transpiled_mapping.append(
94 (last_transpiled_match_idx, len(transpiled_words_lower))
95 )
97 result = ""
98 for case_func, (start_idx, end_idx) in zip(original_mapping, transpiled_mapping):
99 for word in transpiled_words_lower[start_idx:end_idx]:
100 result += case_func(word)
102 return result