sql2sqlx 0.1.0__py3-none-any.whl → 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.
sql2sqlx/converter.py CHANGED
@@ -46,7 +46,13 @@ from concurrent.futures import ProcessPoolExecutor
46
46
  from pathlib import Path, PurePosixPath
47
47
  from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
48
48
 
49
- from sql2sqlx.emitter import apply_edits_escaped, build_sqlx, ref_expr, sqlx_escape_edits
49
+ from sql2sqlx.emitter import (
50
+ apply_edits_escaped,
51
+ build_sqlx,
52
+ normalize_sqlx_comment,
53
+ ref_expr,
54
+ sqlx_escape_edits,
55
+ )
50
56
  from sql2sqlx.errors import ConversionError, LexError
51
57
  from sql2sqlx.lexer import IDENT, OP, LineIndex, Token, tokenize
52
58
  from sql2sqlx.model import (
@@ -1058,12 +1064,12 @@ class _Linker:
1058
1064
  leading = None
1059
1065
  spans = [(a, b) for a, b in self._comments(f) if prev_end <= a and b <= d.stmt_start]
1060
1066
  if spans:
1061
- leading = "\n".join(f.text[a:b] for a, b in spans)
1067
+ leading = "\n".join(normalize_sqlx_comment(f.text[a:b]) for a, b in spans)
1062
1068
  trailing = None
1063
1069
  if is_last_in_file:
1064
1070
  tail_spans = [(a, b) for a, b in self._comments(f) if a >= d.stmt_end]
1065
1071
  if tail_spans:
1066
- trailing = "\n".join(f.text[a:b] for a, b in tail_spans)
1072
+ trailing = "\n".join(normalize_sqlx_comment(f.text[a:b]) for a, b in tail_spans)
1067
1073
  body = None
1068
1074
  if d.action_type is not ActionType.DECLARATION:
1069
1075
  # Semantic rewrites own their complete source spans. In
@@ -1077,11 +1083,17 @@ class _Linker:
1077
1083
  semantic[0] < escape[1] and escape[0] < semantic[1] for semantic in d.edits
1078
1084
  )
1079
1085
  ]
1086
+ comment_edits = []
1087
+ for a, b in self._comments(f):
1088
+ if d.body_start <= a and b <= d.body_end:
1089
+ normalized = normalize_sqlx_comment(f.text[a:b])
1090
+ if normalized != f.text[a:b]:
1091
+ comment_edits.append((a, b, normalized))
1080
1092
  body = apply_edits_escaped(
1081
1093
  f.text,
1082
1094
  d.body_start,
1083
1095
  d.body_end,
1084
- d.edits + escape_edits,
1096
+ d.edits + escape_edits + comment_edits,
1085
1097
  )
1086
1098
  content = build_sqlx(config, body, annotation, leading, trailing)
1087
1099
 
sql2sqlx/emitter.py CHANGED
@@ -12,12 +12,15 @@ select-list aliases, ``${self()}`` substitutions) to slices of the
12
12
  **original** source text - tokens are never re-serialized, so the user's
13
13
  formatting, casing and inline comments survive after input decoding.
14
14
 
15
- Two SQLX-specific safety rules live here:
15
+ SQLX-specific safety rules live here:
16
16
 
