bqsqlparse 0.1.0__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.
bqsqlparse/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """bqsqlparse — a pure-Python BigQuery (GoogleSQL) parser with
2
+ column-level lineage extraction.
3
+
4
+ Quick start::
5
+
6
+ import bqsqlparse
7
+
8
+ ast = bqsqlparse.parse_one("SELECT a, SUM(b) AS total FROM ds.t GROUP BY a")
9
+ print(ast.sql())
10
+
11
+ lineage = bqsqlparse.extract_lineage(
12
+ "CREATE TABLE ds.out AS SELECT o.id, SUM(o.amount) t FROM ds.orders o GROUP BY 1"
13
+ )
14
+ print(lineage.to_json())
15
+ """
16
+
17
+ from . import nodes
18
+ from .errors import (
19
+ BQSQLError, LexError, LineageError, ParseError, UnsupportedStatementError,
20
+ )
21
+ from .functions import (
22
+ AGGREGATE_FUNCTIONS, ALL_FUNCTIONS, NAVIGATION_FUNCTIONS,
23
+ SCALAR_FUNCTIONS, is_aggregate, is_known_function, is_navigation,
24
+ )
25
+ from .lexer import Token, tokenize
26
+ from .lineage import (
27
+ ColumnLineage, ColumnUsage, LineageResult, SourceColumn, extract_lineage,
28
+ extract_script_lineage,
29
+ )
30
+ from .parser import Parser, parse, parse_one
31
+ from .unparse import to_sql
32
+
33
+ __version__ = "0.1.0"
34
+
35
+ __all__ = [
36
+ "parse",
37
+ "parse_one",
38
+ "Parser",
39
+ "tokenize",
40
+ "Token",
41
+ "to_sql",
42
+ "nodes",
43
+ "extract_lineage",
44
+ "extract_script_lineage",
45
+ "LineageResult",
46
+ "ColumnLineage",
47
+ "ColumnUsage",
48
+ "SourceColumn",
49
+ "BQSQLError",
50
+ "LexError",
51
+ "ParseError",
52
+ "UnsupportedStatementError",
53
+ "LineageError",
54
+ "AGGREGATE_FUNCTIONS",
55
+ "NAVIGATION_FUNCTIONS",
56
+ "SCALAR_FUNCTIONS",
57
+ "ALL_FUNCTIONS",
58
+ "is_aggregate",
59
+ "is_navigation",
60
+ "is_known_function",
61
+ "__version__",
62
+ ]
bqsqlparse/errors.py ADDED
@@ -0,0 +1,30 @@
1
+ """Exception types for bqsqlparse."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class BQSQLError(Exception):
7
+ """Base class for all bqsqlparse errors."""
8
+
9
+ def __init__(self, message: str, line: Optional[int] = None, col: Optional[int] = None):
10
+ self.message = message
11
+ self.line = line
12
+ self.col = col
13
+ loc = f" at line {line}, column {col}" if line is not None else ""
14
+ super().__init__(f"{message}{loc}")
15
+
16
+
17
+ class LexError(BQSQLError):
18
+ """Raised when the tokenizer encounters invalid input."""
19
+
20
+
21
+ class ParseError(BQSQLError):
22
+ """Raised when the parser encounters invalid or unexpected syntax."""
23
+
24
+
25
+ class UnsupportedStatementError(ParseError):
26
+ """Raised for statement types the parser does not support."""
27
+
28
+
29
+ class LineageError(BQSQLError):
30
+ """Raised when lineage extraction cannot proceed."""
@@ -0,0 +1,159 @@
1
+ """Registry of BigQuery (GoogleSQL) built-in functions.
2
+
3
+ Used by the lineage engine to classify transformations (aggregation vs.
4
+ window vs. scalar expression) and exposed for consumers who want to check
5
+ whether a function name is a known BigQuery built-in.
6
+ """
7
+
8
+ from typing import Dict, FrozenSet
9
+
10
+ AGGREGATE_FUNCTIONS: FrozenSet[str] = frozenset({
11
+ "ANY_VALUE", "ARRAY_AGG", "ARRAY_CONCAT_AGG", "AVG", "BIT_AND", "BIT_OR",
12
+ "BIT_XOR", "COUNT", "COUNTIF", "GROUPING", "LOGICAL_AND", "LOGICAL_OR",
13
+ "MAX", "MAX_BY", "MIN", "MIN_BY", "STRING_AGG", "SUM", "CORR",
14
+ "COVAR_POP", "COVAR_SAMP", "STDDEV", "STDDEV_POP", "STDDEV_SAMP",
15
+ "VAR_POP", "VAR_SAMP", "VARIANCE",
16
+ "APPROX_COUNT_DISTINCT", "APPROX_QUANTILES", "APPROX_TOP_COUNT",
17
+ "APPROX_TOP_SUM",
18
+ "HLL_COUNT.INIT", "HLL_COUNT.MERGE", "HLL_COUNT.MERGE_PARTIAL",
19
+ "HLL_COUNT.EXTRACT",
20
+ "KLL_QUANTILES.INIT_INT64", "KLL_QUANTILES.INIT_FLOAT64",
21
+ "KLL_QUANTILES.MERGE_PARTIAL", "KLL_QUANTILES.MERGE",
22
+ "KLL_QUANTILES.MERGE_POINT", "KLL_QUANTILES.EXTRACT",
23
+ "KLL_QUANTILES.EXTRACT_POINT",
24
+ "PERCENTILE_CONT", "PERCENTILE_DISC", "ST_EXTENT", "ST_UNION_AGG",
25
+ "ST_CENTROID_AGG",
26
+ })
27
+
28
+ NAVIGATION_FUNCTIONS: FrozenSet[str] = frozenset({
29
+ "RANK", "DENSE_RANK", "ROW_NUMBER", "NTILE", "CUME_DIST", "PERCENT_RANK",
30
+ "LAG", "LEAD", "FIRST_VALUE", "LAST_VALUE", "NTH_VALUE",
31
+ })
32
+
33
+ SCALAR_FUNCTIONS: Dict[str, FrozenSet[str]] = {
34
+ "string": frozenset({
35
+ "ASCII", "BYTE_LENGTH", "CHAR_LENGTH", "CHARACTER_LENGTH", "CHR",
36
+ "CODE_POINTS_TO_BYTES", "CODE_POINTS_TO_STRING", "COLLATE", "CONCAT",
37
+ "CONTAINS_SUBSTR", "EDIT_DISTANCE", "ENDS_WITH", "FORMAT",
38
+ "FROM_BASE32", "FROM_BASE64", "FROM_HEX", "INITCAP", "INSTR", "LEFT",
39
+ "LENGTH", "LOWER", "LPAD", "LTRIM", "NORMALIZE",
40
+ "NORMALIZE_AND_CASEFOLD", "OCTET_LENGTH", "REGEXP_CONTAINS",
41
+ "REGEXP_EXTRACT", "REGEXP_EXTRACT_ALL", "REGEXP_INSTR",
42
+ "REGEXP_REPLACE", "REGEXP_SUBSTR", "REPEAT", "REPLACE", "REVERSE",
43
+ "RIGHT", "RPAD", "RTRIM", "SAFE_CONVERT_BYTES_TO_STRING", "SOUNDEX",
44
+ "SPLIT", "STARTS_WITH", "STRPOS", "SUBSTR", "SUBSTRING", "TO_BASE32",
45
+ "TO_BASE64", "TO_CODE_POINTS", "TO_HEX", "TRANSLATE", "TRIM",
46
+ "UNICODE", "UPPER",
47
+ }),
48
+ "math": frozenset({
49
+ "ABS", "ACOS", "ACOSH", "ASIN", "ASINH", "ATAN", "ATAN2", "ATANH",
50
+ "CBRT", "CEIL", "CEILING", "COS", "COSH", "COSINE_DISTANCE", "COT",
51
+ "COTH", "CSC", "CSCH", "DIV", "EUCLIDEAN_DISTANCE", "EXP", "FLOOR",
52
+ "GREATEST", "IEEE_DIVIDE", "IS_INF", "IS_NAN", "LEAST", "LN", "LOG",
53
+ "LOG10", "MOD", "POW", "POWER", "RAND", "RANGE_BUCKET", "ROUND",
54
+ "SAFE_ADD", "SAFE_DIVIDE", "SAFE_MULTIPLY", "SAFE_NEGATE",
55
+ "SAFE_SUBTRACT", "SEC", "SECH", "SIGN", "SIN", "SINH", "SQRT", "TAN",
56
+ "TANH", "TRUNC",
57
+ }),
58
+ "date_time": frozenset({
59
+ "CURRENT_DATE", "CURRENT_DATETIME", "CURRENT_TIME",
60
+ "CURRENT_TIMESTAMP", "DATE", "DATE_ADD", "DATE_BUCKET", "DATE_DIFF",
61
+ "DATE_FROM_UNIX_DATE", "DATE_SUB", "DATE_TRUNC", "DATETIME",
62
+ "DATETIME_ADD", "DATETIME_BUCKET", "DATETIME_DIFF", "DATETIME_SUB",
63
+ "DATETIME_TRUNC", "FORMAT_DATE", "FORMAT_DATETIME", "FORMAT_TIME",
64
+ "FORMAT_TIMESTAMP", "GENERATE_DATE_ARRAY", "GENERATE_TIMESTAMP_ARRAY",
65
+ "JUSTIFY_DAYS", "JUSTIFY_HOURS", "JUSTIFY_INTERVAL", "LAST_DAY",
66
+ "MAKE_INTERVAL", "PARSE_DATE", "PARSE_DATETIME", "PARSE_TIME",
67
+ "PARSE_TIMESTAMP", "TIME", "TIME_ADD", "TIME_DIFF", "TIME_SUB",
68
+ "TIME_TRUNC", "TIMESTAMP", "TIMESTAMP_ADD", "TIMESTAMP_BUCKET",
69
+ "TIMESTAMP_DIFF", "TIMESTAMP_MICROS", "TIMESTAMP_MILLIS",
70
+ "TIMESTAMP_SECONDS", "TIMESTAMP_SUB", "TIMESTAMP_TRUNC",
71
+ "UNIX_DATE", "UNIX_MICROS", "UNIX_MILLIS", "UNIX_SECONDS", "EXTRACT",
72
+ }),
73
+ "array": frozenset({
74
+ "ARRAY", "ARRAY_CONCAT", "ARRAY_FIRST", "ARRAY_LAST", "ARRAY_LENGTH",
75
+ "ARRAY_REVERSE", "ARRAY_SLICE", "ARRAY_TO_STRING", "GENERATE_ARRAY",
76
+ "ARRAY_ZIP", "FLATTEN",
77
+ }),
78
+ "json": frozenset({
79
+ "BOOL", "FLOAT64", "INT64", "JSON_ARRAY", "JSON_ARRAY_APPEND",
80
+ "JSON_ARRAY_INSERT", "JSON_EXTRACT", "JSON_EXTRACT_ARRAY",
81
+ "JSON_EXTRACT_SCALAR", "JSON_EXTRACT_STRING_ARRAY", "JSON_KEYS",
82
+ "JSON_OBJECT", "JSON_QUERY", "JSON_QUERY_ARRAY", "JSON_REMOVE",
83
+ "JSON_SET", "JSON_STRIP_NULLS", "JSON_TYPE", "JSON_VALUE",
84
+ "JSON_VALUE_ARRAY", "LAX_BOOL", "LAX_FLOAT64", "LAX_INT64",
85
+ "LAX_STRING", "PARSE_JSON", "STRING", "TO_JSON", "TO_JSON_STRING",
86
+ }),
87
+ "conditional": frozenset({
88
+ "COALESCE", "IF", "IFNULL", "NULLIF", "NULLIFZERO", "ZEROIFNULL",
89
+ }),
90
+ "conversion": frozenset({
91
+ "CAST", "SAFE_CAST", "PARSE_BIGNUMERIC", "PARSE_NUMERIC",
92
+ }),
93
+ "hash_crypto": frozenset({
94
+ "FARM_FINGERPRINT", "MD5", "SHA1", "SHA256", "SHA512",
95
+ "KEYS.NEW_KEYSET", "KEYS.ADD_KEY_FROM_RAW_BYTES",
96
+ "AEAD.DECRYPT_BYTES", "AEAD.DECRYPT_STRING", "AEAD.ENCRYPT",
97
+ "DETERMINISTIC_DECRYPT_BYTES", "DETERMINISTIC_DECRYPT_STRING",
98
+ "DETERMINISTIC_ENCRYPT", "KEYS.KEYSET_CHAIN", "KEYS.KEYSET_FROM_JSON",
99
+ "KEYS.KEYSET_TO_JSON", "KEYS.ROTATE_KEYSET", "KEYS.KEYSET_LENGTH",
100
+ }),
101
+ "net": frozenset({
102
+ "NET.HOST", "NET.IP_FROM_STRING", "NET.IP_NET_MASK", "NET.IP_TO_STRING",
103
+ "NET.IP_TRUNC", "NET.IPV4_FROM_INT64", "NET.IPV4_TO_INT64",
104
+ "NET.PUBLIC_SUFFIX", "NET.REG_DOMAIN", "NET.SAFE_IP_FROM_STRING",
105
+ }),
106
+ "geography": frozenset({
107
+ "ST_AREA", "ST_ASBINARY", "ST_ASGEOJSON", "ST_ASTEXT", "ST_ANGLE",
108
+ "ST_AZIMUTH", "ST_BOUNDARY", "ST_BOUNDINGBOX", "ST_BUFFER",
109
+ "ST_BUFFERWITHTOLERANCE", "ST_CENTROID", "ST_CLOSESTPOINT",
110
+ "ST_CLUSTERDBSCAN", "ST_CONTAINS", "ST_CONVEXHULL", "ST_COVEREDBY",
111
+ "ST_COVERS", "ST_DIFFERENCE", "ST_DIMENSION", "ST_DISJOINT",
112
+ "ST_DISTANCE", "ST_DUMP", "ST_DWITHIN", "ST_ENDPOINT", "ST_EQUALS",
113
+ "ST_EXTERIORRING", "ST_GEOGFROM", "ST_GEOGFROMGEOJSON",
114
+ "ST_GEOGFROMTEXT", "ST_GEOGFROMWKB", "ST_GEOGPOINT",
115
+ "ST_GEOGPOINTFROMGEOHASH", "ST_GEOHASH", "ST_GEOMETRYTYPE",
116
+ "ST_HAUSDORFFDISTANCE", "ST_INTERIORRINGS", "ST_INTERSECTION",
117
+ "ST_INTERSECTS", "ST_INTERSECTSBOX", "ST_ISCLOSED", "ST_ISCOLLECTION",
118
+ "ST_ISEMPTY", "ST_ISRING", "ST_LENGTH", "ST_LINEINTERPOLATEPOINT",
119
+ "ST_LINELOCATEPOINT", "ST_LINESUBSTRING", "ST_MAKELINE",
120
+ "ST_MAKEPOLYGON", "ST_MAKEPOLYGONORIENTED", "ST_MAXDISTANCE",
121
+ "ST_NPOINTS", "ST_NUMGEOMETRIES", "ST_NUMPOINTS", "ST_PERIMETER",
122
+ "ST_POINTN", "ST_SIMPLIFY", "ST_SNAPTOGRID", "ST_STARTPOINT",
123
+ "ST_TOUCHES", "ST_UNION", "ST_WITHIN", "ST_X", "ST_Y",
124
+ }),
125
+ "range": frozenset({
126
+ "RANGE", "RANGE_CONTAINS", "RANGE_END", "RANGE_INTERSECT",
127
+ "RANGE_OVERLAPS", "RANGE_SESSIONIZE", "RANGE_START",
128
+ }),
129
+ "utility": frozenset({
130
+ "GENERATE_UUID", "SESSION_USER", "ERROR", "BIT_COUNT",
131
+ "TYPEOF", "VECTOR_SEARCH",
132
+ }),
133
+ "search": frozenset({
134
+ "SEARCH", "TEXT_ANALYZE",
135
+ }),
136
+ "interval": frozenset({
137
+ "JUSTIFY_DAYS", "JUSTIFY_HOURS", "JUSTIFY_INTERVAL", "MAKE_INTERVAL",
138
+ }),
139
+ }
140
+
141
+ _all = set(AGGREGATE_FUNCTIONS) | set(NAVIGATION_FUNCTIONS)
142
+ for _names in SCALAR_FUNCTIONS.values():
143
+ _all |= set(_names)
144
+ ALL_FUNCTIONS: FrozenSet[str] = frozenset(_all)
145
+
146
+
147
+ def is_aggregate(name: str) -> bool:
148
+ """True if *name* (case-insensitive, dotted allowed) is an aggregate."""
149
+ return name.upper() in AGGREGATE_FUNCTIONS
150
+
151
+
152
+ def is_navigation(name: str) -> bool:
153
+ """True if *name* is a numbering/navigation (window-only) function."""
154
+ return name.upper() in NAVIGATION_FUNCTIONS
155
+
156
+
157
+ def is_known_function(name: str) -> bool:
158
+ """True if *name* is a known BigQuery built-in function."""
159
+ return name.upper() in ALL_FUNCTIONS
bqsqlparse/lexer.py ADDED
@@ -0,0 +1,242 @@
1
+ """Tokenizer for the BigQuery (GoogleSQL) dialect.
2
+
3
+ Handles:
4
+ - Reserved keywords vs. contextual identifiers
5
+ - Backtick-quoted identifiers (including dotted paths inside backticks)
6
+ - String / bytes literals with r/b prefixes, single, double and triple quotes
7
+ - Escape sequences (\\n, \\xHH, \\uXXXX, \\UXXXXXXXX, octal, ...)
8
+ - Numeric literals (int, float, exponent, hex)
9
+ - Named (@param), system (@@var) and positional (?) parameters
10
+ - Comments: ``--``, ``#`` and ``/* ... */``
11
+ - All GoogleSQL operators including ``||``, ``<<``, ``>>``, ``=>``
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from bisect import bisect_right
18
+ from dataclasses import dataclass, field
19
+ from typing import List
20
+
21
+ from .errors import LexError
22
+
23
+ # Official GoogleSQL reserved keywords.
24
+ RESERVED = frozenset(
25
+ """
26
+ ALL AND ANY ARRAY AS ASC ASSERT_ROWS_MODIFIED AT BETWEEN BY CASE CAST
27
+ COLLATE CONTAINS CREATE CROSS CUBE CURRENT DEFAULT DEFINE DESC DISTINCT
28
+ ELSE END ENUM ESCAPE EXCEPT EXCLUDE EXISTS EXTRACT FALSE FETCH FOLLOWING
29
+ FOR FROM FULL GROUP GROUPING GROUPS HASH HAVING IF IGNORE IN INNER
30
+ INTERSECT INTERVAL INTO IS JOIN LATERAL LEFT LIKE LIMIT LOOKUP MERGE
31
+ NATURAL NEW NO NOT NULL NULLS OF ON OR ORDER OUTER OVER PARTITION
32
+ PRECEDING PROTO QUALIFY RANGE RECURSIVE RESPECT RIGHT ROLLUP ROWS SELECT
33
+ SET SOME STRUCT TABLESAMPLE THEN TO TREAT TRUE UNBOUNDED UNION UNNEST
34
+ USING WHEN WHERE WINDOW WITH WITHIN
35
+ """.split()
36
+ )
37
+
38
+ # Token types
39
+ KEYWORD = "KEYWORD"
40
+ IDENT = "IDENT"
41
+ QIDENT = "QIDENT" # backtick-quoted identifier
42
+ STRING = "STRING"
43
+ BYTES = "BYTES"
44
+ NUMBER = "NUMBER"
45
+ PARAM = "PARAM"
46
+ OP = "OP"
47
+ EOF = "EOF"
48
+
49
+ _TWO_CHAR_OPS = ("<<", ">>", "<=", ">=", "!=", "<>", "||", "=>")
50
+ _SINGLE_CHAR_OPS = set("+-*/=<>()[]{},.;&|^~:")
51
+
52
+ _IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
53
+ _NUMBER_RE = re.compile(r"0[xX][0-9A-Fa-f]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
54
+
55
+ _ESCAPES = {
56
+ "a": "\a", "b": "\b", "f": "\f", "n": "\n", "r": "\r", "t": "\t",
57
+ "v": "\v", "\\": "\\", "'": "'", '"': '"', "`": "`", "?": "?", "/": "/",
58
+ }
59
+
60
+
61
+ @dataclass
62
+ class Token:
63
+ type: str
64
+ value: str
65
+ pos: int = 0
66
+ line: int = 0
67
+ col: int = 0
68
+ flags: str = field(default="", compare=False)
69
+
70
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
71
+ return f"Token({self.type}, {self.value!r}, L{self.line}:C{self.col})"
72
+
73
+
74
+ class _Lexer:
75
+ def __init__(self, sql: str):
76
+ self.sql = sql
77
+ self.n = len(sql)
78
+ self.i = 0
79
+ self.tokens: List[Token] = []
80
+ self.line_starts = [0]
81
+ for idx, ch in enumerate(sql):
82
+ if ch == "\n":
83
+ self.line_starts.append(idx + 1)
84
+
85
+ def _linecol(self, pos: int):
86
+ li = bisect_right(self.line_starts, pos) - 1
87
+ return li + 1, pos - self.line_starts[li] + 1
88
+
89
+ def _err(self, msg: str, pos: int):
90
+ line, col = self._linecol(pos)
91
+ raise LexError(msg, line, col)
92
+
93
+ def _add(self, type_: str, value: str, pos: int, flags: str = ""):
94
+ line, col = self._linecol(pos)
95
+ self.tokens.append(Token(type_, value, pos, line, col, flags))
96
+
97
+ def run(self) -> List[Token]:
98
+ sql, n = self.sql, self.n
99
+ while self.i < n:
100
+ ch = sql[self.i]
101
+ if ch in " \t\r\n":
102
+ self.i += 1
103
+ elif ch == "-" and self.i + 1 < n and sql[self.i + 1] == "-":
104
+ self._skip_line()
105
+ elif ch == "#":
106
+ self._skip_line()
107
+ elif ch == "/" and self.i + 1 < n and sql[self.i + 1] == "*":
108
+ end = sql.find("*/", self.i + 2)
109
+ if end == -1:
110
+ self._err("Unterminated block comment", self.i)
111
+ self.i = end + 2
112
+ elif ch == "`":
113
+ self._read_qident()
114
+ elif ch in "'\"":
115
+ self._read_string(self.i, "")
116
+ elif ch.isalpha() or ch == "_":
117
+ m = _IDENT_RE.match(sql, self.i)
118
+ word = m.group(0)
119
+ nxt = self.i + len(word)
120
+ if word.lower() in ("r", "b", "rb", "br") and nxt < n and sql[nxt] in "'\"":
121
+ start = self.i
122
+ self.i = nxt
123
+ self._read_string(start, word.lower())
124
+ else:
125
+ upper = word.upper()
126
+ if upper in RESERVED:
127
+ self._add(KEYWORD, upper, self.i)
128
+ else:
129
+ self._add(IDENT, word, self.i)
130
+ self.i = nxt
131
+ elif ch.isdigit() or (ch == "." and self.i + 1 < n and sql[self.i + 1].isdigit()):
132
+ m = _NUMBER_RE.match(sql, self.i)
133
+ self._add(NUMBER, m.group(0), self.i)
134
+ self.i = m.end()
135
+ elif ch == "@":
136
+ start = self.i
137
+ self.i += 1
138
+ prefix = "@"
139
+ if self.i < n and sql[self.i] == "@":
140
+ prefix = "@@"
141
+ self.i += 1
142
+ m = _IDENT_RE.match(sql, self.i)
143
+ if not m:
144
+ self._err("Invalid query parameter", start)
145
+ self._add(PARAM, prefix + m.group(0), start)
146
+ self.i = m.end()
147
+ elif ch == "?":
148
+ self._add(PARAM, "?", self.i)
149
+ self.i += 1
150
+ else:
151
+ two = sql[self.i : self.i + 2]
152
+ if two in _TWO_CHAR_OPS:
153
+ self._add(OP, two, self.i)
154
+ self.i += 2
155
+ elif ch in _SINGLE_CHAR_OPS:
156
+ self._add(OP, ch, self.i)
157
+ self.i += 1
158
+ else:
159
+ self._err(f"Unexpected character {ch!r}", self.i)
160
+ self._add(EOF, "", n)
161
+ return self.tokens
162
+
163
+ def _skip_line(self):
164
+ end = self.sql.find("\n", self.i)
165
+ self.i = self.n if end == -1 else end + 1
166
+
167
+ def _read_qident(self):
168
+ start = self.i
169
+ self.i += 1
170
+ chars = []
171
+ while self.i < self.n:
172
+ ch = self.sql[self.i]
173
+ if ch == "\\" and self.i + 1 < self.n:
174
+ chars.append(self.sql[self.i + 1])
175
+ self.i += 2
176
+ elif ch == "`":
177
+ self.i += 1
178
+ self._add(QIDENT, "".join(chars), start)
179
+ return
180
+ else:
181
+ chars.append(ch)
182
+ self.i += 1
183
+ self._err("Unterminated quoted identifier", start)
184
+
185
+ def _read_string(self, start: int, flags: str):
186
+ sql, n = self.sql, self.n
187
+ quote = sql[self.i]
188
+ raw = "r" in flags
189
+ is_bytes = "b" in flags
190
+ triple = sql[self.i : self.i + 3] == quote * 3
191
+ self.i += 3 if triple else 1
192
+ terminator = quote * 3 if triple else quote
193
+ chars = []
194
+ while self.i < n:
195
+ if sql.startswith(terminator, self.i):
196
+ self.i += len(terminator)
197
+ self._add(BYTES if is_bytes else STRING, "".join(chars), start, flags)
198
+ return
199
+ ch = sql[self.i]
200
+ if not triple and ch == "\n":
201
+ self._err("Unterminated string literal", start)
202
+ if ch == "\\" and self.i + 1 < n:
203
+ if raw:
204
+ chars.append(ch)
205
+ chars.append(sql[self.i + 1])
206
+ self.i += 2
207
+ else:
208
+ self.i += 1
209
+ self._read_escape(chars)
210
+ else:
211
+ chars.append(ch)
212
+ self.i += 1
213
+ self._err("Unterminated string literal", start)
214
+
215
+ def _read_escape(self, chars):
216
+ sql = self.sql
217
+ e = sql[self.i]
218
+ if e in _ESCAPES:
219
+ chars.append(_ESCAPES[e])
220
+ self.i += 1
221
+ elif e == "x" or e == "X":
222
+ hexs = sql[self.i + 1 : self.i + 3]
223
+ chars.append(chr(int(hexs, 16)))
224
+ self.i += 3
225
+ elif e == "u":
226
+ chars.append(chr(int(sql[self.i + 1 : self.i + 5], 16)))
227
+ self.i += 5
228
+ elif e == "U":
229
+ chars.append(chr(int(sql[self.i + 1 : self.i + 9], 16)))
230
+ self.i += 9
231
+ elif e.isdigit():
232
+ m = re.match(r"[0-7]{1,3}", sql[self.i :])
233
+ chars.append(chr(int(m.group(0), 8)))
234
+ self.i += len(m.group(0))
235
+ else:
236
+ chars.append(e)
237
+ self.i += 1
238
+
239
+
240
+ def tokenize(sql: str) -> List[Token]:
241
+ """Tokenize a GoogleSQL string into a list of :class:`Token`."""
242
+ return _Lexer(sql).run()