sqlflip 0.1.1__tar.gz

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.
sqlflip-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
sqlflip-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlflip
3
+ Version: 0.1.1
4
+ Summary: Detect a SQL query's dialect and view it in PostgreSQL, MySQL, SQL Server and standard SQL.
5
+ Author-email: anuvishalp <anuvishalp@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/anuvishalp/sqlflip-view
8
+ Project-URL: Source, https://github.com/anuvishalp/sqlflip-view
9
+ Project-URL: Issues, https://github.com/anuvishalp/sqlflip-view/issues
10
+ Keywords: sql,postgresql,mysql,tsql,sqlserver,sql-converter,sql-dialect
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Database
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # sqlflip
23
+
24
+ Paste a SQL query and `sqlflip` will:
25
+
26
+ 1. **Detect** which dialect it is written in - PostgreSQL, MySQL, SQL Server (T-SQL) or standard SQL.
27
+ 2. **Convert** it to the other dialects (identifier quoting, `TOP` / `LIMIT` / `OFFSET .. FETCH`,
28
+ `GETDATE()` / `NOW()`, `ISNULL` / `IFNULL` / `COALESCE`, `LEN` / `LENGTH`, common data types).
29
+ 3. **Display** all versions in a small window (Tkinter, no extra dependencies).
30
+
31
+ > T-SQL is the language of SQL Server, so `tsql` and `sqlserver` mean the same thing.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install sqlflip
37
+ ```
38
+
39
+ (On Linux you may also need `sudo apt install python3-tk` for the window.)
40
+
41
+ ## Use in Python
42
+
43
+ ```python
44
+ from sqlflip import detect_dialect, convert, convert_all, display
45
+
46
+ q = "SELECT TOP 10 [name], ISNULL([age], 0) FROM [users] ORDER BY [name]"
47
+
48
+ print(detect_dialect(q)) # SQL Server (T-SQL) (confidence: high)
49
+ print(convert(q, "postgresql").sql) # SELECT "name", COALESCE("age", 0) FROM "users" ORDER BY "name" LIMIT 10
50
+ print(convert(q, "mysql").sql) # SELECT `name`, IFNULL(`age`, 0) FROM `users` ORDER BY `name` LIMIT 10
51
+
52
+ for dialect, result in convert_all(q).items():
53
+ print(dialect, "->", result.sql, result.notes)
54
+
55
+ display(q) # opens the window
56
+ ```
57
+
58
+ ## Command line
59
+
60
+ ```bash
61
+ sqlflip "SELECT TOP 5 * FROM users"
62
+ sqlflip "SELECT * FROM users LIMIT 5, 10" --to sqlserver
63
+ sqlflip --gui
64
+ ```
65
+
66
+ ## Limitations
67
+
68
+ This is a lightweight, rule-based tool - **not** a full SQL parser. Always review the output.
69
+ Constructs it cannot safely translate (e.g. `::` casts, `||` / `+` string concatenation,
70
+ auto-increment columns, `GROUP_CONCAT` / `STRING_AGG`, date functions, variables) are left as-is
71
+ and reported in `result.notes`. Detection is heuristic: a query such as `SELECT * FROM t LIMIT 5`
72
+ is valid in both PostgreSQL and MySQL, so it is reported as ambiguous.
73
+
74
+ ## Publish to PyPI
75
+
76
+ ```bash
77
+ pip install build twine
78
+ python -m build
79
+ twine upload dist/* # username: __token__ password: <your API token>
80
+ ```
@@ -0,0 +1,59 @@
1
+ # sqlflip
2
+
3
+ Paste a SQL query and `sqlflip` will:
4
+
5
+ 1. **Detect** which dialect it is written in - PostgreSQL, MySQL, SQL Server (T-SQL) or standard SQL.
6
+ 2. **Convert** it to the other dialects (identifier quoting, `TOP` / `LIMIT` / `OFFSET .. FETCH`,
7
+ `GETDATE()` / `NOW()`, `ISNULL` / `IFNULL` / `COALESCE`, `LEN` / `LENGTH`, common data types).
8
+ 3. **Display** all versions in a small window (Tkinter, no extra dependencies).
9
+
10
+ > T-SQL is the language of SQL Server, so `tsql` and `sqlserver` mean the same thing.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install sqlflip
16
+ ```
17
+
18
+ (On Linux you may also need `sudo apt install python3-tk` for the window.)
19
+
20
+ ## Use in Python
21
+
22
+ ```python
23
+ from sqlflip import detect_dialect, convert, convert_all, display
24
+
25
+ q = "SELECT TOP 10 [name], ISNULL([age], 0) FROM [users] ORDER BY [name]"
26
+
27
+ print(detect_dialect(q)) # SQL Server (T-SQL) (confidence: high)
28
+ print(convert(q, "postgresql").sql) # SELECT "name", COALESCE("age", 0) FROM "users" ORDER BY "name" LIMIT 10
29
+ print(convert(q, "mysql").sql) # SELECT `name`, IFNULL(`age`, 0) FROM `users` ORDER BY `name` LIMIT 10
30
+
31
+ for dialect, result in convert_all(q).items():
32
+ print(dialect, "->", result.sql, result.notes)
33
+
34
+ display(q) # opens the window
35
+ ```
36
+
37
+ ## Command line
38
+
39
+ ```bash
40
+ sqlflip "SELECT TOP 5 * FROM users"
41
+ sqlflip "SELECT * FROM users LIMIT 5, 10" --to sqlserver
42
+ sqlflip --gui
43
+ ```
44
+
45
+ ## Limitations
46
+
47
+ This is a lightweight, rule-based tool - **not** a full SQL parser. Always review the output.
48
+ Constructs it cannot safely translate (e.g. `::` casts, `||` / `+` string concatenation,
49
+ auto-increment columns, `GROUP_CONCAT` / `STRING_AGG`, date functions, variables) are left as-is
50
+ and reported in `result.notes`. Detection is heuristic: a query such as `SELECT * FROM t LIMIT 5`
51
+ is valid in both PostgreSQL and MySQL, so it is reported as ambiguous.
52
+
53
+ ## Publish to PyPI
54
+
55
+ ```bash
56
+ pip install build twine
57
+ python -m build
58
+ twine upload dist/* # username: __token__ password: <your API token>
59
+ ```
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sqlflip"
7
+ version = "0.1.1"
8
+ description = "Detect a SQL query's dialect and view it in PostgreSQL, MySQL, SQL Server and standard SQL."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [{name = "anuvishalp", email = "anuvishalp@gmail.com"}]
12
+ requires-python = ">=3.8"
13
+ keywords = ["sql", "postgresql", "mysql", "tsql", "sqlserver", "sql-converter", "sql-dialect"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Database",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/anuvishalp/sqlflip-view"
25
+ Source = "https://github.com/anuvishalp/sqlflip-view"
26
+ Issues = "https://github.com/anuvishalp/sqlflip-view/issues"
27
+
28
+ [project.scripts]
29
+ sqlflip = "sqlflip.cli:main"
30
+ sqlflip-gui = "sqlflip.gui:main"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ """sqlflip - detect a SQL query's dialect and view it in other dialects."""
2
+ from .converter import Conversion, convert, convert_all
3
+ from .detector import DIALECTS, DISPLAY_NAMES, Detection, detect_dialect
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["detect_dialect", "convert", "convert_all", "display",
7
+ "Detection", "Conversion", "DIALECTS", "DISPLAY_NAMES"]
8
+
9
+
10
+ def display(query: str = "") -> None:
11
+ """Open a window showing `query` in PostgreSQL, MySQL, SQL Server and standard SQL."""
12
+ from .gui import show # imported lazily so the library works without Tkinter
13
+ show(query)
@@ -0,0 +1,4 @@
1
+ import sys
2
+ from .cli import main
3
+
4
+ sys.exit(main())
@@ -0,0 +1,22 @@
1
+ """Small helpers shared by the detector and the converter."""
2
+ import re
3
+ from typing import List, Tuple
4
+
5
+ _STRING_RE = re.compile(r"'(?:[^']|'')*'")
6
+
7
+
8
+ def mask_strings(sql: str) -> Tuple[str, List[str]]:
9
+ """Replace every 'string literal' with a placeholder so regex rules
10
+ never touch text that lives inside quotes."""
11
+ strings: List[str] = []
12
+
13
+ def _sub(match):
14
+ strings.append(match.group(0))
15
+ return "\x00%d\x00" % (len(strings) - 1)
16
+
17
+ return _STRING_RE.sub(_sub, sql), strings
18
+
19
+
20
+ def unmask_strings(sql: str, strings: List[str]) -> str:
21
+ """Put the original string literals back."""
22
+ return re.sub(r"\x00(\d+)\x00", lambda m: strings[int(m.group(1))], sql)
@@ -0,0 +1,53 @@
1
+ """Command-line interface: sqlflip "SELECT TOP 5 * FROM users" """
2
+ import argparse
3
+ import sys
4
+
5
+ from .converter import convert, convert_all
6
+ from .detector import DIALECTS, DISPLAY_NAMES, detect_dialect, normalize_dialect
7
+
8
+
9
+ def main(argv=None) -> int:
10
+ parser = argparse.ArgumentParser(
11
+ prog="sqlflip", description="Detect a query's SQL dialect and convert it.")
12
+ parser.add_argument("query", nargs="?", help="SQL text (or use --file / pipe via stdin)")
13
+ parser.add_argument("-f", "--file", help="read the query from a file")
14
+ parser.add_argument("--from", dest="source", help="source dialect (default: auto-detect)")
15
+ parser.add_argument("--to", dest="target", help="show only this target dialect")
16
+ parser.add_argument("--gui", action="store_true", help="open the display window")
17
+ args = parser.parse_args(argv)
18
+
19
+ query = args.query
20
+ if args.file:
21
+ with open(args.file, encoding="utf-8") as fh:
22
+ query = fh.read()
23
+ elif query is None and not sys.stdin.isatty():
24
+ query = sys.stdin.read()
25
+
26
+ if args.gui or not query:
27
+ from .gui import show
28
+ show(query or "")
29
+ return 0
30
+
31
+ detection = detect_dialect(query)
32
+ source = normalize_dialect(args.source) if args.source else detection.dialect
33
+ print("Detected : %s" % detection)
34
+ if args.source:
35
+ print("Using : %s" % DISPLAY_NAMES[source])
36
+ print()
37
+
38
+ if args.target:
39
+ results = {normalize_dialect(args.target): convert(query, args.target, source)}
40
+ else:
41
+ results = convert_all(query, source)
42
+
43
+ for d, res in results.items():
44
+ print("--- %s %s---" % (DISPLAY_NAMES[d], "(source) " if d == source else ""))
45
+ print(res.sql)
46
+ for note in res.notes:
47
+ print(" ! " + note)
48
+ print()
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__":
53
+ sys.exit(main())
@@ -0,0 +1,244 @@
1
+ """Convert a SQL query from one dialect to another.
2
+
3
+ This is a lightweight, rule-based converter. It handles the most common
4
+ differences (identifier quoting, row limiting, a handful of functions and data
5
+ types) and *warns* about constructs it cannot translate safely.
6
+ """
7
+ import re
8
+ from dataclasses import dataclass, field
9
+ from typing import Dict, List, Optional, Tuple
10
+
11
+ from ._utils import mask_strings, unmask_strings
12
+ from .detector import DIALECTS, detect_dialect, normalize_dialect
13
+
14
+
15
+ @dataclass
16
+ class Conversion:
17
+ dialect: str
18
+ sql: str
19
+ notes: List[str] = field(default_factory=list)
20
+
21
+
22
+ # --------------------------------------------------------------------------
23
+ # 1. Identifier quoting: [name] `name` "name"
24
+ # --------------------------------------------------------------------------
25
+ _QUOTE = {
26
+ "postgresql": ('"', '"'),
27
+ "standard": ('"', '"'),
28
+ "mysql": ("`", "`"),
29
+ "sqlserver": ("[", "]"),
30
+ }
31
+
32
+
33
+ def _requote(text: str, source: str, target: str) -> str:
34
+ opener, closer = _QUOTE[target]
35
+ if source == "sqlserver":
36
+ patterns = [r"\[([^\]\x00]+)\]", r'"([^"\x00]+)"']
37
+ elif source == "mysql":
38
+ patterns = [r"`([^`]+)`"]
39
+ else:
40
+ patterns = [r'"([^"\x00]+)"']
41
+ for pattern in patterns:
42
+ text = re.sub(pattern, lambda m: "%s%s%s" % (opener, m.group(1), closer), text)
43
+ return text
44
+
45
+
46
+ # --------------------------------------------------------------------------
47
+ # 2. Row limiting: TOP n / LIMIT n [OFFSET m] / OFFSET m ROWS FETCH ...
48
+ # --------------------------------------------------------------------------
49
+ _TOP = re.compile(r"\bSELECT(\s+DISTINCT)?\s+TOP\s*\(?\s*(\d+)\s*\)?\s+", re.I)
50
+ _LIMIT_COMMA = re.compile(r"\s*\bLIMIT\s+(\d+)\s*,\s*(\d+)", re.I)
51
+ _LIMIT = re.compile(r"\s*\bLIMIT\s+(\d+)(?:\s+OFFSET\s+(\d+))?", re.I)
52
+ _OFFSET_FETCH = re.compile(
53
+ r"\s*\bOFFSET\s+(\d+)\s+ROWS?(?:\s+FETCH\s+(?:FIRST|NEXT)\s+(\d+)\s+ROWS?\s+ONLY)?", re.I
54
+ )
55
+ _FETCH_ONLY = re.compile(r"\s*\bFETCH\s+(?:FIRST|NEXT)\s+(\d+)\s+ROWS?\s+ONLY", re.I)
56
+
57
+
58
+ def _cut(text: str, match) -> str:
59
+ return text[: match.start()] + text[match.end():]
60
+
61
+
62
+ def _extract_limit(text: str) -> Tuple[str, Optional[int], Optional[int]]:
63
+ """Remove any row-limit clause and return (text, limit, offset)."""
64
+ limit = offset = None
65
+
66
+ m = _TOP.search(text)
67
+ if m:
68
+ limit = int(m.group(2))
69
+ text = text[: m.start()] + "SELECT" + (m.group(1) or "") + " " + text[m.end():]
70
+ return text, limit, offset
71
+
72
+ m = _LIMIT_COMMA.search(text) # MySQL: LIMIT offset, count
73
+ if m:
74
+ return _cut(text, m), int(m.group(2)), int(m.group(1))
75
+
76
+ m = _LIMIT.search(text) # LIMIT n [OFFSET m]
77
+ if m:
78
+ off = int(m.group(2)) if m.group(2) else None
79
+ return _cut(text, m), int(m.group(1)), off
80
+
81
+ m = _OFFSET_FETCH.search(text) # OFFSET m ROWS [FETCH NEXT n ROWS ONLY]
82
+ if m:
83
+ lim = int(m.group(2)) if m.group(2) else None
84
+ return _cut(text, m), lim, int(m.group(1))
85
+
86
+ m = _FETCH_ONLY.search(text) # FETCH FIRST n ROWS ONLY
87
+ if m:
88
+ return _cut(text, m), int(m.group(1)), None
89
+
90
+ return text, None, None
91
+
92
+
93
+ def _apply_limit(text: str, limit: Optional[int], offset: Optional[int], target: str) -> str:
94
+ if limit is None and not offset:
95
+ return text
96
+
97
+ if target in ("postgresql", "mysql"):
98
+ if target == "mysql" and limit is None:
99
+ limit = 18446744073709551615 # MySQL needs a LIMIT to use OFFSET
100
+ clause = ""
101
+ if limit is not None:
102
+ clause += " LIMIT %d" % limit
103
+ if offset:
104
+ clause += " OFFSET %d" % offset
105
+ return text + clause
106
+
107
+ if target == "standard":
108
+ clause = ""
109
+ if offset:
110
+ clause += " OFFSET %d ROWS" % offset
111
+ if limit is not None:
112
+ clause += " FETCH NEXT %d ROWS ONLY" % limit
113
+ return text + clause
114
+
115
+ # SQL Server
116
+ if not offset:
117
+ return re.sub(
118
+ r"^(\s*SELECT(?:\s+DISTINCT)?)\s+",
119
+ lambda m: "%s TOP %d " % (m.group(1), limit),
120
+ text, count=1, flags=re.I,
121
+ )
122
+ if not re.search(r"\bORDER\s+BY\b", text, re.I):
123
+ text += " ORDER BY (SELECT NULL)" # OFFSET/FETCH requires ORDER BY
124
+ text += " OFFSET %d ROWS" % offset
125
+ if limit is not None:
126
+ text += " FETCH NEXT %d ROWS ONLY" % limit
127
+ return text
128
+
129
+
130
+ # --------------------------------------------------------------------------
131
+ # 3. Functions
132
+ # --------------------------------------------------------------------------
133
+ _NOW = {"sqlserver": "GETDATE()", "mysql": "NOW()", "postgresql": "NOW()",
134
+ "standard": "CURRENT_TIMESTAMP"}
135
+ _NULLFN = {"sqlserver": "ISNULL", "mysql": "IFNULL", "postgresql": "COALESCE",
136
+ "standard": "COALESCE"}
137
+ _LENFN = {"sqlserver": "LEN", "mysql": "CHAR_LENGTH", "postgresql": "LENGTH",
138
+ "standard": "CHAR_LENGTH"}
139
+
140
+
141
+ def _convert_functions(text: str, source: str, target: str) -> str:
142
+ text = re.sub(r"\b(?:GETDATE|SYSDATETIME|NOW|CURRENT_TIMESTAMP)\s*\(\s*\)",
143
+ _NOW[target], text, flags=re.I)
144
+
145
+ null_names = "IFNULL|ISNULL" if source == "sqlserver" else "IFNULL"
146
+ text = re.sub(r"\b(?:%s)\s*\(" % null_names, _NULLFN[target] + "(", text, flags=re.I)
147
+
148
+ text = re.sub(r"\b(?:LEN|LENGTH|CHAR_LENGTH|CHARACTER_LENGTH)\s*\(",
149
+ _LENFN[target] + "(", text, flags=re.I)
150
+ return text
151
+
152
+
153
+ # --------------------------------------------------------------------------
154
+ # 4. Data types (only for CREATE / ALTER statements)
155
+ # --------------------------------------------------------------------------
156
+ _TYPES = {
157
+ "sqlserver": [(r"\bBOOLEAN\b", "BIT"), (r"\bTIMESTAMP\b", "DATETIME2"),
158
+ (r"\bTEXT\b", "NVARCHAR(MAX)")],
159
+ "mysql": [(r"\bN?VARCHAR\s*\(\s*MAX\s*\)", "TEXT"), (r"\bNVARCHAR\b", "VARCHAR"),
160
+ (r"\bDATETIME2\b", "DATETIME"), (r"\bBIT\b", "BOOLEAN")],
161
+ "postgresql": [(r"\bN?VARCHAR\s*\(\s*MAX\s*\)", "TEXT"), (r"\bNVARCHAR\b", "VARCHAR"),
162
+ (r"\b(?:DATETIME2|DATETIME)\b", "TIMESTAMP"), (r"\bBIT\b", "BOOLEAN"),
163
+ (r"\bTINYINT\b", "SMALLINT")],
164
+ }
165
+ _TYPES["standard"] = _TYPES["postgresql"]
166
+
167
+
168
+ def _convert_types(text: str, target: str) -> str:
169
+ if not re.match(r"\s*(CREATE|ALTER)\b", text, re.I):
170
+ return text
171
+ for pattern, repl in _TYPES[target]:
172
+ text = re.sub(pattern, repl, text, flags=re.I)
173
+ return text
174
+
175
+
176
+ # --------------------------------------------------------------------------
177
+ # 5. Warnings for things we do NOT translate
178
+ # --------------------------------------------------------------------------
179
+ def _collect_notes(masked: str, source: str, target: str) -> List[str]:
180
+ notes: List[str] = []
181
+
182
+ def has(pattern: str) -> bool:
183
+ return re.search(pattern, masked, re.I) is not None
184
+
185
+ if has(r"::\s*\w+") and target != "postgresql":
186
+ notes.append("PostgreSQL '::' casts were not converted - rewrite as CAST(expr AS type).")
187
+ if has(r"\|\|") and target in ("mysql", "sqlserver"):
188
+ notes.append("String concatenation with '||' was not converted - use CONCAT() "
189
+ "(MySQL / SQL Server) or '+' (SQL Server).")
190
+ if source == "sqlserver" and target != "sqlserver" and has(r"\x00\d+\x00\s*\+|\+\s*\x00\d+\x00"):
191
+ notes.append("String concatenation with '+' was not converted - use CONCAT() or '||'.")
192
+ if has(r"\b(AUTO_INCREMENT|IDENTITY|SERIAL|BIGSERIAL)\b"):
193
+ notes.append("Auto-increment columns differ (AUTO_INCREMENT / IDENTITY / SERIAL) - review manually.")
194
+ if has(r"\b(GROUP_CONCAT|STRING_AGG)\s*\("):
195
+ notes.append("GROUP_CONCAT / STRING_AGG are not converted - review manually.")
196
+ if has(r"\b(DATEADD|DATE_ADD|DATEDIFF|DATE_FORMAT|TO_CHAR|CONVERT|INTERVAL)\b"):
197
+ notes.append("Date/time functions differ a lot between dialects - review manually.")
198
+ if has(r"(?<![\w@])@\w+|\$\d+"):
199
+ notes.append("Variables / parameters (@name, $1) were not converted.")
200
+ return notes
201
+
202
+
203
+ # --------------------------------------------------------------------------
204
+ # Public API
205
+ # --------------------------------------------------------------------------
206
+ def convert(query: str, target: str, source: Optional[str] = None) -> Conversion:
207
+ """Convert `query` to the `target` dialect.
208
+
209
+ If `source` is omitted the dialect is auto-detected.
210
+ """
211
+ target = normalize_dialect(target)
212
+ body = (query or "").strip()
213
+ if not body:
214
+ raise ValueError("The query is empty.")
215
+ src = normalize_dialect(source) if source else detect_dialect(body).dialect
216
+
217
+ if src == target:
218
+ return Conversion(target, body, [])
219
+
220
+ masked, strings = mask_strings(body)
221
+ notes = _collect_notes(masked, src, target)
222
+
223
+ text = masked.rstrip()
224
+ had_semicolon = text.endswith(";")
225
+ text = text.rstrip(";").rstrip()
226
+
227
+ text = _requote(text, src, target)
228
+ text, limit, offset = _extract_limit(text)
229
+ text = _apply_limit(text, limit, offset, target)
230
+ text = _convert_functions(text, src, target)
231
+ text = _convert_types(text, target)
232
+
233
+ if had_semicolon:
234
+ text += ";"
235
+ return Conversion(target, unmask_strings(text, strings), notes)
236
+
237
+
238
+ def convert_all(query: str, source: Optional[str] = None) -> Dict[str, Conversion]:
239
+ """Return the query in every supported dialect."""
240
+ body = (query or "").strip()
241
+ if not body:
242
+ raise ValueError("The query is empty.")
243
+ src = normalize_dialect(source) if source else detect_dialect(body).dialect
244
+ return {d: convert(body, d, src) for d in DIALECTS}
@@ -0,0 +1,146 @@
1
+ """Detect which SQL dialect a query is written in."""
2
+ import re
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, List, Tuple
5
+
6
+ from ._utils import mask_strings
7
+
8
+ # Order matters: it is also the tie-break order and the tab order in the GUI.
9
+ DIALECTS = ("postgresql", "mysql", "sqlserver", "standard")
10
+
11
+ DISPLAY_NAMES = {
12
+ "postgresql": "PostgreSQL",
13
+ "mysql": "MySQL",
14
+ "sqlserver": "SQL Server (T-SQL)",
15
+ "standard": "Standard SQL",
16
+ }
17
+
18
+ # T-SQL *is* the language of SQL Server, so both names map to "sqlserver".
19
+ _ALIASES = {
20
+ "postgresql": "postgresql", "postgres": "postgresql", "pg": "postgresql",
21
+ "psql": "postgresql", "plpgsql": "postgresql",
22
+ "mysql": "mysql", "mariadb": "mysql",
23
+ "sqlserver": "sqlserver", "sql server": "sqlserver", "sql_server": "sqlserver",
24
+ "tsql": "sqlserver", "t-sql": "sqlserver", "mssql": "sqlserver",
25
+ "standard": "standard", "sql": "standard", "ansi": "standard",
26
+ }
27
+
28
+
29
+ def normalize_dialect(name: str) -> str:
30
+ key = (name or "").strip().lower()
31
+ if key not in _ALIASES:
32
+ raise ValueError(
33
+ "Unknown dialect %r. Use one of: postgresql, mysql, sqlserver "
34
+ "(or tsql), standard (or sql)." % name
35
+ )
36
+ return _ALIASES[key]
37
+
38
+
39
+ # (dialect, regex, weight, human-readable label)
40
+ _RULES: List[Tuple[str, str, int, str]] = [
41
+ # ---- SQL Server / T-SQL ------------------------------------------------
42
+ ("sqlserver", r"\bSELECT\s+(DISTINCT\s+)?TOP\s*\(?\s*\d+", 3, "SELECT TOP n"),
43
+ ("sqlserver", r"(?<![\w\)\]])\[[A-Za-z_][\w ]*\]", 3, "[bracket] identifiers"),
44
+ ("sqlserver", r"\bGETDATE\s*\(|\bSYSDATETIME\s*\(", 3, "GETDATE()"),
45
+ ("sqlserver", r"\bISNULL\s*\([^,()]+,", 2, "ISNULL(a, b)"),
46
+ ("sqlserver", r"\bLEN\s*\(", 2, "LEN()"),
47
+ ("sqlserver", r"\bDATEADD\s*\(", 2, "DATEADD()"),
48
+ ("sqlserver", r"\bNVARCHAR\b|\bDATETIME2\b|\bUNIQUEIDENTIFIER\b|\bNEWID\s*\(", 2, "SQL Server data types"),
49
+ ("sqlserver", r"\bWITH\s*\(\s*NOLOCK\s*\)", 3, "WITH (NOLOCK)"),
50
+ ("sqlserver", r"\bIDENTITY\s*\(\s*\d+\s*,\s*\d+\s*\)", 3, "IDENTITY(1,1)"),
51
+ ("sqlserver", r"\bOFFSET\s+\d+\s+ROWS?\s+FETCH\b", 2, "OFFSET .. FETCH NEXT"),
52
+ ("sqlserver", r"\bdbo\.", 2, "dbo. schema"),
53
+ ("sqlserver", r"\bSET\s+NOCOUNT\b|\bEXEC(UTE)?\s+\w", 3, "EXEC / SET NOCOUNT"),
54
+ ("sqlserver", r"\b(FROM|INTO|JOIN)\s+#\w+", 3, "#temp tables"),
55
+ ("sqlserver", r"\bCONVERT\s*\(\s*\w+(\(\s*\w+\s*\))?\s*,", 2, "CONVERT(type, x)"),
56
+ ("sqlserver", r"(?<![\w@])@\w+", 1, "@variables"),
57
+ ("sqlserver", r"\bSTRING_AGG\s*\(", 1, "STRING_AGG()"),
58
+ # ---- MySQL -------------------------------------------------------------
59
+ ("mysql", r"`[^`]+`", 3, "`backtick` identifiers"),
60
+ ("mysql", r"\bLIMIT\s+\d+\s*,\s*\d+", 3, "LIMIT offset, count"),
61
+ ("mysql", r"\bIFNULL\s*\(", 2, "IFNULL()"),
62
+ ("mysql", r"\bAUTO_INCREMENT\b", 3, "AUTO_INCREMENT"),
63
+ ("mysql", r"\bENGINE\s*=", 3, "ENGINE="),
64
+ ("mysql", r"\b(CURDATE|CURTIME|UNIX_TIMESTAMP)\s*\(", 2, "CURDATE()/CURTIME()"),
65
+ ("mysql", r"\bGROUP_CONCAT\s*\(", 3, "GROUP_CONCAT()"),
66
+ ("mysql", r"\bON\s+DUPLICATE\s+KEY\s+UPDATE\b", 3, "ON DUPLICATE KEY UPDATE"),
67
+ ("mysql", r"\bINSERT\s+IGNORE\b", 3, "INSERT IGNORE"),
68
+ ("mysql", r"\bDATE_FORMAT\s*\(", 2, "DATE_FORMAT()"),
69
+ ("mysql", r"^\s*SHOW\s+(TABLES|DATABASES|COLUMNS)\b", 2, "SHOW TABLES"),
70
+ ("mysql", r"\bUNSIGNED\b|\bTINYINT\s*\(", 2, "MySQL data types"),
71
+ ("mysql", r"\bLIMIT\s+\d+", 1, "LIMIT n"),
72
+ ("mysql", r"\bNOW\s*\(\s*\)", 1, "NOW()"),
73
+ ("mysql", r"(?<![\w@])@\w+", 1, "@variables"),
74
+ # ---- PostgreSQL --------------------------------------------------------
75
+ ("postgresql", r"::\s*\w+", 3, ":: type cast"),
76
+ ("postgresql", r"\bILIKE\b", 3, "ILIKE"),
77
+ ("postgresql", r"\bRETURNING\b", 3, "RETURNING"),
78
+ ("postgresql", r"\b(BIG)?SERIAL\b", 3, "SERIAL"),
79
+ ("postgresql", r"\$\d+", 3, "$1 parameters"),
80
+ ("postgresql", r"\bGENERATE_SERIES\s*\(", 3, "GENERATE_SERIES()"),
81
+ ("postgresql", r"\bJSONB\b", 3, "JSONB"),
82
+ ("postgresql", r"\bARRAY\s*\[", 3, "ARRAY[...]"),
83
+ ("postgresql", r"\bON\s+CONFLICT\b", 3, "ON CONFLICT"),
84
+ ("postgresql", r"\bTO_CHAR\s*\(|\bTO_DATE\s*\(", 2, "TO_CHAR()/TO_DATE()"),
85
+ ("postgresql", r"\bSTRING_AGG\s*\(", 2, "STRING_AGG()"),
86
+ ("postgresql", r"\|\|", 1, "|| concatenation"),
87
+ ("postgresql", r"\bLIMIT\s+\d+", 1, "LIMIT n"),
88
+ ("postgresql", r"\bNOW\s*\(\s*\)", 1, "NOW()"),
89
+ # ---- Standard SQL ------------------------------------------------------
90
+ ("standard", r"\bFETCH\s+FIRST\s+\d+\s+ROWS?\s+ONLY\b", 2, "FETCH FIRST n ROWS ONLY"),
91
+ ("standard", r"\bCURRENT_TIMESTAMP\b", 1, "CURRENT_TIMESTAMP"),
92
+ ("standard", r"\bCOALESCE\s*\(", 1, "COALESCE()"),
93
+ ("standard", r"\|\|", 1, "|| concatenation"),
94
+ ]
95
+
96
+ _COMPILED = [(d, re.compile(p, re.I | re.M), w, lbl) for d, p, w, lbl in _RULES]
97
+
98
+
99
+ @dataclass
100
+ class Detection:
101
+ dialect: str # best guess
102
+ candidates: List[str] # all dialects tied for first place
103
+ scores: Dict[str, int]
104
+ evidence: List[Tuple[str, str]] = field(default_factory=list)
105
+ confidence: str = "low" # low / medium / high
106
+
107
+ @property
108
+ def display_name(self) -> str:
109
+ return DISPLAY_NAMES[self.dialect]
110
+
111
+ @property
112
+ def ambiguous(self) -> bool:
113
+ return len(self.candidates) > 1
114
+
115
+ def __str__(self) -> str:
116
+ if self.ambiguous:
117
+ names = " or ".join(DISPLAY_NAMES[c] for c in self.candidates)
118
+ return "Ambiguous: could be %s (confidence: low)" % names
119
+ return "%s (confidence: %s)" % (self.display_name, self.confidence)
120
+
121
+
122
+ def detect_dialect(query: str) -> Detection:
123
+ """Guess the SQL dialect of `query` by scoring dialect-specific syntax."""
124
+ masked, _ = mask_strings(query)
125
+ scores = {d: 0 for d in DIALECTS}
126
+ evidence: List[Tuple[str, str]] = []
127
+
128
+ for dialect, rx, weight, label in _COMPILED:
129
+ if rx.search(masked):
130
+ scores[dialect] += weight
131
+ evidence.append((dialect, label))
132
+
133
+ top = max(scores.values())
134
+ if top == 0:
135
+ # Nothing dialect-specific found -> plain SQL that runs almost anywhere.
136
+ return Detection("standard", ["standard"], scores, [], "low")
137
+
138
+ candidates = [d for d in DIALECTS if scores[d] == top]
139
+ second = sorted(scores.values(), reverse=True)[1]
140
+ if len(candidates) > 1:
141
+ confidence = "low"
142
+ elif top >= 3 and top >= 2 * second:
143
+ confidence = "high"
144
+ else:
145
+ confidence = "medium"
146
+ return Detection(candidates[0], candidates, scores, evidence, confidence)
@@ -0,0 +1,107 @@
1
+ """A small Tkinter window that shows a query in every SQL dialect."""
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+
5
+ from .converter import convert_all
6
+ from .detector import DIALECTS, DISPLAY_NAMES, detect_dialect, normalize_dialect
7
+
8
+ _AUTO = "Auto-detect"
9
+ _MONO = ("Consolas", 11)
10
+
11
+
12
+ def show(query: str = "") -> None:
13
+ """Open the window. Optionally pre-fill it with `query`."""
14
+ root = tk.Tk()
15
+ root.title("sqlflip - SQL dialect detector & converter")
16
+ root.geometry("920x680")
17
+ root.minsize(700, 520)
18
+
19
+ main = ttk.Frame(root, padding=10)
20
+ main.pack(fill="both", expand=True)
21
+
22
+ # ---- input --------------------------------------------------------------
23
+ ttk.Label(main, text="Paste your SQL query:").pack(anchor="w")
24
+ input_box = tk.Text(main, height=8, font=_MONO, wrap="word", undo=True)
25
+ input_box.pack(fill="x", pady=(2, 8))
26
+ input_box.insert("1.0", query)
27
+
28
+ # ---- controls -----------------------------------------------------------
29
+ controls = ttk.Frame(main)
30
+ controls.pack(fill="x")
31
+ ttk.Label(controls, text="Source dialect:").pack(side="left")
32
+ source_var = tk.StringVar(value=_AUTO)
33
+ ttk.Combobox(
34
+ controls, textvariable=source_var, state="readonly", width=20,
35
+ values=[_AUTO] + [DISPLAY_NAMES[d] for d in DIALECTS],
36
+ ).pack(side="left", padx=6)
37
+
38
+ status_var = tk.StringVar(value="Paste a query and click 'Detect & Convert'.")
39
+ ttk.Label(main, textvariable=status_var, font=("Segoe UI", 10, "bold")).pack(anchor="w", pady=(8, 4))
40
+
41
+ # ---- one tab per dialect --------------------------------------------------
42
+ notebook = ttk.Notebook(main)
43
+ notebook.pack(fill="both", expand=True)
44
+ outputs, note_vars = {}, {}
45
+
46
+ def make_copy(d):
47
+ def _copy():
48
+ root.clipboard_clear()
49
+ root.clipboard_append(outputs[d].get("1.0", "end").strip())
50
+ return _copy
51
+
52
+ for d in DIALECTS:
53
+ tab = ttk.Frame(notebook, padding=6)
54
+ notebook.add(tab, text=DISPLAY_NAMES[d])
55
+ box = tk.Text(tab, font=_MONO, wrap="word", state="disabled", bg="#f6f6f6")
56
+ box.pack(fill="both", expand=True)
57
+ outputs[d] = box
58
+ note_vars[d] = tk.StringVar()
59
+ ttk.Label(tab, textvariable=note_vars[d], foreground="#a05a00",
60
+ wraplength=840, justify="left").pack(anchor="w", pady=(6, 0))
61
+ ttk.Button(tab, text="Copy", command=make_copy(d)).pack(anchor="e", pady=(6, 0))
62
+
63
+ # ---- actions -----------------------------------------------------------
64
+ def run():
65
+ q = input_box.get("1.0", "end").strip()
66
+ if not q:
67
+ status_var.set("Please paste a SQL query first.")
68
+ return
69
+ detection = detect_dialect(q)
70
+ chosen = source_var.get()
71
+ if chosen == _AUTO:
72
+ source = detection.dialect
73
+ status_var.set("Detected: %s" % detection)
74
+ else:
75
+ source = next(d for d in DIALECTS if DISPLAY_NAMES[d] == chosen)
76
+ status_var.set("Using source dialect: %s" % chosen)
77
+
78
+ results = convert_all(q, source)
79
+ for i, d in enumerate(DIALECTS):
80
+ box = outputs[d]
81
+ box.config(state="normal")
82
+ box.delete("1.0", "end")
83
+ box.insert("1.0", results[d].sql)
84
+ box.config(state="disabled")
85
+ note_vars[d].set("\n".join("! " + n for n in results[d].notes))
86
+ title = DISPLAY_NAMES[d] + (" (source)" if d == source else "")
87
+ notebook.tab(i, text=title)
88
+ notebook.select(DIALECTS.index(source))
89
+
90
+ def clear():
91
+ input_box.delete("1.0", "end")
92
+ status_var.set("Paste a query and click 'Detect & Convert'.")
93
+
94
+ ttk.Button(controls, text="Detect & Convert", command=run).pack(side="left", padx=6)
95
+ ttk.Button(controls, text="Clear", command=clear).pack(side="left")
96
+
97
+ if query.strip():
98
+ run()
99
+ root.mainloop()
100
+
101
+
102
+ def main() -> None:
103
+ show()
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlflip
3
+ Version: 0.1.1
4
+ Summary: Detect a SQL query's dialect and view it in PostgreSQL, MySQL, SQL Server and standard SQL.
5
+ Author-email: anuvishalp <anuvishalp@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/anuvishalp/sqlflip-view
8
+ Project-URL: Source, https://github.com/anuvishalp/sqlflip-view
9
+ Project-URL: Issues, https://github.com/anuvishalp/sqlflip-view/issues
10
+ Keywords: sql,postgresql,mysql,tsql,sqlserver,sql-converter,sql-dialect
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Database
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # sqlflip
23
+
24
+ Paste a SQL query and `sqlflip` will:
25
+
26
+ 1. **Detect** which dialect it is written in - PostgreSQL, MySQL, SQL Server (T-SQL) or standard SQL.
27
+ 2. **Convert** it to the other dialects (identifier quoting, `TOP` / `LIMIT` / `OFFSET .. FETCH`,
28
+ `GETDATE()` / `NOW()`, `ISNULL` / `IFNULL` / `COALESCE`, `LEN` / `LENGTH`, common data types).
29
+ 3. **Display** all versions in a small window (Tkinter, no extra dependencies).
30
+
31
+ > T-SQL is the language of SQL Server, so `tsql` and `sqlserver` mean the same thing.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install sqlflip
37
+ ```
38
+
39
+ (On Linux you may also need `sudo apt install python3-tk` for the window.)
40
+
41
+ ## Use in Python
42
+
43
+ ```python
44
+ from sqlflip import detect_dialect, convert, convert_all, display
45
+
46
+ q = "SELECT TOP 10 [name], ISNULL([age], 0) FROM [users] ORDER BY [name]"
47
+
48
+ print(detect_dialect(q)) # SQL Server (T-SQL) (confidence: high)
49
+ print(convert(q, "postgresql").sql) # SELECT "name", COALESCE("age", 0) FROM "users" ORDER BY "name" LIMIT 10
50
+ print(convert(q, "mysql").sql) # SELECT `name`, IFNULL(`age`, 0) FROM `users` ORDER BY `name` LIMIT 10
51
+
52
+ for dialect, result in convert_all(q).items():
53
+ print(dialect, "->", result.sql, result.notes)
54
+
55
+ display(q) # opens the window
56
+ ```
57
+
58
+ ## Command line
59
+
60
+ ```bash
61
+ sqlflip "SELECT TOP 5 * FROM users"
62
+ sqlflip "SELECT * FROM users LIMIT 5, 10" --to sqlserver
63
+ sqlflip --gui
64
+ ```
65
+
66
+ ## Limitations
67
+
68
+ This is a lightweight, rule-based tool - **not** a full SQL parser. Always review the output.
69
+ Constructs it cannot safely translate (e.g. `::` casts, `||` / `+` string concatenation,
70
+ auto-increment columns, `GROUP_CONCAT` / `STRING_AGG`, date functions, variables) are left as-is
71
+ and reported in `result.notes`. Detection is heuristic: a query such as `SELECT * FROM t LIMIT 5`
72
+ is valid in both PostgreSQL and MySQL, so it is reported as ambiguous.
73
+
74
+ ## Publish to PyPI
75
+
76
+ ```bash
77
+ pip install build twine
78
+ python -m build
79
+ twine upload dist/* # username: __token__ password: <your API token>
80
+ ```
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/sqlflip/__init__.py
5
+ src/sqlflip/__main__.py
6
+ src/sqlflip/_utils.py
7
+ src/sqlflip/cli.py
8
+ src/sqlflip/converter.py
9
+ src/sqlflip/detector.py
10
+ src/sqlflip/gui.py
11
+ src/sqlflip.egg-info/PKG-INFO
12
+ src/sqlflip.egg-info/SOURCES.txt
13
+ src/sqlflip.egg-info/dependency_links.txt
14
+ src/sqlflip.egg-info/entry_points.txt
15
+ src/sqlflip.egg-info/top_level.txt
16
+ tests/test_sqlflip.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ sqlflip = sqlflip.cli:main
3
+ sqlflip-gui = sqlflip.gui:main
@@ -0,0 +1 @@
1
+ sqlflip
@@ -0,0 +1,73 @@
1
+ import unittest
2
+
3
+ from sqlflip import convert, convert_all, detect_dialect
4
+
5
+
6
+ class DetectTests(unittest.TestCase):
7
+ def test_sqlserver(self):
8
+ self.assertEqual(detect_dialect("SELECT TOP 10 [name] FROM [users]").dialect, "sqlserver")
9
+
10
+ def test_mysql(self):
11
+ self.assertEqual(detect_dialect("SELECT `name` FROM `users` LIMIT 5, 10").dialect, "mysql")
12
+
13
+ def test_postgres(self):
14
+ self.assertEqual(detect_dialect("SELECT name FROM users WHERE name ILIKE 'a%'").dialect, "postgresql")
15
+
16
+ def test_plain_is_standard(self):
17
+ d = detect_dialect("SELECT name FROM users WHERE id = 1")
18
+ self.assertEqual(d.dialect, "standard")
19
+
20
+ def test_ambiguous_limit(self):
21
+ d = detect_dialect("SELECT name FROM users LIMIT 5")
22
+ self.assertTrue(d.ambiguous)
23
+
24
+ def test_strings_are_ignored(self):
25
+ d = detect_dialect("SELECT 'TOP 5 [x] ILIKE' FROM users")
26
+ self.assertEqual(d.dialect, "standard")
27
+
28
+
29
+ class ConvertTests(unittest.TestCase):
30
+ Q = "SELECT TOP 10 [name], ISNULL([age], 0) FROM [users] ORDER BY [name]"
31
+
32
+ def test_sqlserver_to_postgres(self):
33
+ r = convert(self.Q, "postgresql")
34
+ self.assertEqual(r.sql, 'SELECT "name", COALESCE("age", 0) FROM "users" ORDER BY "name" LIMIT 10')
35
+
36
+ def test_sqlserver_to_mysql(self):
37
+ r = convert(self.Q, "mysql")
38
+ self.assertEqual(r.sql, "SELECT `name`, IFNULL(`age`, 0) FROM `users` ORDER BY `name` LIMIT 10")
39
+
40
+ def test_mysql_offset_to_sqlserver(self):
41
+ r = convert("SELECT `id` FROM `t` ORDER BY `id` LIMIT 5, 10", "tsql")
42
+ self.assertEqual(r.sql, "SELECT [id] FROM [t] ORDER BY [id] OFFSET 5 ROWS FETCH NEXT 10 ROWS ONLY")
43
+
44
+ def test_offset_without_order_by(self):
45
+ r = convert("SELECT id FROM t LIMIT 10 OFFSET 20", "sqlserver", source="postgresql")
46
+ self.assertIn("ORDER BY (SELECT NULL) OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY", r.sql)
47
+
48
+ def test_string_literals_untouched(self):
49
+ r = convert("SELECT TOP 3 'LIMIT 5 [x]' FROM [t]", "postgresql")
50
+ self.assertEqual(r.sql, "SELECT 'LIMIT 5 [x]' FROM \"t\" LIMIT 3")
51
+
52
+ def test_now_and_len(self):
53
+ r = convert("SELECT LEN(name), GETDATE() FROM t", "postgresql", source="sqlserver")
54
+ self.assertEqual(r.sql, "SELECT LENGTH(name), NOW() FROM t")
55
+
56
+ def test_semicolon_kept(self):
57
+ r = convert("SELECT TOP 1 * FROM t;", "mysql", source="sqlserver")
58
+ self.assertEqual(r.sql, "SELECT * FROM t LIMIT 1;")
59
+
60
+ def test_notes_for_cast(self):
61
+ r = convert("SELECT id::text FROM t", "mysql", source="postgresql")
62
+ self.assertTrue(r.notes)
63
+
64
+ def test_convert_all_has_four_dialects(self):
65
+ self.assertEqual(len(convert_all("SELECT 1")), 4)
66
+
67
+ def test_empty_query(self):
68
+ with self.assertRaises(ValueError):
69
+ convert(" ", "mysql")
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()