17
17
  * Any literal ``${`` in a SQL string or quoted identifier is emitted through
18
18
  a constant JavaScript placeholder. Dataform does not provide a backslash
19
19
  escape for SQL placeholders, so ``\\${`` is insufficient. Replacement text
20
20
  inserted *by* sql2sqlx (``${ref(...)}``, ``${self()}``) stays active.
21
+ * GoogleSQL comments with SQLX-specific lexical hazards (``#`` comments and
22
+ exact ``---`` separator-looking comments) are normalized without changing
23
+ BigQuery semantics.
21
24
  * Operations bodies have exactly one trailing semicolon stripped:
22
25
  Dataform executes the body as a BigQuery script, and a trailing empty
23
26
  statement is at best noise.
@@ -29,7 +32,7 @@ import json
29
32
  import re
30
33
  from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
31
34
 
32
- from sql2sqlx.lexer import BACKTICK, PARAM, STRING, Token
35
+ from sql2sqlx.lexer import BACKTICK, COMMENT, PARAM, STRING, Token
33
36
  from sql2sqlx.model import TableName
34
37
 
35
38
  #: Canonical top-level config key order (unknown keys follow, insertion-ordered).
@@ -62,6 +65,12 @@ _BQ_ORDER = (
62
65
  )
63
66
 
64
67
  _JS_IDENT_RE = re.compile(r"^[A-Za-z_$][A-Za-z0-9_$]*$")
68
+ _SEPARATOR_COMMENT_RE = re.compile(r"^---[^\S\r\n]*$")
69
+ #: Control characters that JSON encoding turns into ``\n``/``\t``/``\uXXXX``
70
+ #: escapes. Dataform's placeholder-string lexer accepts only ``\"`` and ``\\``
71
+ #: escapes (``/"(?:\\["\\]|[^\n"\\])*"/``), so a whole-string placeholder that
72
+ #: embeds one of these characters cannot be lexed by Dataform.
73
+ _JS_STRING_UNSAFE_RE = re.compile(r"[\x00-\x1f]")
65
74
 
66
75
 
67
76
  def _json(value: str) -> str:
@@ -74,32 +83,61 @@ def sqlx_constant(value: str) -> str:
74
83
  return "${" + _json(value) + "}"
75
84
 
76
85
 
77
- def sqlx_escape_edits(tokens: Sequence[Token]) -> List[Tuple[int, int, str]]:
78
- """Build edits that preserve literal ``${`` through Dataform compile.
86
+ def normalize_sqlx_comment(comment: str) -> str:
87
+ """Return a SQLX-safe spelling of a single GoogleSQL comment token.
88
+
89
+ Dataform SQLX treats ``---`` at the start of a line as a statement
90
+ separator and does not recognize GoogleSQL ``#`` line comments. The
91
+ rewrites here preserve BigQuery semantics while preventing comment text
92
+ from being parsed as SQLX syntax.
93
+ """
94
+ if comment.startswith("#"):
95
+ # Rewriting ``#`` to ``--`` can itself produce a ``---`` separator
96
+ # line (e.g. the comment ``#-``), so fall through to the separator
97
+ # guard below instead of returning early.
98
+ comment = "--" + comment[1:]
99
+ if _SEPARATOR_COMMENT_RE.match(comment):
100
+ return "-- " + comment
101
+ return comment
79
102
 
80
- GoogleSQL only permits ``$`` in the relevant valid inputs inside string
81
- literals or quoted identifiers. String occurrences can be replaced by a
82
- constant placeholder for the two characters. A quoted identifier must be
83
- replaced as a whole because a SQLX placeholder nested between BigQuery
84
- backticks produces invalid generated JavaScript.
103
+
104
+ def sqlx_escape_edits(tokens: Sequence[Token]) -> List[Tuple[int, int, str]]:
105
+ """Build edits that keep literal SQL safe when parsed as SQLX.
106
+
107
+ Literal ``${`` in SQL strings, quoted identifiers, and quoted parameters
108
+ is emitted through a constant JavaScript placeholder because Dataform has
109
+ no SQL-level escape for placeholders. String literals are replaced as
110
+ whole tokens so safety does not depend on Dataform's per-quote lexer
111
+ states. A string that carries a control character (for example a
112
+ multi-line triple-quoted literal) cannot be embedded whole - JSON encoding
113
+ would introduce a ``\\n``/``\\t`` escape that Dataform's placeholder-string
114
+ lexer rejects - so each ``${`` in such a string is escaped in place, where
115
+ the surrounding SQL string state carries the raw characters unchanged.
116
+ GoogleSQL comments that are unsafe in SQLX are normalized as token edits.
85
117
  """
86
118
  edits: List[Tuple[int, int, str]] = []
87
119
  for token in tokens:
88
- if "${" not in token.text:
120
+ if token.kind == COMMENT:
121
+ normalized = normalize_sqlx_comment(token.text)
122
+ if normalized != token.text:
123
+ edits.append((token.start, token.end, normalized))
89
124
  continue
90
- if token.kind == BACKTICK or (token.kind == PARAM and token.text.startswith("@`")):
91
- edits.append((token.start, token.end, sqlx_constant(token.text)))
125
+ if "${" not in token.text:
92
126
  continue
93
- if token.kind != STRING:
127
+ if token.kind == STRING and _JS_STRING_UNSAFE_RE.search(token.text):
128
+ offset = 0
129
+ while True:
130
+ offset = token.text.find("${", offset)
131
+ if offset < 0:
132
+ break
133
+ start = token.start + offset
134
+ edits.append((start, start + 2, sqlx_constant("${")))
135
+ offset += 2
94
136
  continue
95
- offset = 0
96
- while True:
97
- offset = token.text.find("${", offset)
98
- if offset < 0:
99
- break
100
- start = token.start + offset
101
- edits.append((start, start + 2, sqlx_constant("${")))
102
- offset += 2
137
+ if token.kind in {BACKTICK, STRING} or (
138
+ token.kind == PARAM and token.text.startswith("@`")
139
+ ):
140
+ edits.append((token.start, token.end, sqlx_constant(token.text)))
103
141
  return edits
104
142
 
105
143
 
sql2sqlx/version.py CHANGED
@@ -12,4 +12,4 @@ without circular-import risk, and so packaging tools can read it
12
12
  statically.
13
13
  """
14
14
 
15
- __version__ = "0.1.0"
15
+ __version__ = "0.1.1"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql2sqlx
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Convert BigQuery SQL into Dataform SQLX with conservative, dependency-aware migration tooling.
5
5
  Author-email: Soumyadip Sarkar <soumyadip@soumyadipsarkar.com>
6
6
  License-Expression: Apache-2.0
@@ -8,7 +8,6 @@ Project-URL: Homepage, https://github.com/neuralsorcerer/sql2sqlx
8
8
  Project-URL: Documentation, https://neuralsorcerer.github.io/sql2sqlx/
9
9
  Project-URL: Source, https://github.com/neuralsorcerer/sql2sqlx
10
10
  Project-URL: Issues, https://github.com/neuralsorcerer/sql2sqlx/issues
11
- Project-URL: Changelog, https://github.com/neuralsorcerer/sql2sqlx/blob/main/CHANGELOG.md
12
11
  Keywords: analytics-engineering,bigquery,bigquery-sql,code-generation,data-engineering,dataform,dataform-sqlx,ddl,dependency-graph,dml,elt,etl,gcp,google-cloud,googlesql,migration,sql,sql-converter,sql-migration,sql-parser,sqlx,warehouse-migration
13
12
  Classifier: Development Status :: 4 - Beta
14
13
  Classifier: Intended Audience :: Developers
@@ -56,13 +55,13 @@ BigQuery SQL -> Dataform SQLX migration tooling.
56
55
 
57
56
  <div align="center">
58
57
 
59
- [![Current Release](https://img.shields.io/github/release/neuralsorcerer/sql2sqlx.svg)](https://github.com/neuralsorcerer/sql2sqlx/releases)
60
58
  [![PyPI](https://img.shields.io/pypi/v/sql2sqlx.svg?logo=pypi&logoColor=white)](https://pypi.org/project/sql2sqlx/)
61
59
  [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-fcbc2c.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
62
60
  [![Test Ubuntu](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml?query=branch%3Amain)
63
61
  [![Test Windows](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml?query=branch%3Amain)
64
62
  [![Test MacOS](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml?query=branch%3Amain)
65
63
  [![Lints](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml?query=branch%3Amain)
64
+ [![CodeQL](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/codeql.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/codeql.yml?query=branch%3Amain)
66
65
  [![Dataform Compile](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml?query=branch%3Amain)
67
66
  [![Build](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml?query=branch%3Amain)
68
67
  [![Documentation](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/docs.yml/badge.svg)](https://neuralsorcerer.github.io/sql2sqlx/)
@@ -1,8 +1,8 @@
1
1
  sql2sqlx/__init__.py,sha256=kT1TsG_BQaD9I0nMhQJOIIHbmQ_AHeyLFENt_g5wAFY,2101
2
2
  sql2sqlx/__main__.py,sha256=_w-oHujFK7NsgnIQmZ9RW22bvH9YZrbajk6YSS3csYE,363
3
3
  sql2sqlx/cli.py,sha256=jBZ6QeLs-to8vjhY_vbiTreE-svBrIcYsGGSXZjhuPQ,11850
4
- sql2sqlx/converter.py,sha256=M4m5phsGjkwS2rzrss8d2fYjogpLIPCrB4la0KVrIi0,50474
5
- sql2sqlx/emitter.py,sha256=vMJiIKyz4EJrkgVmylgO-I67BfHxAa2TLSroxZOvjnU,9517
4
+ sql2sqlx/converter.py,sha256=7QLLnnCD7hOQKtmx3a4iKxdagDepPcCuucrpZENpA1I,50903
5
+ sql2sqlx/emitter.py,sha256=wjykaralrmwSvuc1c-9jg0MrqFJXNUY7mfT3c6nFKQg,11563
6
6
  sql2sqlx/errors.py,sha256=XpNecYBPylxO_PevnGeJLjrO6n4zYnf9U3QeK9yTvXs,3231
7
7
  sql2sqlx/keywords.py,sha256=4ofwRHXp2gDn_z1dNoti2_kaOiHEcdNOgdzadW7imSQ,3559
8
8
  sql2sqlx/lexer.py,sha256=bVMDwcSgGN_k9eCQa-rziyQPqdx17OMkiPeEx9Ub26E,18837
@@ -11,10 +11,10 @@ sql2sqlx/parser.py,sha256=yKs5FujN_9OgbSoKqU3ioN0PKJXgzAT2TDS9BrQZdYs,77049
11
11
  sql2sqlx/py.typed,sha256=daEdpEyAJIa8b2VkCqSKcw8PaExcB6Qro80XNes_sHA,2
12
12
  sql2sqlx/refs.py,sha256=UZtom1SYw_QWYVwigniy882eBaL6M9orz_y9lPBRYVI,33916
13
13
  sql2sqlx/splitter.py,sha256=Ppo_gxaWmzZO6iYO_0Tc28W1bI4pbP1I3EF1juAw4Yg,14735
14
- sql2sqlx/version.py,sha256=1AtcjToFoNlIFnG4TyKDefXN69BkLcebIAzKKIS8leY,476
15
- sql2sqlx-0.1.0.dist-info/licenses/LICENSE,sha256=eK7fnHYjw-1st_pakYslBS41i1dEaHkvmtJzEVoubcE,11347
16
- sql2sqlx-0.1.0.dist-info/METADATA,sha256=YdE0HgPLhJylIAznnmfil5clKicuam_v9Pxw8-O4_20,28835
17
- sql2sqlx-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
- sql2sqlx-0.1.0.dist-info/entry_points.txt,sha256=n30wxr_Q4nH5h7tTEZO5ohXdJWqePYesmwpA32D4ygk,47
19
- sql2sqlx-0.1.0.dist-info/top_level.txt,sha256=nbDaiS9bRjW8S9naDOgtVvFBiFhiXgX9k8PcKCF_goI,9
20
- sql2sqlx-0.1.0.dist-info/RECORD,,
14
+ sql2sqlx/version.py,sha256=p3LTeKIkF_Myl1AFF5RPWlTN28MJvmQHvBnzwyT6YOo,476
15
+ sql2sqlx-0.1.1.dist-info/licenses/LICENSE,sha256=eK7fnHYjw-1st_pakYslBS41i1dEaHkvmtJzEVoubcE,11347
16
+ sql2sqlx-0.1.1.dist-info/METADATA,sha256=B86YhahfoD9Z3WgDejmjTUwytgc7GDGZzY1gdWWhM9M,28792
17
+ sql2sqlx-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
18
+ sql2sqlx-0.1.1.dist-info/entry_points.txt,sha256=n30wxr_Q4nH5h7tTEZO5ohXdJWqePYesmwpA32D4ygk,47
19
+ sql2sqlx-0.1.1.dist-info/top_level.txt,sha256=nbDaiS9bRjW8S9naDOgtVvFBiFhiXgX9k8PcKCF_goI,9
20
+ sql2sqlx-0.1.1.dist-info/RECORD,,