sqlh 0.2.8__tar.gz → 0.3.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.
@@ -1,10 +1,8 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sqlh
3
- Version: 0.2.8
3
+ Version: 0.3.1
4
4
  Summary: A lightweight SQL lineage analysis library for tracking table dependencies in data pipelines
5
5
  Keywords: sql,lineage,data-pipeline,dag,dependency,database,etl,data-engineering
6
- Maintainer: Perry DU
7
- Maintainer-email: Perry DU <duneite@gmail.com>
8
6
  Requires-Python: >=3.10
9
7
  Description-Content-Type: text/markdown
10
8
 
@@ -1,9 +1,7 @@
1
1
  [project]
2
2
  name = "sqlh"
3
- version = "0.2.8"
4
- maintainers = [
5
- {name = "Perry DU", email = "duneite@gmail.com"}
6
- ]
3
+ version = "0.3.1"
4
+
7
5
  description = "A lightweight SQL lineage analysis library for tracking table dependencies in data pipelines"
8
6
  readme = "README.md"
9
7
  requires-python = ">=3.10"
@@ -1,5 +1,5 @@
1
1
  from .core.graph import DagGraph
2
- from .core.helper import split_sql, trim_comment
2
+ from .core.helper import split_sql, trim_comment, split_sql_v2, split_sql_v3
3
3
  from .utils import (
4
4
  get_all_leaf_tables,
5
5
  get_all_root_tables,
@@ -14,10 +14,12 @@ from .utils import (
14
14
  table_count
15
15
  )
16
16
 
17
- __version__ = "0.2.8"
17
+ __version__ = "0.3.1"
18
18
 
19
19
  __all__ = [
20
20
  "split_sql",
21
+ "split_sql_v2",
22
+ "split_sql_v3",
21
23
  "trim_comment",
22
24
  "DagGraph",
23
25
  "read_sql_from_directory",
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from sqlh import __version__
7
7
 
8
+ from .core.helper import split_sql, split_sql_v2, split_sql_v3
8
9
  from .utils import (
9
10
  get_all_dag,
10
11
  get_all_leaf_tables,
@@ -46,7 +47,7 @@ def _create_parent_parser():
46
47
 
47
48
 
48
49
  def arg_parse():
49
- parser = argparse.ArgumentParser(usage="%(prog)s [OPTIONS] <COMMAND>", description="mini-sqllineage")
50
+ parser = argparse.ArgumentParser(usage="%(prog)s <COMMAND> [OPTIONS] ")
50
51
  parser.add_argument("-v", "--version", action="version", version=__version__)
51
52
 
52
53
  # 获取共享参数的父解析器
@@ -103,6 +104,24 @@ def arg_parse():
103
104
  )
104
105
  table_count_parser.add_argument("-t", "--table", help="table name to search")
105
106
  table_count_parser.add_argument("-h", "--help", action="help", default=argparse.SUPPRESS, help="show this help message")
107
+
108
+ # split 子命令
109
+ split_parser = subparsers.add_parser(
110
+ "split",
111
+ parents=[parent_parser],
112
+ help="split sql file",
113
+ add_help=False,
114
+ )
115
+ split_parser.add_argument(
116
+ "-sv",
117
+ "--split-version",
118
+ type=int,
119
+ choices=[1, 2, 3],
120
+ default=1,
121
+ help="split version",
122
+ )
123
+ split_parser.add_argument("-h", "--help", action="help", default=argparse.SUPPRESS, help="show this help message")
124
+
106
125
  return parser.parse_args()
107
126
 
108
127
 
@@ -181,3 +200,19 @@ def main():
181
200
  else:
182
201
  print(f"Error: Not Supported output format: {args.output_format}")
183
202
  sys.exit(1)
203
+
204
+ elif args.command == "split":
205
+ import time
206
+
207
+ t = time.perf_counter()
208
+ if args.split_version == 1:
209
+ split_sql(sql_stmt_str)
210
+ elif args.split_version == 2:
211
+ split_sql_v2(sql_stmt_str)
212
+ elif args.split_version == 3:
213
+ split_sql_v3(sql_stmt_str)
214
+ else:
215
+ print(f"Error: Not Supported split version: {args.split_version}")
216
+ sys.exit(1)
217
+ t_parse = time.perf_counter() - t
218
+ print(f"parse time: {t_parse * 1000:.3f} ms")
@@ -9,13 +9,28 @@ This module provides SQL parsing functionality using token-based analysis:
9
9
 
10
10
  The parser uses keyword-based tokenization rather than full AST parsing,
11
11
  making it lightweight and fast for simple table/field extraction tasks.
12
+
13
+ === 性能统计 ===
14
+ 文件数量 : 489 个
15
+ 总字符数 : 2264584 字节 (2.16 MB)
16
+ SQL 语句数 : 1174 条
17
+ 读取耗时 : 15.196 ms
18
+ 解析耗时 : 129.142 ms
19
+ 总耗时 : 535.163 ms
20
+ 解析速度v1 : 16.7 MB/s
21
+ 解析速度v2 : 14.9 MB/s
22
+ 解析速度v3 : 8.8 MB/s
23
+
12
24
  """
13
25
 
26
+ import re
27
+
14
28
  from .keywords import KeyWords
15
29
 
16
30
 
17
31
  class ParseException(Exception):
18
32
  """Exception raised when SQL parsing fails."""
33
+
19
34
  pass
20
35
 
21
36
 
@@ -121,6 +136,209 @@ def split_sql(sql: str) -> list[str]:
121
136
  return result
122
137
 
123
138
 
139
+ def split_sql_v2(sql: str) -> list[str]:
140
+ """
141
+ Split multi-statement SQL by semicolons, handling comments and quotes.
142
+ Optimized for performance and readability.
143
+ """
144
+ if not sql.strip():
145
+ return []
146
+
147
+ result = []
148
+ current_stmt = []
149
+
150
+ i = 0
151
+ n = len(sql)
152
+
153
+ while i < n:
154
+ char = sql[i]
155
+
156
+ # 1. 处理单行注释
157
+ if char == "-" and i + 1 < n and sql[i + 1] == "-":
158
+ # 跳过直到换行符
159
+ while i < n and sql[i] not in ("\n", "\r"):
160
+ i += 1
161
+ if i < n: # 包含换行符
162
+ current_stmt.append(sql[i])
163
+ i += 1
164
+ continue
165
+
166
+ # 2. 处理块注释 /* ... */
167
+ if char == "/" and i + 1 < n and sql[i + 1] == "*":
168
+ current_stmt.append("/*")
169
+ i += 2
170
+ depth = 1
171
+ while i < n and depth > 0:
172
+ if sql[i] == "/" and i + 1 < n and sql[i + 1] == "*":
173
+ depth += 1
174
+ current_stmt.append("/*")
175
+ i += 2
176
+ elif sql[i] == "*" and i + 1 < n and sql[i + 1] == "/":
177
+ depth -= 1
178
+ if depth == 0:
179
+ current_stmt.append("*/")
180
+ i += 2
181
+ break
182
+ current_stmt.append("*/")
183
+ i += 2
184
+ else:
185
+ current_stmt.append(sql[i])
186
+ i += 1
187
+ continue
188
+
189
+ # 3. 处理引号
190
+ if char == "'" or char == '"':
191
+ quote_char = char
192
+ current_stmt.append(char)
193
+ i += 1
194
+ while i < n:
195
+ c = sql[i]
196
+ current_stmt.append(c)
197
+ i += 1
198
+ if c == quote_char:
199
+ # 处理转义引号 '' 或 "" (取决于方言,这里简单处理为成对出现)
200
+ if i < n and sql[i] == quote_char:
201
+ current_stmt.append(sql[i])
202
+ i += 1
203
+ else:
204
+ break
205
+ continue
206
+
207
+ # 4. 处理分号
208
+ if char == ";":
209
+ stmt_str = "".join(current_stmt)
210
+ if stmt_str.strip():
211
+ result.append(stmt_str)
212
+ current_stmt = []
213
+ i += 1
214
+ continue
215
+
216
+ # 5. 普通字符
217
+ current_stmt.append(char)
218
+ i += 1
219
+
220
+ # 处理最后剩余的语句
221
+ if current_stmt:
222
+ stmt_str = "".join(current_stmt)
223
+ if stmt_str.strip():
224
+ result.append(stmt_str)
225
+
226
+ return result
227
+
228
+
229
+ def _try_parse_dollar_tag(text: str, pos: int) -> tuple[str, int] | None:
230
+ """
231
+ 从 text[pos] 开始尝试匹配美元引号开标签 $tag$。
232
+ 返回 (tag, end_pos) 或 None。
233
+ """
234
+ # 美元引号标签的合法字符:字母开头,字母/数字/下划线
235
+ _DOLLAR_TAG_RE = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)?\$")
236
+
237
+ m = _DOLLAR_TAG_RE.match(text, pos)
238
+ if m:
239
+ tag = m.group(1) or ""
240
+ return tag, m.end()
241
+ return None
242
+
243
+
244
+ def split_sql_v3(text: str) -> list[str]:
245
+ """
246
+ 词法解析,完全对应 Rust lib.rs 中的 split_sql()。
247
+ 状态:NORMAL / SINGLE_QUOTE / DOUBLE_QUOTE /
248
+ LINE_COMMENT / BLOCK_COMMENT / DOLLAR_QUOTE
249
+ """
250
+ NORMAL = 0
251
+ SINGLE_QUOTE = 1
252
+ DOUBLE_QUOTE = 2
253
+ LINE_COMMENT = 3
254
+ BLOCK_COMMENT = 4
255
+ DOLLAR_QUOTE = 5
256
+
257
+ state = NORMAL
258
+ block_depth = 0 # 嵌套块注释深度
259
+ dollar_tag = "" # 当前美元引号的标签
260
+ dollar_close = "" # 对应的闭标签字符串(缓存)
261
+
262
+ result = []
263
+ stmt_start = 0
264
+ i = 0
265
+ n = len(text)
266
+
267
+ while i < n:
268
+ c = text[i]
269
+ nxt = text[i + 1] if i + 1 < n else "\0"
270
+
271
+ if state == NORMAL:
272
+ if c == "'":
273
+ state = SINGLE_QUOTE
274
+ elif c == '"':
275
+ state = DOUBLE_QUOTE
276
+ elif c == "-" and nxt == "-":
277
+ state = LINE_COMMENT
278
+ i += 1
279
+ elif c == "/" and nxt == "*":
280
+ state = BLOCK_COMMENT
281
+ block_depth = 1
282
+ i += 1
283
+ elif c == "$":
284
+ parsed = _try_parse_dollar_tag(text, i)
285
+ if parsed is not None:
286
+ dollar_tag, end = parsed
287
+ dollar_close = f"${dollar_tag}$"
288
+ state = DOLLAR_QUOTE
289
+ i = end
290
+ continue # i 已跳过整个开标签,不再 +1
291
+ elif c == ";":
292
+ stmt = text[stmt_start:i].strip()
293
+ if stmt:
294
+ result.append(stmt)
295
+ stmt_start = i + 1
296
+
297
+ elif state == SINGLE_QUOTE:
298
+ if c == "'" and nxt == "'":
299
+ i += 1
300
+ elif c == "\\" and nxt == "'":
301
+ i += 1
302
+ elif c == "'":
303
+ state = NORMAL
304
+
305
+ elif state == DOUBLE_QUOTE:
306
+ if c == '"' and nxt == '"':
307
+ i += 1
308
+ elif c == '"':
309
+ state = NORMAL
310
+
311
+ elif state == LINE_COMMENT:
312
+ if c == "\n":
313
+ state = NORMAL
314
+
315
+ elif state == BLOCK_COMMENT:
316
+ if c == "/" and nxt == "*":
317
+ block_depth += 1
318
+ i += 1
319
+ elif c == "*" and nxt == "/":
320
+ block_depth -= 1
321
+ if block_depth == 0:
322
+ state = NORMAL
323
+ i += 1
324
+
325
+ elif state == DOLLAR_QUOTE:
326
+ if c == "$" and text[i : i + len(dollar_close)] == dollar_close:
327
+ i += len(dollar_close)
328
+ state = NORMAL
329
+ dollar_tag = ""
330
+ dollar_close = ""
331
+ continue
332
+
333
+ i += 1
334
+
335
+ tail = text[stmt_start:].strip()
336
+ if tail:
337
+ result.append(tail)
338
+
339
+ return result
340
+
341
+
124
342
  def trim_comment(sql: str) -> str:
125
343
  """
126
344
  Remove single-line and multi-line comments from SQL.
@@ -226,7 +444,7 @@ def get_source_target_tables(sql: str) -> dict[str, list[str]] | None:
226
444
  ParseException: If SQL contains multiple statements
227
445
 
228
446
  Note:
229
- TODO:
447
+ TODO: 不能识别join后面的 [hint] table_name(已在 get_source_target_tables_v2 实现)
230
448
  {
231
449
  "source_tables": [(t1, 1), (t2, 2), (t3, 3)],
232
450
  "target_tables": [(t4, 1)]
@@ -345,6 +563,153 @@ def get_source_target_tables(sql: str) -> dict[str, list[str]] | None:
345
563
  return
346
564
 
347
565
 
566
+ def get_source_target_tables_v2(sql: str) -> dict[str, list[str]] | None:
567
+ """
568
+ Extract source and target tables from a single SQL statement using a token-based approach.
569
+ changelog:
570
+ - 支持识别join后面的 [hint] table_name
571
+ """
572
+ # 1. 预处理
573
+ clean_sql = trim_comment(sql).strip()
574
+ if not clean_sql:
575
+ return None
576
+
577
+ # 去除末尾分号
578
+ if clean_sql.endswith(";"):
579
+ clean_sql = clean_sql[:-1]
580
+
581
+ # 校验多语句
582
+ # 注意:这里直接传原始 sql 给 split_sql 可能更稳妥,或者传 clean_sql 取决于 split_sql 的实现
583
+ if len(split_sql(clean_sql)) > 1:
584
+ raise ParseException("sql脚本为多条SQL语句,需传入单条SQL语句.")
585
+
586
+ # 2. 分词 (使用正则优化性能)
587
+ # 匹配单词、括号、逗号。忽略空白字符。
588
+ # tokens = re.findall(r"[A-Za-z_][A-Za-z0-9_]*|[()]|,", clean_sql)
589
+ tokens = re.findall(r"\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_]*|[()]|,", clean_sql)
590
+
591
+ if not tokens:
592
+ return None
593
+
594
+ # 3. 状态机变量
595
+ source_tables = set()
596
+ target_tables = set()
597
+
598
+ # 状态标志
599
+ # 状态标志
600
+ is_insert_context = False
601
+ is_from_context = False
602
+ is_join_context = False # 新增:专门标记 JOIN 后的状态
603
+ is_using_context = False
604
+ is_merge_context = False
605
+
606
+ # 辅助变量
607
+
608
+ for token in tokens:
609
+ token_upper = token.upper()
610
+
611
+ # --- 特殊处理:跳过 Hint 内容 ---
612
+ # 如果 token 是 [...] 格式,直接跳过,不要把它当成括号或关键字
613
+ if token.startswith("[") and token.endswith("]"):
614
+ # 如果是在 JOIN 后面遇到的 Hint,保持 join_context 为 True
615
+ # 如果是在 FROM 后面遇到的 Hint,保持 from_context 为 True
616
+ continue
617
+
618
+ # --- 上下文重置 ---
619
+ if token == "(":
620
+ # 左括号通常意味着子查询,重置 FROM/JOIN 上下文
621
+ # 但我们不能重置 INSERT 上下文
622
+ is_from_context = False
623
+ is_join_context = False
624
+ is_using_context = False
625
+ continue
626
+
627
+ # --- 关键字状态流转 ---
628
+
629
+ if token_upper in KeyWords.insert_keywords:
630
+ is_insert_context = True
631
+ is_from_context = False
632
+ is_join_context = False
633
+ continue
634
+
635
+ if token_upper == "FROM":
636
+ is_from_context = True
637
+ is_join_context = False
638
+ is_insert_context = False
639
+ continue
640
+
641
+ # 修复核心:处理 JOIN
642
+ if token_upper in ["JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS"]:
643
+ is_from_context = True # JOIN 也是一种来源
644
+ is_join_context = True # 标记刚刚遇到了 JOIN,下一个非关键字即为表名
645
+ continue
646
+
647
+ # 【关键修复】:遇到 ON,说明表名部分结束了,后面是关联条件
648
+ if token_upper == "ON":
649
+ is_from_context = False
650
+ is_join_context = False
651
+ continue
652
+
653
+ if token_upper == "MERGE":
654
+ is_merge_context = True
655
+ continue
656
+
657
+ if token_upper == "USING":
658
+ is_from_context = False
659
+ is_join_context = False
660
+ if is_merge_context:
661
+ is_using_context = True
662
+ continue
663
+
664
+ # --- 表名捕获逻辑 ---
665
+
666
+ # 1. INSERT 目标表
667
+ if is_insert_context and token_upper not in KeyWords.keywords and token not in ("(", ")"):
668
+ target_tables.add(token)
669
+ is_insert_context = False
670
+ continue
671
+
672
+ # 2. MERGE 逻辑 (略,保持原有逻辑) ...
673
+ if is_merge_context and not is_using_context and token_upper not in KeyWords.keywords:
674
+ target_tables.add(token)
675
+ is_merge_context = False
676
+ continue
677
+
678
+ if is_using_context and token_upper not in KeyWords.keywords:
679
+ if token != "(":
680
+ source_tables.add(token)
681
+ is_using_context = False
682
+ is_merge_context = False
683
+ continue
684
+
685
+ # 3. FROM / JOIN 源表 (修复核心)
686
+ # 条件:处于 FROM 或 JOIN 状态,且不是关键字,且不是括号/逗号
687
+ if (is_from_context or is_join_context) and token_upper not in KeyWords.keywords and token not in ("(", ")", ","):
688
+ source_tables.add(token)
689
+
690
+ # 捕获到表名后,重置状态,等待下一个 JOIN 或逗号
691
+ is_join_context = False
692
+ # 注意:is_from_context 保持 True,以便处理 "FROM t1, t2" 这种逗号分隔的情况
693
+ continue
694
+
695
+ # 处理逗号:逗号意味着可能有下一个表名,重置别名状态,保持 FROM 上下文
696
+ if token == ",":
697
+ is_join_context = False
698
+ # is_from_context 保持不变
699
+ continue
700
+
701
+ # 4. 过滤 CTE 中间表
702
+ # 注意:_get_cte_mid_tables 需要解析 WITH 子句,这里假设它工作正常
703
+ cte_tables = _get_cte_mid_tables(clean_sql)
704
+ source_tables -= set(cte_tables)
705
+
706
+ # 5. 构建结果
707
+ if source_tables or target_tables:
708
+ return {"source_tables": list(source_tables), "target_tables": list(target_tables)}
709
+
710
+ return None
711
+
712
+
348
713
  # ============================================================================
349
714
  # Private helper functions
350
715
  # ============================================================================
@@ -0,0 +1,77 @@
1
+ from sqlh.core.helper import (
2
+ get_source_target_tables,
3
+ get_source_target_tables_v2,
4
+ split_sql,
5
+ split_sql_v2,
6
+ split_sql_v3,
7
+ trim_comment,
8
+ )
9
+
10
+
11
+ def test_split():
12
+ """Test SQL splitting functionality."""
13
+ sql = """
14
+ SELECT * FROM t1;
15
+ INSERT INTO t2 SELECT * FROM t1;
16
+ """
17
+ result = split_sql(sql)
18
+ assert len(result) == 2
19
+ assert "SELECT * FROM t1" in result[0]
20
+ assert "INSERT INTO t2 SELECT * FROM t1" in result[1]
21
+
22
+
23
+ def test_split_v2():
24
+ """Test SQL splitting functionality."""
25
+ sql = """/*select '12;', ; */
26
+ -- okk
27
+ SELECT * FROM t1; -- ddd ;
28
+ INSERT INTO t2 SELECT * FROM t1;
29
+ """
30
+ result = split_sql_v2(sql)
31
+ for stmt in result:
32
+ print("--- SQL Statement ---")
33
+ print(stmt)
34
+
35
+
36
+ def test_split_v3():
37
+ """Test SQL splitting functionality."""
38
+ sql = """/*select '12;', ; */
39
+ -- okk
40
+ SELECT * FROM t1; -- ddd ;
41
+ INSERT INTO t2 SELECT * FROM t1;
42
+ """
43
+ result = split_sql_v3(sql)
44
+ for stmt in result:
45
+ print("--- SQL Statement ---")
46
+ print(stmt)
47
+
48
+
49
+ def test_trim_comment():
50
+ """Test comment removal."""
51
+ sql = """
52
+ -- This is a comment
53
+ SELECT * FROM t1;
54
+ /* Multi-line
55
+ comment */
56
+ INSERT INTO t2 SELECT * FROM t1;
57
+ """
58
+ result = trim_comment(sql)
59
+ assert "--" not in result
60
+ assert "/*" not in result
61
+
62
+
63
+ def test_get_source_target_tables():
64
+ """Test source/target table extraction."""
65
+ sql = "INSERT INTO dwd.user_dim SELECT * FROM ods.user;"
66
+ result = get_source_target_tables(sql)
67
+ assert result is not None
68
+ assert "ods.user" in result["source_tables"]
69
+ assert "dwd.user_dim" in result["target_tables"]
70
+
71
+
72
+ def test_get_source_target_tables_v2():
73
+ """Test source/target table extraction."""
74
+ sql = "SELECT COUNT(*) FROM t2 JOIN [broadcast] t1 ON t1.c1 = t2.c2;"
75
+ result = get_source_target_tables_v2(sql)
76
+ for table in result["source_tables"]:
77
+ print(f"Source Table: {table}")
@@ -5,7 +5,7 @@ from sqlh import utils
5
5
  # 读取目录或文件
6
6
  sql_path = ""
7
7
  sql_stmt_str = utils.read_sql_from_directory(sql_path)
8
-
8
+ sql_stmt_str = """insert into t3 SELECT /*+edede */ COUNT(*) FROM t2 JOIN [broadcast] t1 ON t1.c1 = t2.c2;"""
9
9
 
10
10
  def test_read_sql_from_directory():
11
11
  import timeit
@@ -26,7 +26,7 @@ from pathlib import Path
26
26
  from typing import List, Literal, Tuple, Union
27
27
 
28
28
  from .core.graph import DagGraph, NodeNotFoundException
29
- from .core.helper import get_source_target_tables, split_sql, trim_comment
29
+ from .core.helper import get_source_target_tables_v2, split_sql, trim_comment
30
30
 
31
31
  SearchResult = Union[Tuple[List[str], DagGraph], NodeNotFoundException]
32
32
 
@@ -96,7 +96,7 @@ def __build_tables_and_graph(sql_stmt_str: str) -> Tuple[list, list, DagGraph]:
96
96
  dg = DagGraph()
97
97
 
98
98
  for sql_stmt in sql_stmt_lst:
99
- table_info = get_source_target_tables(sql_stmt)
99
+ table_info = get_source_target_tables_v2(sql_stmt)
100
100
  if table_info:
101
101
  sources = [re.sub(r"`|\"", "", t) for t in table_info["source_tables"]]
102
102
  targets = [re.sub(r"`|\"", "", t) for t in table_info["target_tables"]]
@@ -305,6 +305,10 @@ def visualize_dag(
305
305
  ) -> None:
306
306
  import webbrowser
307
307
 
308
+ if dag_graph.empty:
309
+ print("DAG图为空, 无需生成可视化")
310
+ return
311
+
308
312
  html_content = dag_graph.to_html(template_type=template_type)
309
313
 
310
314
  with open(filename, "w", encoding="utf-8") as f:
@@ -1,36 +0,0 @@
1
- from sqlh.core.helper import get_source_target_tables, split_sql, trim_comment
2
-
3
-
4
- def test_split():
5
- """Test SQL splitting functionality."""
6
- sql = """
7
- SELECT * FROM t1;
8
- INSERT INTO t2 SELECT * FROM t1;
9
- """
10
- result = split_sql(sql)
11
- assert len(result) == 2
12
- assert "SELECT * FROM t1" in result[0]
13
- assert "INSERT INTO t2 SELECT * FROM t1" in result[1]
14
-
15
-
16
- def test_trim_comment():
17
- """Test comment removal."""
18
- sql = """
19
- -- This is a comment
20
- SELECT * FROM t1;
21
- /* Multi-line
22
- comment */
23
- INSERT INTO t2 SELECT * FROM t1;
24
- """
25
- result = trim_comment(sql)
26
- assert "--" not in result
27
- assert "/*" not in result
28
-
29
-
30
- def test_get_source_target_tables():
31
- """Test source/target table extraction."""
32
- sql = "INSERT INTO dwd.user_dim SELECT * FROM ods.user;"
33
- result = get_source_target_tables(sql)
34
- assert result is not None
35
- assert "ods.user" in result["source_tables"]
36
- assert "dwd.user_dim" in result["target_tables"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes