waghelad commited on
Commit
12eff8e
·
verified ·
1 Parent(s): 65b94c1

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +6 -35
  2. .gitignore +19 -0
  3. .hfignore +17 -0
  4. LICENSE +190 -0
  5. MODEL_CARD.md +47 -0
  6. PROJECT.md +115 -0
  7. README.md +62 -9
  8. README_HF_SPACE.md +68 -0
  9. app.py +534 -0
  10. datasets/.gitignore +2 -0
  11. datasets/leaderboard.json +12 -0
  12. datasets/vertica_snowflake_pairs.jsonl +256 -0
  13. demo/__init__.py +0 -0
  14. demo/handlers.py +1376 -0
  15. demo/theme.py +265 -0
  16. examples/ml_features/churn_feature_sql.sql +40 -0
  17. examples/vertica_legacy/procedures/SP_BUILD_CUSTOMER_DAILY.sql +61 -0
  18. examples/vertica_legacy/queries/customer_lifetime_value.sql +60 -0
  19. examples/vertica_legacy/tables/customer_tables.sql +37 -0
  20. examples/vertica_legacy/tables/refunds.sql +12 -0
  21. examples/vertica_legacy/views/V_CUSTOMER_METRICS.sql +25 -0
  22. model/README.md +59 -0
  23. model/config.json +34 -0
  24. model/rewrite_vocabulary.json +12 -0
  25. model/risk_classifier.joblib +3 -0
  26. morphsql/__init__.py +22 -0
  27. morphsql/ai/__init__.py +14 -0
  28. morphsql/ai/agent.py +251 -0
  29. morphsql/ai/pipeline.py +82 -0
  30. morphsql/ai/risk_model.py +195 -0
  31. morphsql/assistant/__init__.py +5 -0
  32. morphsql/assistant/copilot.py +370 -0
  33. morphsql/cli.py +275 -0
  34. morphsql/dbt_generator/__init__.py +5 -0
  35. morphsql/dbt_generator/decomposer.py +520 -0
  36. morphsql/eval/__init__.py +14 -0
  37. morphsql/eval/leaderboard.py +68 -0
  38. morphsql/eval/metrics.py +170 -0
  39. morphsql/eval/pairs.py +274 -0
  40. morphsql/intelligence/__init__.py +12 -0
  41. morphsql/intelligence/lineage_viz.py +119 -0
  42. morphsql/intelligence/rag.py +168 -0
  43. morphsql/intelligence/rationalization.py +65 -0
  44. morphsql/intelligence/runbook.py +113 -0
  45. morphsql/knowledge/__init__.py +15 -0
  46. morphsql/knowledge/behavior.py +227 -0
  47. morphsql/lineage/__init__.py +21 -0
  48. morphsql/lineage/builder.py +243 -0
  49. morphsql/models.py +128 -0
  50. morphsql/parser/__init__.py +19 -0
.gitattributes CHANGED
@@ -1,35 +1,6 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ [README.md]
2
+ README_HF_SPACE.md linguist-generated=false
3
+
4
+ [examples/**]
5
+ *.sql linguist-language=SQL
6
+ model/risk_classifier.joblib filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .env
12
+ .mypy_cache/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ htmlcov/
16
+ .coverage
17
+ migration-output/
18
+ *.html
19
+ !sqlshift/report/*.py
.hfignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Extra excludes if this repo is used as a git-backed Hugging Face Space
2
+ .venv/
3
+ venv/
4
+ .git/
5
+ __pycache__/
6
+ *.pyc
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ htmlcov/
12
+ .coverage
13
+ migration-output/
14
+ .cursor/
15
+ canvases/
16
+ .env
17
+ .DS_Store
LICENSE ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ Copyright 2026 Digvijay Waghela
179
+
180
+ Licensed under the Apache License, Version 2.0 (the "License");
181
+ you may not use this file except in compliance with the License.
182
+ You may obtain a copy of the License at
183
+
184
+ http://www.apache.org/licenses/LICENSE-2.0
185
+
186
+ Unless required by applicable law or agreed to in writing, software
187
+ distributed under the License is distributed on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189
+ See the License for the specific language governing permissions and
190
+ limitations under the License.
MODEL_CARD.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ license: apache-2.0
4
+ library_name: morphsql
5
+ # Product name on the Hub UI / Space: MorphSQL
6
+ pipeline_tag: text-classification
7
+ tags:
8
+ - morphsql
9
+ - agent
10
+ - code
11
+ - sql
12
+ - text-generation
13
+ - text-classification
14
+ - rag
15
+ - sklearn
16
+ - migration
17
+ - dbt
18
+ - snowflake
19
+ datasets:
20
+ - dgvj-work/vertica-snowflake-pairs
21
+ ---
22
+
23
+ # MorphSQL
24
+
25
+ Convert Vertica / Oracle / Redshift / BigQuery SQL to **pandas**, **PySpark**, Snowflake, BigQuery, or dbt.
26
+
27
+ > Brand: **MorphSQL** · Package import: `morphsql` · Hub slug: `dgvj-work/morphsql`
28
+
29
+ ## Artifacts
30
+ - `risk_classifier.joblib` — migration risk (`low` / `medium` / `high`)
31
+ - `rewrite_vocabulary.json` — rewrite lexicon
32
+ - `config.json` — dialect metadata
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ from morphsql.ai import pipeline
38
+
39
+ print(pipeline("sql-migration")("SELECT COALESCE(a, 0) FROM t", source="snowflake", target="pandas"))
40
+ print(pipeline("sql-migration")("SELECT COALESCE(a, 0) FROM t", source="snowflake", target="pyspark"))
41
+ print(pipeline("sql-risk-classification")("EXECUTE IMMEDIATE 'x'"))
42
+ ```
43
+
44
+ ## Demo
45
+ https://huggingface.co/spaces/dgvj-work/morphsql
46
+
47
+ Author: Digvijay Waghela · Apache-2.0
PROJECT.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MorphSQL — Project Context Document
2
+
3
+ > **Purpose:** Give this file to any AI assistant or new developer to continue work without prior conversation context.
4
+
5
+ ## Naming
6
+
7
+ | Layer | Name |
8
+ |-------|------|
9
+ | **Product (UI, docs, Hugging Face Space title)** | **MorphSQL** |
10
+ | Python import | `morphsql` |
11
+ | PyPI package | `morphsql` |
12
+ | GitHub repo | `morphsql` |
13
+ | HF Space / model slug | `dgvj-work/morphsql` |
14
+
15
+ Always use **MorphSQL** / `morphsql` consistently (product, package, CLI, Hub).
16
+
17
+ ## What this project is
18
+
19
+ **MorphSQL** is an open-source **SQL migration toolkit** (package import: `morphsql`).
20
+
21
+ It is **not** only a single-query converter. It analyzes legacy SQL repositories and produces:
22
+
23
+ 1. **Discovery** — scan zip files/directories for SQL, procedures, views, dbt models
24
+ 2. **Lineage** — dependency graphs (table/object level)
25
+ 3. **Risk assessment** — complexity scoring, unsupported syntax, downstream impact
26
+ 4. **Workload rationalization** — migrate / review / rewrite / retire per object
27
+ 5. **Hybrid translation** — rule-based + sqlglot dialect conversion (pandas / PySpark / warehouse / dbt)
28
+ 6. **dbt architecture** — decompose procedures into staging/intermediate/mart
29
+ 7. **Validation** — reconciliation tests (row count, null rate, checksums)
30
+ 8. **Migration runbook** — phased cutover plan with object action table
31
+ 9. **Migration copilot** — LLM advisor grounded in scan context (HF Inference API)
32
+
33
+ ## Repository layout
34
+
35
+ ```
36
+ morphsql/
37
+ ├── app.py # Hugging Face Gradio Space (MorphSQL UI)
38
+ ├── demo/
39
+ │ ├── handlers.py # Gradio event handlers
40
+ │ └── theme.py # Dark theme CSS/constants
41
+ ├── morphsql/ # Core Python package
42
+ │ ├── scanner/ # Repository discovery
43
+ │ ├── parser/ # SQL parsing (sqlglot)
44
+ │ ├── lineage/ # Dependency graphs (networkx)
45
+ │ ├── translator/ # Hybrid SQL conversion (+ pandas/pyspark codegen)
46
+ │ ├── risk/ # Complexity & risk scoring
47
+ │ ├── dbt_generator/ # dbt project decomposition
48
+ │ ├── validation/ # Reconciliation tests
49
+ │ ├── knowledge/ # Platform behavior differences KB
50
+ │ ├── intelligence/ # Runbook, rationalization, lineage viz
51
+ │ ├── assistant/ # LLM copilot (huggingface_hub)
52
+ │ ├── report/ # HTML report generation
53
+ │ ├── pipeline.py # Agent orchestration
54
+ │ ├── models.py # Pydantic data models
55
+ │ └── cli.py # `morphsql` CLI
56
+ ├── examples/vertica_legacy/ # Sample legacy Vertica repository
57
+ ├── tests/
58
+ ├── PROJECT.md # This file
59
+ ├── README.md
60
+ ├── README_HF_SPACE.md # HF Space card (title: MorphSQL)
61
+ ├── MODEL_CARD.md
62
+ └── pyproject.toml
63
+ ```
64
+
65
+ ## Architecture
66
+
67
+ ```
68
+ Repository (zip / directory)
69
+ ↓
70
+ MigrationPipeline.analyze() → Discovery + Lineage + Risk
71
+ ↓
72
+ MigrationPipeline.convert() → Hybrid SQL translation
73
+ ↓
74
+ MigrationPipeline.validate() → Reconciliation tests
75
+ ↓
76
+ generate_runbook() → Cutover plan
77
+ generate_rationalization() → Migrate/retire recommendations
78
+ decompose_to_dbt() → dbt scaffold
79
+ MigrationCopilot.respond() → LLM Q&A grounded in report
80
+ ```
81
+
82
+ ## Key modules
83
+
84
+ | Module | File | Purpose |
85
+ |--------|------|---------|
86
+ | Pipeline | `morphsql/pipeline.py` | Orchestrates full workflow |
87
+ | Translator | `morphsql/translator/engine.py` | ZEROIFNULL, DATEADD, procedure wrappers, sqlglot |
88
+ | Pandas / PySpark | `morphsql/translator/*_codegen.py` | Notebook / Spark DataFrame codegen |
89
+ | Copilot | `morphsql/assistant/copilot.py` | HF Inference API + fallback KB |
90
+ | Runbook | `morphsql/intelligence/runbook.py` | Migration runbook markdown |
91
+ | Lineage viz | `morphsql/intelligence/lineage_viz.py` | Plotly network graph |
92
+ | Rationalization | `morphsql/intelligence/rationalization.py` | Workload action plan |
93
+
94
+ ## CLI commands
95
+
96
+ ```bash
97
+ morphsql analyze PATH -s vertica -t snowflake
98
+ morphsql convert PATH -s snowflake -t pandas
99
+ morphsql migrate PATH -s vertica -t snowflake -o migration-output
100
+ morphsql version
101
+ ```
102
+
103
+ ## Hugging Face
104
+
105
+ - Space title: **MorphSQL**
106
+ - Space URL: https://huggingface.co/spaces/dgvj-work/morphsql
107
+ - Deploy: `./scripts/deploy_hf.sh`
108
+ - Preflight: `python scripts/check_space.py`
109
+ - Copilot model env: `MORPHSQL_MODEL`
110
+
111
+ ## Gradio UI tabs
112
+
113
+ 1. **Convert** — primary: paste/upload SQL → pandas/PySpark/SQL/dbt + preview + download
114
+ 2. **Guide** — how-to for DS / AI users
115
+ 3. **More** — object assess, repo workbench, feature SQL, copilot, RAG, eval
README.md CHANGED
@@ -1,15 +1,68 @@
1
  ---
2
- title: Morphsql
3
- emoji: ⚡
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
11
  license: apache-2.0
12
- short_description: AI Sql Converter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MorphSQL
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: "5.49.1"
8
+ python_version: "3.11"
9
  app_file: app.py
10
+ pinned: true
11
  license: apache-2.0
12
+ short_description: Convert SQL to pandas, PySpark, warehouses, or dbt
13
+ tags:
14
+ - sql
15
+ - pandas
16
+ - pyspark
17
+ - code
18
+ - data-science
19
+ - machine-learning
20
+ - feature-extraction
21
+ - snowflake
22
+ - dbt
23
+ - data-engineering
24
+ models:
25
+ - dgvj-work/morphsql
26
+ datasets:
27
+ - dgvj-work/vertica-snowflake-pairs
28
+ suggested_hardware: cpu-basic
29
  ---
30
 
31
+ # MorphSQL
32
+
33
+ Convert warehouse SQL → **pandas**, **PySpark**, Snowflake, BigQuery, or **dbt** — then **download** the result.
34
+
35
+ Package / CLI / Hub: `morphsql` · Space: [dgvj-work/morphsql](https://huggingface.co/spaces/dgvj-work/morphsql)
36
+
37
+ ## How to use (30 seconds)
38
+
39
+ 1. Open the **Convert** tab
40
+ 2. Pick source dialect + **Convert to** target
41
+ 3. Load an example, paste SQL, or **upload** a `.sql` / `.zip`
42
+ 4. Click **Convert** or **Upload & Convert → Download**
43
+ 5. Download the `.py` / `.sql` / `.zip` to your machine
44
+
45
+ | Target | Download |
46
+ |--------|----------|
47
+ | pandas / PySpark | `.py` ready for notebooks / Databricks |
48
+ | Snowflake / BigQuery | `.sql` |
49
+ | dbt | `.txt` project preview (or zip for batches) |
50
+
51
+ ## Python API
52
+
53
+ ```python
54
+ from morphsql.ai import pipeline
55
+
56
+ out = pipeline("sql-migration")(
57
+ "SELECT COALESCE(a, 0) FROM t",
58
+ source="snowflake",
59
+ target="pandas", # or "pyspark", "snowflake", "bigquery", "dbt-snowflake"
60
+ )
61
+ print(out["converted_sql"][:500])
62
+ ```
63
+
64
+ ## More tab
65
+
66
+ Object risk assess · repo workbench · ML feature SQL · copilot · dialect notes · offline eval
67
+
68
+ Author: Digvijay Waghela · Apache-2.0 · [GitHub](https://github.com/dgvj-work/morphsql)
README_HF_SPACE.md ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MorphSQL
3
+ emoji: 🧬
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: "5.49.1"
8
+ python_version: "3.11"
9
+ app_file: app.py
10
+ pinned: true
11
+ license: apache-2.0
12
+ short_description: Convert SQL to pandas, PySpark, warehouses, or dbt
13
+ tags:
14
+ - sql
15
+ - pandas
16
+ - pyspark
17
+ - code
18
+ - data-science
19
+ - machine-learning
20
+ - feature-extraction
21
+ - snowflake
22
+ - dbt
23
+ - data-engineering
24
+ models:
25
+ - dgvj-work/morphsql
26
+ datasets:
27
+ - dgvj-work/vertica-snowflake-pairs
28
+ suggested_hardware: cpu-basic
29
+ ---
30
+
31
+ # MorphSQL
32
+
33
+ Convert warehouse SQL → **pandas**, **PySpark**, Snowflake, BigQuery, or **dbt** — then **download** the result.
34
+
35
+ Package / CLI / Hub: `morphsql` · Space: [dgvj-work/morphsql](https://huggingface.co/spaces/dgvj-work/morphsql)
36
+
37
+ ## How to use (30 seconds)
38
+
39
+ 1. Open the **Convert** tab
40
+ 2. Pick source dialect + **Convert to** target
41
+ 3. Load an example, paste SQL, or **upload** a `.sql` / `.zip`
42
+ 4. Click **Convert** or **Upload & Convert → Download**
43
+ 5. Download the `.py` / `.sql` / `.zip` to your machine
44
+
45
+ | Target | Download |
46
+ |--------|----------|
47
+ | pandas / PySpark | `.py` ready for notebooks / Databricks |
48
+ | Snowflake / BigQuery | `.sql` |
49
+ | dbt | `.txt` project preview (or zip for batches) |
50
+
51
+ ## Python API
52
+
53
+ ```python
54
+ from morphsql.ai import pipeline
55
+
56
+ out = pipeline("sql-migration")(
57
+ "SELECT COALESCE(a, 0) FROM t",
58
+ source="snowflake",
59
+ target="pandas", # or "pyspark", "snowflake", "bigquery", "dbt-snowflake"
60
+ )
61
+ print(out["converted_sql"][:500])
62
+ ```
63
+
64
+ ## More tab
65
+
66
+ Object risk assess · repo workbench · ML feature SQL · copilot · dialect notes · offline eval
67
+
68
+ Author: Digvijay Waghela · Apache-2.0 · [GitHub](https://github.com/dgvj-work/morphsql)
app.py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MorphSQL — Hugging Face Space (conversion-first, DS-friendly)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+
7
+ import gradio as gr
8
+
9
+ from morphsql import __product_name__, __version__
10
+ from morphsql.ai.risk_model import train_and_save
11
+ from morphsql.eval.pairs import ensure_pairs_file
12
+ from demo.handlers import (
13
+ HERO_EXAMPLE,
14
+ PLAYGROUND_EXAMPLE_LABELS,
15
+ SOURCE_DROPDOWN,
16
+ TARGET_DROPDOWN,
17
+ analyze_sql_object_ui,
18
+ convert_for_ui,
19
+ convert_upload_for_ui,
20
+ copilot_chat,
21
+ get_leaderboard_md,
22
+ load_sql_from_upload,
23
+ on_example_selected,
24
+ run_behavior_rag,
25
+ run_eval_suite,
26
+ run_feature_migration,
27
+ run_workbench_ui,
28
+ submit_eval_score,
29
+ )
30
+ from demo.theme import CUSTOM_CSS, build_theme
31
+
32
+ SPACE_URL = "https://huggingface.co/spaces/dgvj-work/morphsql"
33
+ GITHUB_URL = "https://github.com/dgvj-work/morphsql"
34
+
35
+ # Soft-fail boot so the Space still loads if artifacts are regenerating
36
+ try:
37
+ ensure_pairs_file()
38
+ except Exception as exc: # pragma: no cover
39
+ print(f"[MorphSQL] pairs bootstrap skipped: {exc}")
40
+ try:
41
+ train_and_save()
42
+ except Exception as exc: # pragma: no cover
43
+ print(f"[MorphSQL] risk model bootstrap skipped: {exc}")
44
+
45
+ try:
46
+ _BOOT = convert_for_ui(HERO_EXAMPLE, "snowflake", "pandas")
47
+ except Exception as exc: # pragma: no cover
48
+ print(f"[MorphSQL] boot convert failed: {exc}")
49
+ _BOOT = (
50
+ "Paste SQL and click **Convert**.",
51
+ "",
52
+ "Ready",
53
+ f"[Space]({SPACE_URL})",
54
+ None,
55
+ None,
56
+ "# Convert SQL to get a notebook cell",
57
+ 'from morphsql.ai import pipeline\nprint(pipeline("sql-migration")("SELECT 1", source="snowflake", target="pandas"))\n',
58
+ )
59
+
60
+
61
+ def _blocks_style_kwargs() -> dict:
62
+ """Gradio 5 (HF Spaces): theme/css on Blocks. Gradio 6+: those moved to launch()."""
63
+ params = inspect.signature(gr.Blocks.__init__).parameters
64
+ kwargs: dict = {}
65
+ if "theme" in params:
66
+ kwargs["theme"] = build_theme()
67
+ if "css" in params:
68
+ kwargs["css"] = CUSTOM_CSS
69
+ return kwargs
70
+
71
+
72
+ def _launch_style_kwargs() -> dict:
73
+ """Pass theme/css to launch() when Blocks no longer accepts them (Gradio 6+)."""
74
+ if "theme" in inspect.signature(gr.Blocks.__init__).parameters:
75
+ return {}
76
+ return {"theme": build_theme(), "css": CUSTOM_CSS}
77
+
78
+
79
+ def _chatbot(**kwargs):
80
+ """Chatbot with type='messages' when supported (Gradio 5.x)."""
81
+ params = inspect.signature(gr.Chatbot.__init__).parameters
82
+ if "type" in params and "type" not in kwargs:
83
+ kwargs["type"] = "messages"
84
+ return gr.Chatbot(**kwargs)
85
+
86
+
87
+ def _build_demo() -> gr.Blocks:
88
+ with gr.Blocks(
89
+ title=f"{__product_name__} — SQL → pandas / PySpark",
90
+ **_blocks_style_kwargs(),
91
+ ) as demo:
92
+ eval_state = gr.State(value={})
93
+ eval_category = gr.State(value="all")
94
+
95
+ gr.HTML(
96
+ f"""
97
+ <div class="header-block">
98
+ <div class="eyebrow">AI / ML · DATA SCIENCE · SQL → PANDAS / PYSPARK</div>
99
+ <h1>{__product_name__}</h1>
100
+ <p>Turn warehouse SQL into notebook-ready <strong>pandas</strong> or
101
+ <strong>PySpark</strong> — paste or upload a file, convert, then download
102
+ <code>.py</code> / <code>.sql</code> to your machine.</p>
103
+ </div>
104
+ """
105
+ )
106
+
107
+ with gr.Tabs():
108
+ with gr.Tab("Convert"):
109
+ gr.Markdown(
110
+ "1. Choose input dialect + output · "
111
+ "2. Paste SQL, load an example, or **upload** a `.sql` / `.zip` · "
112
+ "3. **Convert** → preview + **download** to your machine"
113
+ )
114
+
115
+ example = gr.Dropdown(
116
+ choices=PLAYGROUND_EXAMPLE_LABELS,
117
+ value=PLAYGROUND_EXAMPLE_LABELS[0],
118
+ label="Load a data-science / AI example",
119
+ info="Fills SQL, converts, runs sample preview.",
120
+ )
121
+
122
+ with gr.Row(equal_height=True):
123
+ source = gr.Dropdown(
124
+ choices=SOURCE_DROPDOWN,
125
+ value="snowflake",
126
+ label="SQL is written for",
127
+ scale=2,
128
+ min_width=160,
129
+ )
130
+ target = gr.Dropdown(
131
+ choices=TARGET_DROPDOWN,
132
+ value="pandas",
133
+ label="Convert to",
134
+ scale=3,
135
+ min_width=220,
136
+ )
137
+ convert_btn = gr.Button(
138
+ "Convert",
139
+ variant="primary",
140
+ scale=1,
141
+ min_width=120,
142
+ elem_classes=["action-btn"],
143
+ )
144
+
145
+ with gr.Row(equal_height=False):
146
+ sql_upload = gr.File(
147
+ label="Upload SQL (.sql / .txt) or a .zip of SQL files",
148
+ file_types=[".sql", ".txt", ".ddl", ".zip"],
149
+ type="filepath",
150
+ scale=4,
151
+ min_width=280,
152
+ elem_classes=["sql-upload-box"],
153
+ )
154
+ with gr.Column(scale=1, min_width=180, elem_classes=["upload-action-col"]):
155
+ upload_convert_btn = gr.Button(
156
+ "Upload & Convert → Download",
157
+ variant="primary",
158
+ elem_classes=["action-btn", "upload-action-btn"],
159
+ )
160
+
161
+ status = gr.Textbox(
162
+ label="Status",
163
+ value=_BOOT[2],
164
+ interactive=False,
165
+ max_lines=1,
166
+ )
167
+
168
+ with gr.Row():
169
+ sql_in = gr.Textbox(
170
+ label="Input SQL",
171
+ value=HERO_EXAMPLE,
172
+ lines=12,
173
+ max_lines=24,
174
+ placeholder="Paste warehouse SQL, or upload a file above…",
175
+ )
176
+ sql_out = gr.Textbox(
177
+ label="Output code",
178
+ value=_BOOT[1],
179
+ lines=12,
180
+ max_lines=24,
181
+ )
182
+
183
+ with gr.Row():
184
+ download = gr.File(
185
+ label="Download converted file (.py / .sql / .zip)",
186
+ value=_BOOT[5],
187
+ elem_classes=["download-box"],
188
+ )
189
+ preview = gr.Dataframe(
190
+ label="Sample preview",
191
+ value=_BOOT[4],
192
+ wrap=True,
193
+ elem_classes=["preview-table"],
194
+ )
195
+
196
+ notes = gr.Markdown(value=_BOOT[0])
197
+ with gr.Accordion("Notebook starter cell", open=False):
198
+ notebook = gr.Code(
199
+ language="python",
200
+ value=_BOOT[6],
201
+ lines=10,
202
+ )
203
+ with gr.Accordion("Hugging Face pipeline API (AI / ML)", open=True):
204
+ gr.Markdown(
205
+ "Same style as `transformers.pipeline` — use in Colab, HF Jobs, or training scripts."
206
+ )
207
+ api_code = gr.Code(
208
+ language="python",
209
+ value=_BOOT[7],
210
+ lines=12,
211
+ )
212
+ share = gr.Markdown(value=_BOOT[3])
213
+
214
+ outs = [
215
+ sql_in,
216
+ source,
217
+ target,
218
+ notes,
219
+ sql_out,
220
+ status,
221
+ share,
222
+ preview,
223
+ download,
224
+ notebook,
225
+ api_code,
226
+ ]
227
+ example.change(on_example_selected, inputs=[example], outputs=outs)
228
+
229
+ convert_outs = [
230
+ notes,
231
+ sql_out,
232
+ status,
233
+ share,
234
+ preview,
235
+ download,
236
+ notebook,
237
+ api_code,
238
+ ]
239
+ convert_btn.click(
240
+ convert_for_ui,
241
+ inputs=[sql_in, source, target],
242
+ outputs=convert_outs,
243
+ )
244
+ sql_in.submit(
245
+ convert_for_ui,
246
+ inputs=[sql_in, source, target],
247
+ outputs=convert_outs,
248
+ )
249
+
250
+ # Upload: fill the editor immediately; button converts + refreshes download
251
+ sql_upload.change(
252
+ load_sql_from_upload,
253
+ inputs=[sql_upload, sql_in],
254
+ outputs=[sql_in],
255
+ )
256
+ upload_convert_outs = [
257
+ sql_in,
258
+ notes,
259
+ sql_out,
260
+ status,
261
+ share,
262
+ preview,
263
+ download,
264
+ notebook,
265
+ api_code,
266
+ ]
267
+ upload_convert_btn.click(
268
+ convert_upload_for_ui,
269
+ inputs=[sql_upload, sql_in, source, target],
270
+ outputs=upload_convert_outs,
271
+ )
272
+
273
+ with gr.Tab("Guide"):
274
+ gr.Markdown(
275
+ f"""
276
+ ## For AI / ML practitioners
277
+ MorphSQL is a **deterministic** SQL→pandas / PySpark codegen tool (not a chat LLM). Use it when you have
278
+ warehouse SQL for labels/features and want a Python frame for training or Spark jobs.
279
+
280
+ **Typical path**
281
+ 1. Convert feature SQL → pandas or PySpark
282
+ 2. Point `tables[...]` at parquet / `datasets` / warehouse extracts / Spark tables
283
+ 3. Feed `result` into sklearn, XGBoost, Transformers, or Spark ML
284
+
285
+ ## Why data scientists use this
286
+ Warehouse SQL often lives in BI tools. MorphSQL rewrites dialect quirks (NVL, ZEROIFNULL, dates)
287
+ into pandas or PySpark you can run in Jupyter / Colab / Databricks.
288
+
289
+ ## Recommended workflow
290
+ 1. Paste SQL, load an example, or **upload** a `.sql` / `.zip`
291
+ 2. Choose **Convert to** (pandas / PySpark / Snowflake / BigQuery / dbt)
292
+ 3. Click **Convert** or **Upload & Convert → Download**
293
+ 4. Download the `.py` / `.sql` / `.zip` to your machine and replace synthetic tables with real data
294
+
295
+ ## Output choices
296
+ | Convert to | Best for |
297
+ |---|---|
298
+ | **Python (pandas)** | Feature engineering, EDA, model training prep |
299
+ | **Python (PySpark)** | Large-scale Spark DataFrame transforms |
300
+ | **Snowflake / BigQuery SQL** | Keeping transforms in the warehouse |
301
+ | **dbt project** | Productionizing SQL into models |
302
+
303
+ ## More tab (Lab)
304
+ | Tool | Use when |
305
+ |---|---|
306
+ | **Object assess** | Need risk/complexity scoring for one SQL object |
307
+ | **Repository workbench** | Scanning a SQL repo (sample or zip) with lineage + validation |
308
+ | **ML feature SQL** | Migrating Vertica feature-engineering SQL → Snowflake/dbt |
309
+ | **Copilot** | Migration Q&A (keyword / optional HF LLM) |
310
+ | **Behavior notes / Eval** | Dialect quirks lookup and offline conversion scoring |
311
+
312
+ ```python
313
+ from morphsql.ai import pipeline
314
+ out = pipeline("sql-migration")(sql, source="snowflake", target="pandas")
315
+ # or target="pyspark"
316
+ # out["converted_sql"] → exec / save as features.py
317
+ ```
318
+
319
+ [Space]({SPACE_URL}) · [GitHub]({GITHUB_URL})
320
+ """
321
+ )
322
+
323
+ with gr.Tab("More"):
324
+ gr.Markdown(
325
+ "Lab extras beyond day-to-day Convert: assess objects, scan a repo, "
326
+ "migrate feature SQL, ask the copilot, or run offline eval."
327
+ )
328
+ report_state = gr.State(value=None)
329
+
330
+ with gr.Accordion("Object assess & convert", open=False):
331
+ gr.Markdown(
332
+ "Score complexity/risk and convert a single SQL object "
333
+ "(pandas, PySpark, warehouse SQL, or dbt)."
334
+ )
335
+ with gr.Row():
336
+ assess_source = gr.Dropdown(
337
+ choices=SOURCE_DROPDOWN, value="vertica", label="Source"
338
+ )
339
+ assess_target = gr.Dropdown(
340
+ choices=TARGET_DROPDOWN, value="snowflake", label="Target"
341
+ )
342
+ assess_btn = gr.Button("Assess & Convert", variant="secondary")
343
+ assess_sql = gr.Textbox(
344
+ label="SQL object",
345
+ value="SELECT customer_id, ZEROIFNULL(order_amount) AS order_amount "
346
+ "FROM staging.orders WHERE order_date >= CURRENT_DATE - 30",
347
+ lines=8,
348
+ )
349
+ assess_badge = gr.Textbox(label="Score", interactive=False, max_lines=1)
350
+ with gr.Row():
351
+ assess_analysis = gr.Markdown()
352
+ assess_risk = gr.HTML()
353
+ assess_out = gr.Textbox(label="Converted output", lines=10)
354
+ assess_notes = gr.Markdown()
355
+ assess_btn.click(
356
+ analyze_sql_object_ui,
357
+ [assess_sql, assess_source, assess_target],
358
+ [assess_analysis, assess_risk, assess_badge, assess_out, assess_notes],
359
+ )
360
+
361
+ with gr.Accordion("Repository workbench", open=False):
362
+ gr.Markdown(
363
+ "Scan the sample Vertica repo (or upload a `.zip`), then convert / "
364
+ "validate / preview dbt + lineage."
365
+ )
366
+ with gr.Row():
367
+ wb_source = gr.Dropdown(
368
+ choices=SOURCE_DROPDOWN, value="vertica", label="Source"
369
+ )
370
+ wb_target = gr.Dropdown(
371
+ choices=TARGET_DROPDOWN, value="snowflake", label="Target"
372
+ )
373
+ wb_sample = gr.Checkbox(value=True, label="Use sample repository")
374
+ wb_upload = gr.File(label="Or upload SQL repo (.zip)", file_types=[".zip"])
375
+ wb_btn = gr.Button("Run migration intelligence", variant="secondary")
376
+ wb_summary = gr.Markdown()
377
+ wb_metrics = gr.Markdown()
378
+ with gr.Row():
379
+ wb_risk = gr.HTML()
380
+ wb_dist = gr.HTML()
381
+ wb_objects = gr.Markdown()
382
+ wb_lineage = gr.HTML()
383
+ with gr.Row():
384
+ wb_rationalization = gr.Markdown()
385
+ wb_runbook = gr.Markdown()
386
+ with gr.Row():
387
+ wb_dbt = gr.Markdown()
388
+ wb_validation = gr.Markdown()
389
+ wb_export = gr.Code(language="json", lines=8, label="Export JSON")
390
+ wb_btn.click(
391
+ run_workbench_ui,
392
+ [wb_upload, wb_sample, wb_source, wb_target],
393
+ [
394
+ wb_summary,
395
+ wb_objects,
396
+ wb_rationalization,
397
+ wb_runbook,
398
+ wb_dbt,
399
+ wb_validation,
400
+ wb_metrics,
401
+ wb_risk,
402
+ wb_dist,
403
+ wb_lineage,
404
+ wb_export,
405
+ report_state,
406
+ ],
407
+ )
408
+
409
+ with gr.Accordion("ML feature SQL migration", open=False):
410
+ gr.Markdown(
411
+ "Convert `examples/ml_features/churn_feature_sql.sql` "
412
+ "(Vertica feature engineering) → Snowflake SQL or a dbt feature mart."
413
+ )
414
+ with gr.Row():
415
+ feat_target = gr.Dropdown(
416
+ choices=[
417
+ ("Snowflake SQL", "snowflake"),
418
+ ("dbt project (Snowflake)", "dbt-snowflake"),
419
+ ],
420
+ value="snowflake",
421
+ label="Output",
422
+ )
423
+ feat_btn = gr.Button("Migrate feature SQL", variant="secondary")
424
+ feat_md = gr.Markdown()
425
+ feat_out = gr.Textbox(label="Output", lines=14)
426
+ feat_btn.click(run_feature_migration, [feat_target], [feat_md, feat_out])
427
+
428
+ with gr.Accordion("Migration copilot", open=False):
429
+ gr.Markdown(
430
+ "Ask migration questions. Uses keyword/HF fallback guidance "
431
+ "(set `HF_TOKEN` for LLM replies)."
432
+ )
433
+ copilot = _chatbot(label="Copilot", height=320)
434
+ copilot_msg = gr.Textbox(
435
+ label="Message",
436
+ placeholder="e.g. How should I handle ZEROIFNULL on Snowflake?",
437
+ lines=2,
438
+ )
439
+ with gr.Row():
440
+ copilot_source = gr.Dropdown(
441
+ choices=SOURCE_DROPDOWN, value="vertica", label="Source"
442
+ )
443
+ copilot_target = gr.Dropdown(
444
+ choices=TARGET_DROPDOWN, value="snowflake", label="Target"
445
+ )
446
+ copilot_btn = gr.Button("Ask", variant="secondary")
447
+ copilot_sql = gr.Textbox(
448
+ label="Optional SQL context",
449
+ value="SELECT ZEROIFNULL(amount) FROM staging.transactions",
450
+ lines=3,
451
+ )
452
+ copilot_btn.click(
453
+ copilot_chat,
454
+ [
455
+ copilot_msg,
456
+ copilot,
457
+ report_state,
458
+ copilot_sql,
459
+ copilot_source,
460
+ copilot_target,
461
+ ],
462
+ [copilot, copilot_msg],
463
+ )
464
+ copilot_msg.submit(
465
+ copilot_chat,
466
+ [
467
+ copilot_msg,
468
+ copilot,
469
+ report_state,
470
+ copilot_sql,
471
+ copilot_source,
472
+ copilot_target,
473
+ ],
474
+ [copilot, copilot_msg],
475
+ )
476
+
477
+ with gr.Accordion("Dialect behavior notes", open=False):
478
+ rag_q = gr.Textbox(
479
+ label="Question",
480
+ value="Oracle empty string vs Snowflake NULL",
481
+ lines=2,
482
+ )
483
+ with gr.Row():
484
+ rag_source = gr.Dropdown(
485
+ choices=SOURCE_DROPDOWN, value="oracle", label="From"
486
+ )
487
+ rag_target = gr.Dropdown(
488
+ choices=[
489
+ ("Snowflake SQL", "snowflake"),
490
+ ("BigQuery SQL", "bigquery"),
491
+ ],
492
+ value="snowflake",
493
+ label="To",
494
+ )
495
+ rag_run = gr.Button("Search", variant="secondary")
496
+ rag_out = gr.Markdown()
497
+ rag_run.click(
498
+ run_behavior_rag,
499
+ [rag_q, rag_source, rag_target],
500
+ [rag_out],
501
+ )
502
+
503
+ with gr.Accordion("Offline conversion eval", open=False):
504
+ with gr.Row():
505
+ eval_limit = gr.Slider(10, 200, value=40, step=10, label="Pairs")
506
+ eval_run = gr.Button("Run eval", variant="secondary")
507
+ eval_summary = gr.Markdown()
508
+ eval_detail = gr.Markdown()
509
+ with gr.Row():
510
+ lb_name = gr.Textbox(label="Handle", value="hf-user")
511
+ lb_submit = gr.Button("Submit local score")
512
+ leaderboard_md = gr.Markdown(value=get_leaderboard_md())
513
+ eval_run.click(
514
+ run_eval_suite,
515
+ [eval_limit, eval_category],
516
+ [eval_summary, eval_detail, eval_state],
517
+ )
518
+ lb_submit.click(
519
+ submit_eval_score, [lb_name, eval_state], [leaderboard_md]
520
+ )
521
+
522
+ gr.Markdown(
523
+ f"<p class='footer-viral'>{__product_name__} v{__version__} · "
524
+ f"<a href='{SPACE_URL}'>Space</a> · "
525
+ f"<a href='{GITHUB_URL}'>GitHub</a> · Apache-2.0</p>"
526
+ )
527
+ return demo
528
+
529
+
530
+ demo = _build_demo()
531
+
532
+
533
+ if __name__ == "__main__":
534
+ demo.launch(**_launch_style_kwargs())
datasets/.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Keep generated leaderboard out of git; ship model weights
2
+ leaderboard.json
datasets/leaderboard.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "smoke-test",
4
+ "exact_match": 0.9333,
5
+ "token_f1": 0.9992,
6
+ "fuzzy": 0.9967,
7
+ "pass_rate": 1.0,
8
+ "n_pairs": 30,
9
+ "notes": "MorphSQL hybrid translator",
10
+ "ts": "2026-07-13T14:41:11.441415+00:00"
11
+ }
12
+ ]
datasets/vertica_snowflake_pairs.jsonl ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"id": "zeroifnull_basic", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(amount) AS amount FROM staging.orders", "target_sql": "SELECT COALESCE(amount, 0) AS amount FROM staging.orders", "notes": "ZEROIFNULL \u2192 COALESCE(expr, 0)"}
2
+ {"id": "nvl_basic", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT NVL(discount, 0) AS discount FROM staging.orders", "target_sql": "SELECT COALESCE(discount, 0) AS discount FROM staging.orders", "notes": "NVL \u2192 COALESCE"}
3
+ {"id": "date_sub_days", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM t WHERE order_date >= CURRENT_DATE - 30", "target_sql": "SELECT * FROM t WHERE order_date >= DATEADD(day, -30, CURRENT_DATE)", "notes": "Date arithmetic \u2192 DATEADD"}
4
+ {"id": "local_temp", "category": "ddl", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "CREATE LOCAL TEMP TABLE tmp_x ON COMMIT PRESERVE ROWS AS SELECT 1 AS id", "target_sql": "CREATE OR REPLACE TEMPORARY TABLE tmp_x AS SELECT 1 AS id", "notes": "LOCAL TEMP \u2192 TEMPORARY TABLE"}
5
+ {"id": "string_agg", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT STRING_AGG(name, ',') FROM users", "target_sql": "SELECT LISTAGG(name, ',') FROM users", "notes": "STRING_AGG \u2192 LISTAGG"}
6
+ {"id": "approx_distinct", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT APPROXIMATE_COUNT_DISTINCT(user_id) FROM events", "target_sql": "SELECT APPROX_COUNT_DISTINCT(user_id) FROM events", "notes": "APPROXIMATE_COUNT_DISTINCT \u2192 APPROX_COUNT_DISTINCT"}
7
+ {"id": "datediff_quoted", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT DATEDIFF('day', start_dt, end_dt) FROM t", "target_sql": "SELECT DATEDIFF(day, start_dt, end_dt) FROM t", "notes": "Quoted DATEDIFF unit \u2192 unquoted"}
8
+ {"id": "sysdate", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT SYSDATE FROM dual", "target_sql": "SELECT CURRENT_TIMESTAMP()", "notes": "SYSDATE + drop DUAL"}
9
+ {"id": "getdate_redshift", "category": "function", "source_dialect": "redshift", "target_dialect": "snowflake", "source_sql": "SELECT GETDATE(), NVL(x, 0) FROM t", "target_sql": "SELECT CURRENT_TIMESTAMP(), COALESCE(x, 0) FROM t", "notes": "GETDATE/NVL mappings"}
10
+ {"id": "bigquery_ifnull", "category": "function", "source_dialect": "bigquery", "target_dialect": "snowflake", "source_sql": "SELECT IFNULL(a, 0), STRING_AGG(b, ',') FROM t GROUP BY a", "target_sql": "SELECT COALESCE(a, 0), LISTAGG(b, ',') FROM t GROUP BY a", "notes": "IFNULL/STRING_AGG \u2192 COALESCE/LISTAGG"}
11
+ {"id": "feature_rfm", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT customer_id, ZEROIFNULL(SUM(order_amount)) AS monetary, COUNT(DISTINCT order_id) AS frequency, DATEDIFF('day', MAX(order_date), CURRENT_DATE) AS recency FROM staging.orders GROUP BY customer_id", "target_sql": "SELECT customer_id, COALESCE(SUM(order_amount), 0) AS monetary, COUNT(DISTINCT order_id) AS frequency, DATEDIFF(day, MAX(order_date), CURRENT_DATE) AS recency FROM staging.orders GROUP BY customer_id", "notes": "ML RFM feature SQL migration"}
12
+ {"id": "feature_label", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT user_id, CASE WHEN ZEROIFNULL(churn_score) > 0.7 THEN 1 ELSE 0 END AS churn_label FROM ml.user_scores", "target_sql": "SELECT user_id, CASE WHEN COALESCE(churn_score, 0) > 0.7 THEN 1 ELSE 0 END AS churn_label FROM ml.user_scores", "notes": "Training label feature SQL"}
13
+ {"id": "date_sub_7", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 7", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -7, CURRENT_DATE)", "notes": "CURRENT_DATE - 7 \u2192 DATEADD"}
14
+ {"id": "date_sub_14", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 14", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -14, CURRENT_DATE)", "notes": "CURRENT_DATE - 14 \u2192 DATEADD"}
15
+ {"id": "date_sub_30", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 30", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -30, CURRENT_DATE)", "notes": "CURRENT_DATE - 30 \u2192 DATEADD"}
16
+ {"id": "date_sub_60", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 60", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -60, CURRENT_DATE)", "notes": "CURRENT_DATE - 60 \u2192 DATEADD"}
17
+ {"id": "date_sub_90", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 90", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -90, CURRENT_DATE)", "notes": "CURRENT_DATE - 90 \u2192 DATEADD"}
18
+ {"id": "date_sub_180", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 180", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -180, CURRENT_DATE)", "notes": "CURRENT_DATE - 180 \u2192 DATEADD"}
19
+ {"id": "date_sub_365", "category": "date", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT * FROM events WHERE event_date >= CURRENT_DATE - 365", "target_sql": "SELECT * FROM events WHERE event_date >= DATEADD(day, -365, CURRENT_DATE)", "notes": "CURRENT_DATE - 365 \u2192 DATEADD"}
20
+ {"id": "zeroifnull_amount", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(amount) AS amount FROM staging.facts", "target_sql": "SELECT COALESCE(amount, 0) AS amount FROM staging.facts", "notes": "ZEROIFNULL expansion"}
21
+ {"id": "zeroifnull_revenue", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(revenue) AS revenue FROM staging.facts", "target_sql": "SELECT COALESCE(revenue, 0) AS revenue FROM staging.facts", "notes": "ZEROIFNULL expansion"}
22
+ {"id": "zeroifnull_fee", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(fee) AS fee FROM staging.facts", "target_sql": "SELECT COALESCE(fee, 0) AS fee FROM staging.facts", "notes": "ZEROIFNULL expansion"}
23
+ {"id": "zeroifnull_tax", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(tax) AS tax FROM staging.facts", "target_sql": "SELECT COALESCE(tax, 0) AS tax FROM staging.facts", "notes": "ZEROIFNULL expansion"}
24
+ {"id": "zeroifnull_balance", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(balance) AS balance FROM staging.facts", "target_sql": "SELECT COALESCE(balance, 0) AS balance FROM staging.facts", "notes": "ZEROIFNULL expansion"}
25
+ {"id": "zeroifnull_score", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score) AS score FROM staging.facts", "target_sql": "SELECT COALESCE(score, 0) AS score FROM staging.facts", "notes": "ZEROIFNULL expansion"}
26
+ {"id": "zeroifnull_qty", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(qty) AS qty FROM staging.facts", "target_sql": "SELECT COALESCE(qty, 0) AS qty FROM staging.facts", "notes": "ZEROIFNULL expansion"}
27
+ {"id": "agg_mix_1", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_1)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_1), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
28
+ {"id": "agg_mix_2", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_2)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_2), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
29
+ {"id": "agg_mix_3", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_3)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_3), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
30
+ {"id": "agg_mix_4", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_4)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_4), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
31
+ {"id": "agg_mix_5", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_5)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_5), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
32
+ {"id": "agg_mix_6", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_6)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_6), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
33
+ {"id": "agg_mix_7", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_7)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_7), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
34
+ {"id": "agg_mix_8", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_8)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_8), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
35
+ {"id": "agg_mix_9", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_9)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_9), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
36
+ {"id": "agg_mix_10", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_10)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_10), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
37
+ {"id": "agg_mix_11", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_11)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_11), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
38
+ {"id": "agg_mix_12", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_12)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_12), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
39
+ {"id": "agg_mix_13", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_13)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_13), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
40
+ {"id": "agg_mix_14", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_14)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_14), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
41
+ {"id": "agg_mix_15", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_15)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_15), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
42
+ {"id": "agg_mix_16", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_16)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_16), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
43
+ {"id": "agg_mix_17", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_17)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_17), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
44
+ {"id": "agg_mix_18", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_18)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_18), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
45
+ {"id": "agg_mix_19", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_19)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_19), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
46
+ {"id": "agg_mix_20", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_20)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_20), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
47
+ {"id": "agg_mix_21", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_21)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_21), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
48
+ {"id": "agg_mix_22", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_22)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_22), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
49
+ {"id": "agg_mix_23", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_23)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_23), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
50
+ {"id": "agg_mix_24", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_24)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_24), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
51
+ {"id": "agg_mix_25", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_25)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_25), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
52
+ {"id": "agg_mix_26", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_26)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_26), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
53
+ {"id": "agg_mix_27", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_27)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_27), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
54
+ {"id": "agg_mix_28", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_28)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_28), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
55
+ {"id": "agg_mix_29", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_29)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_29), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
56
+ {"id": "agg_mix_30", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_30)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_30), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
57
+ {"id": "agg_mix_31", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_31)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_31), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
58
+ {"id": "agg_mix_32", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_32)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_32), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
59
+ {"id": "agg_mix_33", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_33)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_33), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
60
+ {"id": "agg_mix_34", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_34)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_34), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
61
+ {"id": "agg_mix_35", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_35)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_35), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
62
+ {"id": "agg_mix_36", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_36)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_36), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
63
+ {"id": "agg_mix_37", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_37)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_37), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
64
+ {"id": "agg_mix_38", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_38)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_38), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
65
+ {"id": "agg_mix_39", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_39)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_39), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
66
+ {"id": "agg_mix_40", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_40)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_40), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
67
+ {"id": "agg_mix_41", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_41)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_41), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
68
+ {"id": "agg_mix_42", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_42)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_42), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
69
+ {"id": "agg_mix_43", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_43)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_43), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
70
+ {"id": "agg_mix_44", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_44)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_44), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
71
+ {"id": "agg_mix_45", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_45)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_45), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
72
+ {"id": "agg_mix_46", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_46)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_46), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
73
+ {"id": "agg_mix_47", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_47)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_47), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
74
+ {"id": "agg_mix_48", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_48)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_48), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
75
+ {"id": "agg_mix_49", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_49)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_49), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
76
+ {"id": "agg_mix_50", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_50)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_50), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
77
+ {"id": "agg_mix_51", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_51)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_51), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
78
+ {"id": "agg_mix_52", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_52)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_52), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
79
+ {"id": "agg_mix_53", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_53)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_53), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
80
+ {"id": "agg_mix_54", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_54)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_54), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
81
+ {"id": "agg_mix_55", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_55)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_55), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
82
+ {"id": "agg_mix_56", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_56)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_56), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
83
+ {"id": "agg_mix_57", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_57)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_57), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
84
+ {"id": "agg_mix_58", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_58)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_58), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
85
+ {"id": "agg_mix_59", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_59)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_59), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
86
+ {"id": "agg_mix_60", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_60)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_60), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
87
+ {"id": "agg_mix_61", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_61)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_61), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
88
+ {"id": "agg_mix_62", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_62)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_62), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
89
+ {"id": "agg_mix_63", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_63)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_63), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
90
+ {"id": "agg_mix_64", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_64)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_64), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
91
+ {"id": "agg_mix_65", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_65)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_65), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
92
+ {"id": "agg_mix_66", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_66)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_66), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
93
+ {"id": "agg_mix_67", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_67)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_67), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
94
+ {"id": "agg_mix_68", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_68)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_68), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
95
+ {"id": "agg_mix_69", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_69)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_69), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
96
+ {"id": "agg_mix_70", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_70)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_70), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
97
+ {"id": "agg_mix_71", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_71)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_71), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
98
+ {"id": "agg_mix_72", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_72)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_72), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
99
+ {"id": "agg_mix_73", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_73)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_73), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
100
+ {"id": "agg_mix_74", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_74)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_74), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
101
+ {"id": "agg_mix_75", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_75)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_75), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
102
+ {"id": "agg_mix_76", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_1, ZEROIFNULL(SUM(metric_76)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_1, COALESCE(SUM(metric_76), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
103
+ {"id": "agg_mix_77", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_2, ZEROIFNULL(SUM(metric_77)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_2, COALESCE(SUM(metric_77), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
104
+ {"id": "agg_mix_78", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_3, ZEROIFNULL(SUM(metric_78)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "target_sql": "SELECT dim_3, COALESCE(SUM(metric_78), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_0 GROUP BY 1", "notes": "Synthetic aggregate pair"}
105
+ {"id": "agg_mix_79", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_4, ZEROIFNULL(SUM(metric_79)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "target_sql": "SELECT dim_4, COALESCE(SUM(metric_79), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_1 GROUP BY 1", "notes": "Synthetic aggregate pair"}
106
+ {"id": "agg_mix_80", "category": "aggregate", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT dim_0, ZEROIFNULL(SUM(metric_80)), APPROXIMATE_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "target_sql": "SELECT dim_0, COALESCE(SUM(metric_80), 0), APPROX_COUNT_DISTINCT(user_id) FROM fact_2 GROUP BY 1", "notes": "Synthetic aggregate pair"}
107
+ {"id": "oracle_nvl_1", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_1, 0) FROM dual", "target_sql": "SELECT COALESCE(col_1, 0)", "notes": "Oracle NVL expansion"}
108
+ {"id": "oracle_nvl_2", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_2, 0) FROM dual", "target_sql": "SELECT COALESCE(col_2, 0)", "notes": "Oracle NVL expansion"}
109
+ {"id": "oracle_nvl_3", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_3, 0) FROM dual", "target_sql": "SELECT COALESCE(col_3, 0)", "notes": "Oracle NVL expansion"}
110
+ {"id": "oracle_nvl_4", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_4, 0) FROM dual", "target_sql": "SELECT COALESCE(col_4, 0)", "notes": "Oracle NVL expansion"}
111
+ {"id": "oracle_nvl_5", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_5, 0) FROM dual", "target_sql": "SELECT COALESCE(col_5, 0)", "notes": "Oracle NVL expansion"}
112
+ {"id": "oracle_nvl_6", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_6, 0) FROM dual", "target_sql": "SELECT COALESCE(col_6, 0)", "notes": "Oracle NVL expansion"}
113
+ {"id": "oracle_nvl_7", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_7, 0) FROM dual", "target_sql": "SELECT COALESCE(col_7, 0)", "notes": "Oracle NVL expansion"}
114
+ {"id": "oracle_nvl_8", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_8, 0) FROM dual", "target_sql": "SELECT COALESCE(col_8, 0)", "notes": "Oracle NVL expansion"}
115
+ {"id": "oracle_nvl_9", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_9, 0) FROM dual", "target_sql": "SELECT COALESCE(col_9, 0)", "notes": "Oracle NVL expansion"}
116
+ {"id": "oracle_nvl_10", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_10, 0) FROM dual", "target_sql": "SELECT COALESCE(col_10, 0)", "notes": "Oracle NVL expansion"}
117
+ {"id": "oracle_nvl_11", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_11, 0) FROM dual", "target_sql": "SELECT COALESCE(col_11, 0)", "notes": "Oracle NVL expansion"}
118
+ {"id": "oracle_nvl_12", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_12, 0) FROM dual", "target_sql": "SELECT COALESCE(col_12, 0)", "notes": "Oracle NVL expansion"}
119
+ {"id": "oracle_nvl_13", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_13, 0) FROM dual", "target_sql": "SELECT COALESCE(col_13, 0)", "notes": "Oracle NVL expansion"}
120
+ {"id": "oracle_nvl_14", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_14, 0) FROM dual", "target_sql": "SELECT COALESCE(col_14, 0)", "notes": "Oracle NVL expansion"}
121
+ {"id": "oracle_nvl_15", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_15, 0) FROM dual", "target_sql": "SELECT COALESCE(col_15, 0)", "notes": "Oracle NVL expansion"}
122
+ {"id": "oracle_nvl_16", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_16, 0) FROM dual", "target_sql": "SELECT COALESCE(col_16, 0)", "notes": "Oracle NVL expansion"}
123
+ {"id": "oracle_nvl_17", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_17, 0) FROM dual", "target_sql": "SELECT COALESCE(col_17, 0)", "notes": "Oracle NVL expansion"}
124
+ {"id": "oracle_nvl_18", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_18, 0) FROM dual", "target_sql": "SELECT COALESCE(col_18, 0)", "notes": "Oracle NVL expansion"}
125
+ {"id": "oracle_nvl_19", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_19, 0) FROM dual", "target_sql": "SELECT COALESCE(col_19, 0)", "notes": "Oracle NVL expansion"}
126
+ {"id": "oracle_nvl_20", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_20, 0) FROM dual", "target_sql": "SELECT COALESCE(col_20, 0)", "notes": "Oracle NVL expansion"}
127
+ {"id": "oracle_nvl_21", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_21, 0) FROM dual", "target_sql": "SELECT COALESCE(col_21, 0)", "notes": "Oracle NVL expansion"}
128
+ {"id": "oracle_nvl_22", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_22, 0) FROM dual", "target_sql": "SELECT COALESCE(col_22, 0)", "notes": "Oracle NVL expansion"}
129
+ {"id": "oracle_nvl_23", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_23, 0) FROM dual", "target_sql": "SELECT COALESCE(col_23, 0)", "notes": "Oracle NVL expansion"}
130
+ {"id": "oracle_nvl_24", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_24, 0) FROM dual", "target_sql": "SELECT COALESCE(col_24, 0)", "notes": "Oracle NVL expansion"}
131
+ {"id": "oracle_nvl_25", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_25, 0) FROM dual", "target_sql": "SELECT COALESCE(col_25, 0)", "notes": "Oracle NVL expansion"}
132
+ {"id": "oracle_nvl_26", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_26, 0) FROM dual", "target_sql": "SELECT COALESCE(col_26, 0)", "notes": "Oracle NVL expansion"}
133
+ {"id": "oracle_nvl_27", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_27, 0) FROM dual", "target_sql": "SELECT COALESCE(col_27, 0)", "notes": "Oracle NVL expansion"}
134
+ {"id": "oracle_nvl_28", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_28, 0) FROM dual", "target_sql": "SELECT COALESCE(col_28, 0)", "notes": "Oracle NVL expansion"}
135
+ {"id": "oracle_nvl_29", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_29, 0) FROM dual", "target_sql": "SELECT COALESCE(col_29, 0)", "notes": "Oracle NVL expansion"}
136
+ {"id": "oracle_nvl_30", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_30, 0) FROM dual", "target_sql": "SELECT COALESCE(col_30, 0)", "notes": "Oracle NVL expansion"}
137
+ {"id": "oracle_nvl_31", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_31, 0) FROM dual", "target_sql": "SELECT COALESCE(col_31, 0)", "notes": "Oracle NVL expansion"}
138
+ {"id": "oracle_nvl_32", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_32, 0) FROM dual", "target_sql": "SELECT COALESCE(col_32, 0)", "notes": "Oracle NVL expansion"}
139
+ {"id": "oracle_nvl_33", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_33, 0) FROM dual", "target_sql": "SELECT COALESCE(col_33, 0)", "notes": "Oracle NVL expansion"}
140
+ {"id": "oracle_nvl_34", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_34, 0) FROM dual", "target_sql": "SELECT COALESCE(col_34, 0)", "notes": "Oracle NVL expansion"}
141
+ {"id": "oracle_nvl_35", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_35, 0) FROM dual", "target_sql": "SELECT COALESCE(col_35, 0)", "notes": "Oracle NVL expansion"}
142
+ {"id": "oracle_nvl_36", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_36, 0) FROM dual", "target_sql": "SELECT COALESCE(col_36, 0)", "notes": "Oracle NVL expansion"}
143
+ {"id": "oracle_nvl_37", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_37, 0) FROM dual", "target_sql": "SELECT COALESCE(col_37, 0)", "notes": "Oracle NVL expansion"}
144
+ {"id": "oracle_nvl_38", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_38, 0) FROM dual", "target_sql": "SELECT COALESCE(col_38, 0)", "notes": "Oracle NVL expansion"}
145
+ {"id": "oracle_nvl_39", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_39, 0) FROM dual", "target_sql": "SELECT COALESCE(col_39, 0)", "notes": "Oracle NVL expansion"}
146
+ {"id": "oracle_nvl_40", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_40, 0) FROM dual", "target_sql": "SELECT COALESCE(col_40, 0)", "notes": "Oracle NVL expansion"}
147
+ {"id": "oracle_nvl_41", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_41, 0) FROM dual", "target_sql": "SELECT COALESCE(col_41, 0)", "notes": "Oracle NVL expansion"}
148
+ {"id": "oracle_nvl_42", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_42, 0) FROM dual", "target_sql": "SELECT COALESCE(col_42, 0)", "notes": "Oracle NVL expansion"}
149
+ {"id": "oracle_nvl_43", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_43, 0) FROM dual", "target_sql": "SELECT COALESCE(col_43, 0)", "notes": "Oracle NVL expansion"}
150
+ {"id": "oracle_nvl_44", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_44, 0) FROM dual", "target_sql": "SELECT COALESCE(col_44, 0)", "notes": "Oracle NVL expansion"}
151
+ {"id": "oracle_nvl_45", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_45, 0) FROM dual", "target_sql": "SELECT COALESCE(col_45, 0)", "notes": "Oracle NVL expansion"}
152
+ {"id": "oracle_nvl_46", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_46, 0) FROM dual", "target_sql": "SELECT COALESCE(col_46, 0)", "notes": "Oracle NVL expansion"}
153
+ {"id": "oracle_nvl_47", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_47, 0) FROM dual", "target_sql": "SELECT COALESCE(col_47, 0)", "notes": "Oracle NVL expansion"}
154
+ {"id": "oracle_nvl_48", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_48, 0) FROM dual", "target_sql": "SELECT COALESCE(col_48, 0)", "notes": "Oracle NVL expansion"}
155
+ {"id": "oracle_nvl_49", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_49, 0) FROM dual", "target_sql": "SELECT COALESCE(col_49, 0)", "notes": "Oracle NVL expansion"}
156
+ {"id": "oracle_nvl_50", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_50, 0) FROM dual", "target_sql": "SELECT COALESCE(col_50, 0)", "notes": "Oracle NVL expansion"}
157
+ {"id": "oracle_nvl_51", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_51, 0) FROM dual", "target_sql": "SELECT COALESCE(col_51, 0)", "notes": "Oracle NVL expansion"}
158
+ {"id": "oracle_nvl_52", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_52, 0) FROM dual", "target_sql": "SELECT COALESCE(col_52, 0)", "notes": "Oracle NVL expansion"}
159
+ {"id": "oracle_nvl_53", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_53, 0) FROM dual", "target_sql": "SELECT COALESCE(col_53, 0)", "notes": "Oracle NVL expansion"}
160
+ {"id": "oracle_nvl_54", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_54, 0) FROM dual", "target_sql": "SELECT COALESCE(col_54, 0)", "notes": "Oracle NVL expansion"}
161
+ {"id": "oracle_nvl_55", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_55, 0) FROM dual", "target_sql": "SELECT COALESCE(col_55, 0)", "notes": "Oracle NVL expansion"}
162
+ {"id": "oracle_nvl_56", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_56, 0) FROM dual", "target_sql": "SELECT COALESCE(col_56, 0)", "notes": "Oracle NVL expansion"}
163
+ {"id": "oracle_nvl_57", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_57, 0) FROM dual", "target_sql": "SELECT COALESCE(col_57, 0)", "notes": "Oracle NVL expansion"}
164
+ {"id": "oracle_nvl_58", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_58, 0) FROM dual", "target_sql": "SELECT COALESCE(col_58, 0)", "notes": "Oracle NVL expansion"}
165
+ {"id": "oracle_nvl_59", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_59, 0) FROM dual", "target_sql": "SELECT COALESCE(col_59, 0)", "notes": "Oracle NVL expansion"}
166
+ {"id": "oracle_nvl_60", "category": "function", "source_dialect": "oracle", "target_dialect": "snowflake", "source_sql": "SELECT NVL(col_60, 0) FROM dual", "target_sql": "SELECT COALESCE(col_60, 0)", "notes": "Oracle NVL expansion"}
167
+ {"id": "ml_feature_1", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_1)) AS f_1 FROM ml.training_window WHERE ds >= CURRENT_DATE - 8 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_1), 0) AS f_1 FROM ml.training_window WHERE ds >= DATEADD(day, -8, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
168
+ {"id": "ml_feature_2", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_2)) AS f_2 FROM ml.training_window WHERE ds >= CURRENT_DATE - 9 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_2), 0) AS f_2 FROM ml.training_window WHERE ds >= DATEADD(day, -9, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
169
+ {"id": "ml_feature_3", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_3)) AS f_3 FROM ml.training_window WHERE ds >= CURRENT_DATE - 10 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_3), 0) AS f_3 FROM ml.training_window WHERE ds >= DATEADD(day, -10, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
170
+ {"id": "ml_feature_4", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_4)) AS f_4 FROM ml.training_window WHERE ds >= CURRENT_DATE - 11 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_4), 0) AS f_4 FROM ml.training_window WHERE ds >= DATEADD(day, -11, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
171
+ {"id": "ml_feature_5", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_5)) AS f_5 FROM ml.training_window WHERE ds >= CURRENT_DATE - 12 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_5), 0) AS f_5 FROM ml.training_window WHERE ds >= DATEADD(day, -12, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
172
+ {"id": "ml_feature_6", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_6)) AS f_6 FROM ml.training_window WHERE ds >= CURRENT_DATE - 13 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_6), 0) AS f_6 FROM ml.training_window WHERE ds >= DATEADD(day, -13, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
173
+ {"id": "ml_feature_7", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_7)) AS f_7 FROM ml.training_window WHERE ds >= CURRENT_DATE - 14 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_7), 0) AS f_7 FROM ml.training_window WHERE ds >= DATEADD(day, -14, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
174
+ {"id": "ml_feature_8", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_8)) AS f_8 FROM ml.training_window WHERE ds >= CURRENT_DATE - 15 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_8), 0) AS f_8 FROM ml.training_window WHERE ds >= DATEADD(day, -15, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
175
+ {"id": "ml_feature_9", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_9)) AS f_9 FROM ml.training_window WHERE ds >= CURRENT_DATE - 16 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_9), 0) AS f_9 FROM ml.training_window WHERE ds >= DATEADD(day, -16, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
176
+ {"id": "ml_feature_10", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_10)) AS f_10 FROM ml.training_window WHERE ds >= CURRENT_DATE - 17 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_10), 0) AS f_10 FROM ml.training_window WHERE ds >= DATEADD(day, -17, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
177
+ {"id": "ml_feature_11", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_11)) AS f_11 FROM ml.training_window WHERE ds >= CURRENT_DATE - 18 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_11), 0) AS f_11 FROM ml.training_window WHERE ds >= DATEADD(day, -18, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
178
+ {"id": "ml_feature_12", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_12)) AS f_12 FROM ml.training_window WHERE ds >= CURRENT_DATE - 19 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_12), 0) AS f_12 FROM ml.training_window WHERE ds >= DATEADD(day, -19, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
179
+ {"id": "ml_feature_13", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_13)) AS f_13 FROM ml.training_window WHERE ds >= CURRENT_DATE - 20 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_13), 0) AS f_13 FROM ml.training_window WHERE ds >= DATEADD(day, -20, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
180
+ {"id": "ml_feature_14", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_14)) AS f_14 FROM ml.training_window WHERE ds >= CURRENT_DATE - 21 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_14), 0) AS f_14 FROM ml.training_window WHERE ds >= DATEADD(day, -21, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
181
+ {"id": "ml_feature_15", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_15)) AS f_15 FROM ml.training_window WHERE ds >= CURRENT_DATE - 22 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_15), 0) AS f_15 FROM ml.training_window WHERE ds >= DATEADD(day, -22, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
182
+ {"id": "ml_feature_16", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_16)) AS f_16 FROM ml.training_window WHERE ds >= CURRENT_DATE - 23 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_16), 0) AS f_16 FROM ml.training_window WHERE ds >= DATEADD(day, -23, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
183
+ {"id": "ml_feature_17", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_17)) AS f_17 FROM ml.training_window WHERE ds >= CURRENT_DATE - 24 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_17), 0) AS f_17 FROM ml.training_window WHERE ds >= DATEADD(day, -24, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
184
+ {"id": "ml_feature_18", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_18)) AS f_18 FROM ml.training_window WHERE ds >= CURRENT_DATE - 25 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_18), 0) AS f_18 FROM ml.training_window WHERE ds >= DATEADD(day, -25, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
185
+ {"id": "ml_feature_19", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_19)) AS f_19 FROM ml.training_window WHERE ds >= CURRENT_DATE - 26 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_19), 0) AS f_19 FROM ml.training_window WHERE ds >= DATEADD(day, -26, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
186
+ {"id": "ml_feature_20", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_20)) AS f_20 FROM ml.training_window WHERE ds >= CURRENT_DATE - 27 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_20), 0) AS f_20 FROM ml.training_window WHERE ds >= DATEADD(day, -27, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
187
+ {"id": "ml_feature_21", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_21)) AS f_21 FROM ml.training_window WHERE ds >= CURRENT_DATE - 28 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_21), 0) AS f_21 FROM ml.training_window WHERE ds >= DATEADD(day, -28, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
188
+ {"id": "ml_feature_22", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_22)) AS f_22 FROM ml.training_window WHERE ds >= CURRENT_DATE - 29 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_22), 0) AS f_22 FROM ml.training_window WHERE ds >= DATEADD(day, -29, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
189
+ {"id": "ml_feature_23", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_23)) AS f_23 FROM ml.training_window WHERE ds >= CURRENT_DATE - 30 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_23), 0) AS f_23 FROM ml.training_window WHERE ds >= DATEADD(day, -30, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
190
+ {"id": "ml_feature_24", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_24)) AS f_24 FROM ml.training_window WHERE ds >= CURRENT_DATE - 31 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_24), 0) AS f_24 FROM ml.training_window WHERE ds >= DATEADD(day, -31, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
191
+ {"id": "ml_feature_25", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_25)) AS f_25 FROM ml.training_window WHERE ds >= CURRENT_DATE - 32 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_25), 0) AS f_25 FROM ml.training_window WHERE ds >= DATEADD(day, -32, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
192
+ {"id": "ml_feature_26", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_26)) AS f_26 FROM ml.training_window WHERE ds >= CURRENT_DATE - 33 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_26), 0) AS f_26 FROM ml.training_window WHERE ds >= DATEADD(day, -33, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
193
+ {"id": "ml_feature_27", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_27)) AS f_27 FROM ml.training_window WHERE ds >= CURRENT_DATE - 34 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_27), 0) AS f_27 FROM ml.training_window WHERE ds >= DATEADD(day, -34, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
194
+ {"id": "ml_feature_28", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_28)) AS f_28 FROM ml.training_window WHERE ds >= CURRENT_DATE - 35 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_28), 0) AS f_28 FROM ml.training_window WHERE ds >= DATEADD(day, -35, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
195
+ {"id": "ml_feature_29", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_29)) AS f_29 FROM ml.training_window WHERE ds >= CURRENT_DATE - 36 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_29), 0) AS f_29 FROM ml.training_window WHERE ds >= DATEADD(day, -36, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
196
+ {"id": "ml_feature_30", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_30)) AS f_30 FROM ml.training_window WHERE ds >= CURRENT_DATE - 37 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_30), 0) AS f_30 FROM ml.training_window WHERE ds >= DATEADD(day, -37, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
197
+ {"id": "ml_feature_31", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_31)) AS f_31 FROM ml.training_window WHERE ds >= CURRENT_DATE - 38 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_31), 0) AS f_31 FROM ml.training_window WHERE ds >= DATEADD(day, -38, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
198
+ {"id": "ml_feature_32", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_32)) AS f_32 FROM ml.training_window WHERE ds >= CURRENT_DATE - 39 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_32), 0) AS f_32 FROM ml.training_window WHERE ds >= DATEADD(day, -39, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
199
+ {"id": "ml_feature_33", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_33)) AS f_33 FROM ml.training_window WHERE ds >= CURRENT_DATE - 40 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_33), 0) AS f_33 FROM ml.training_window WHERE ds >= DATEADD(day, -40, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
200
+ {"id": "ml_feature_34", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_34)) AS f_34 FROM ml.training_window WHERE ds >= CURRENT_DATE - 41 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_34), 0) AS f_34 FROM ml.training_window WHERE ds >= DATEADD(day, -41, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
201
+ {"id": "ml_feature_35", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_35)) AS f_35 FROM ml.training_window WHERE ds >= CURRENT_DATE - 42 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_35), 0) AS f_35 FROM ml.training_window WHERE ds >= DATEADD(day, -42, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
202
+ {"id": "ml_feature_36", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_36)) AS f_36 FROM ml.training_window WHERE ds >= CURRENT_DATE - 43 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_36), 0) AS f_36 FROM ml.training_window WHERE ds >= DATEADD(day, -43, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
203
+ {"id": "ml_feature_37", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_37)) AS f_37 FROM ml.training_window WHERE ds >= CURRENT_DATE - 44 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_37), 0) AS f_37 FROM ml.training_window WHERE ds >= DATEADD(day, -44, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
204
+ {"id": "ml_feature_38", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_38)) AS f_38 FROM ml.training_window WHERE ds >= CURRENT_DATE - 45 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_38), 0) AS f_38 FROM ml.training_window WHERE ds >= DATEADD(day, -45, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
205
+ {"id": "ml_feature_39", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_39)) AS f_39 FROM ml.training_window WHERE ds >= CURRENT_DATE - 46 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_39), 0) AS f_39 FROM ml.training_window WHERE ds >= DATEADD(day, -46, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
206
+ {"id": "ml_feature_40", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_40)) AS f_40 FROM ml.training_window WHERE ds >= CURRENT_DATE - 47 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_40), 0) AS f_40 FROM ml.training_window WHERE ds >= DATEADD(day, -47, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
207
+ {"id": "ml_feature_41", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_41)) AS f_41 FROM ml.training_window WHERE ds >= CURRENT_DATE - 48 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_41), 0) AS f_41 FROM ml.training_window WHERE ds >= DATEADD(day, -48, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
208
+ {"id": "ml_feature_42", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_42)) AS f_42 FROM ml.training_window WHERE ds >= CURRENT_DATE - 49 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_42), 0) AS f_42 FROM ml.training_window WHERE ds >= DATEADD(day, -49, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
209
+ {"id": "ml_feature_43", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_43)) AS f_43 FROM ml.training_window WHERE ds >= CURRENT_DATE - 50 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_43), 0) AS f_43 FROM ml.training_window WHERE ds >= DATEADD(day, -50, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
210
+ {"id": "ml_feature_44", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_44)) AS f_44 FROM ml.training_window WHERE ds >= CURRENT_DATE - 51 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_44), 0) AS f_44 FROM ml.training_window WHERE ds >= DATEADD(day, -51, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
211
+ {"id": "ml_feature_45", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_45)) AS f_45 FROM ml.training_window WHERE ds >= CURRENT_DATE - 52 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_45), 0) AS f_45 FROM ml.training_window WHERE ds >= DATEADD(day, -52, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
212
+ {"id": "ml_feature_46", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_46)) AS f_46 FROM ml.training_window WHERE ds >= CURRENT_DATE - 53 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_46), 0) AS f_46 FROM ml.training_window WHERE ds >= DATEADD(day, -53, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
213
+ {"id": "ml_feature_47", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_47)) AS f_47 FROM ml.training_window WHERE ds >= CURRENT_DATE - 54 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_47), 0) AS f_47 FROM ml.training_window WHERE ds >= DATEADD(day, -54, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
214
+ {"id": "ml_feature_48", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_48)) AS f_48 FROM ml.training_window WHERE ds >= CURRENT_DATE - 55 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_48), 0) AS f_48 FROM ml.training_window WHERE ds >= DATEADD(day, -55, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
215
+ {"id": "ml_feature_49", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_49)) AS f_49 FROM ml.training_window WHERE ds >= CURRENT_DATE - 56 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_49), 0) AS f_49 FROM ml.training_window WHERE ds >= DATEADD(day, -56, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
216
+ {"id": "ml_feature_50", "category": "ml_feature", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT entity_id, ZEROIFNULL(AVG(feature_50)) AS f_50 FROM ml.training_window WHERE ds >= CURRENT_DATE - 57 GROUP BY 1", "target_sql": "SELECT entity_id, COALESCE(AVG(feature_50), 0) AS f_50 FROM ml.training_window WHERE ds >= DATEADD(day, -57, CURRENT_DATE) GROUP BY 1", "notes": "ML feature window SQL"}
217
+ {"id": "ai_codegen_1", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_1), NVL(flag_1, 0) FROM ml.inference_logs WHERE run_id = 1", "target_sql": "SELECT COALESCE(score_1, 0), COALESCE(flag_1, 0) FROM ml.inference_logs WHERE run_id = 1", "notes": "AI inference-log SQL pair"}
218
+ {"id": "ai_codegen_2", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_2), NVL(flag_2, 0) FROM ml.inference_logs WHERE run_id = 2", "target_sql": "SELECT COALESCE(score_2, 0), COALESCE(flag_2, 0) FROM ml.inference_logs WHERE run_id = 2", "notes": "AI inference-log SQL pair"}
219
+ {"id": "ai_codegen_3", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_3), NVL(flag_3, 0) FROM ml.inference_logs WHERE run_id = 3", "target_sql": "SELECT COALESCE(score_3, 0), COALESCE(flag_3, 0) FROM ml.inference_logs WHERE run_id = 3", "notes": "AI inference-log SQL pair"}
220
+ {"id": "ai_codegen_4", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_4), NVL(flag_4, 0) FROM ml.inference_logs WHERE run_id = 4", "target_sql": "SELECT COALESCE(score_4, 0), COALESCE(flag_4, 0) FROM ml.inference_logs WHERE run_id = 4", "notes": "AI inference-log SQL pair"}
221
+ {"id": "ai_codegen_5", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_5), NVL(flag_5, 0) FROM ml.inference_logs WHERE run_id = 5", "target_sql": "SELECT COALESCE(score_5, 0), COALESCE(flag_5, 0) FROM ml.inference_logs WHERE run_id = 5", "notes": "AI inference-log SQL pair"}
222
+ {"id": "ai_codegen_6", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_6), NVL(flag_6, 0) FROM ml.inference_logs WHERE run_id = 6", "target_sql": "SELECT COALESCE(score_6, 0), COALESCE(flag_6, 0) FROM ml.inference_logs WHERE run_id = 6", "notes": "AI inference-log SQL pair"}
223
+ {"id": "ai_codegen_7", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_7), NVL(flag_7, 0) FROM ml.inference_logs WHERE run_id = 7", "target_sql": "SELECT COALESCE(score_7, 0), COALESCE(flag_7, 0) FROM ml.inference_logs WHERE run_id = 7", "notes": "AI inference-log SQL pair"}
224
+ {"id": "ai_codegen_8", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_8), NVL(flag_8, 0) FROM ml.inference_logs WHERE run_id = 8", "target_sql": "SELECT COALESCE(score_8, 0), COALESCE(flag_8, 0) FROM ml.inference_logs WHERE run_id = 8", "notes": "AI inference-log SQL pair"}
225
+ {"id": "ai_codegen_9", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_9), NVL(flag_9, 0) FROM ml.inference_logs WHERE run_id = 9", "target_sql": "SELECT COALESCE(score_9, 0), COALESCE(flag_9, 0) FROM ml.inference_logs WHERE run_id = 9", "notes": "AI inference-log SQL pair"}
226
+ {"id": "ai_codegen_10", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_10), NVL(flag_10, 0) FROM ml.inference_logs WHERE run_id = 10", "target_sql": "SELECT COALESCE(score_10, 0), COALESCE(flag_10, 0) FROM ml.inference_logs WHERE run_id = 10", "notes": "AI inference-log SQL pair"}
227
+ {"id": "ai_codegen_11", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_11), NVL(flag_11, 0) FROM ml.inference_logs WHERE run_id = 11", "target_sql": "SELECT COALESCE(score_11, 0), COALESCE(flag_11, 0) FROM ml.inference_logs WHERE run_id = 11", "notes": "AI inference-log SQL pair"}
228
+ {"id": "ai_codegen_12", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_12), NVL(flag_12, 0) FROM ml.inference_logs WHERE run_id = 12", "target_sql": "SELECT COALESCE(score_12, 0), COALESCE(flag_12, 0) FROM ml.inference_logs WHERE run_id = 12", "notes": "AI inference-log SQL pair"}
229
+ {"id": "ai_codegen_13", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_13), NVL(flag_13, 0) FROM ml.inference_logs WHERE run_id = 13", "target_sql": "SELECT COALESCE(score_13, 0), COALESCE(flag_13, 0) FROM ml.inference_logs WHERE run_id = 13", "notes": "AI inference-log SQL pair"}
230
+ {"id": "ai_codegen_14", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_14), NVL(flag_14, 0) FROM ml.inference_logs WHERE run_id = 14", "target_sql": "SELECT COALESCE(score_14, 0), COALESCE(flag_14, 0) FROM ml.inference_logs WHERE run_id = 14", "notes": "AI inference-log SQL pair"}
231
+ {"id": "ai_codegen_15", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_15), NVL(flag_15, 0) FROM ml.inference_logs WHERE run_id = 15", "target_sql": "SELECT COALESCE(score_15, 0), COALESCE(flag_15, 0) FROM ml.inference_logs WHERE run_id = 15", "notes": "AI inference-log SQL pair"}
232
+ {"id": "ai_codegen_16", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_16), NVL(flag_16, 0) FROM ml.inference_logs WHERE run_id = 16", "target_sql": "SELECT COALESCE(score_16, 0), COALESCE(flag_16, 0) FROM ml.inference_logs WHERE run_id = 16", "notes": "AI inference-log SQL pair"}
233
+ {"id": "ai_codegen_17", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_17), NVL(flag_17, 0) FROM ml.inference_logs WHERE run_id = 17", "target_sql": "SELECT COALESCE(score_17, 0), COALESCE(flag_17, 0) FROM ml.inference_logs WHERE run_id = 17", "notes": "AI inference-log SQL pair"}
234
+ {"id": "ai_codegen_18", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_18), NVL(flag_18, 0) FROM ml.inference_logs WHERE run_id = 18", "target_sql": "SELECT COALESCE(score_18, 0), COALESCE(flag_18, 0) FROM ml.inference_logs WHERE run_id = 18", "notes": "AI inference-log SQL pair"}
235
+ {"id": "ai_codegen_19", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_19), NVL(flag_19, 0) FROM ml.inference_logs WHERE run_id = 19", "target_sql": "SELECT COALESCE(score_19, 0), COALESCE(flag_19, 0) FROM ml.inference_logs WHERE run_id = 19", "notes": "AI inference-log SQL pair"}
236
+ {"id": "ai_codegen_20", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_20), NVL(flag_20, 0) FROM ml.inference_logs WHERE run_id = 20", "target_sql": "SELECT COALESCE(score_20, 0), COALESCE(flag_20, 0) FROM ml.inference_logs WHERE run_id = 20", "notes": "AI inference-log SQL pair"}
237
+ {"id": "ai_codegen_21", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_21), NVL(flag_21, 0) FROM ml.inference_logs WHERE run_id = 21", "target_sql": "SELECT COALESCE(score_21, 0), COALESCE(flag_21, 0) FROM ml.inference_logs WHERE run_id = 21", "notes": "AI inference-log SQL pair"}
238
+ {"id": "ai_codegen_22", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_22), NVL(flag_22, 0) FROM ml.inference_logs WHERE run_id = 22", "target_sql": "SELECT COALESCE(score_22, 0), COALESCE(flag_22, 0) FROM ml.inference_logs WHERE run_id = 22", "notes": "AI inference-log SQL pair"}
239
+ {"id": "ai_codegen_23", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_23), NVL(flag_23, 0) FROM ml.inference_logs WHERE run_id = 23", "target_sql": "SELECT COALESCE(score_23, 0), COALESCE(flag_23, 0) FROM ml.inference_logs WHERE run_id = 23", "notes": "AI inference-log SQL pair"}
240
+ {"id": "ai_codegen_24", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_24), NVL(flag_24, 0) FROM ml.inference_logs WHERE run_id = 24", "target_sql": "SELECT COALESCE(score_24, 0), COALESCE(flag_24, 0) FROM ml.inference_logs WHERE run_id = 24", "notes": "AI inference-log SQL pair"}
241
+ {"id": "ai_codegen_25", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_25), NVL(flag_25, 0) FROM ml.inference_logs WHERE run_id = 25", "target_sql": "SELECT COALESCE(score_25, 0), COALESCE(flag_25, 0) FROM ml.inference_logs WHERE run_id = 25", "notes": "AI inference-log SQL pair"}
242
+ {"id": "ai_codegen_26", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_26), NVL(flag_26, 0) FROM ml.inference_logs WHERE run_id = 26", "target_sql": "SELECT COALESCE(score_26, 0), COALESCE(flag_26, 0) FROM ml.inference_logs WHERE run_id = 26", "notes": "AI inference-log SQL pair"}
243
+ {"id": "ai_codegen_27", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_27), NVL(flag_27, 0) FROM ml.inference_logs WHERE run_id = 27", "target_sql": "SELECT COALESCE(score_27, 0), COALESCE(flag_27, 0) FROM ml.inference_logs WHERE run_id = 27", "notes": "AI inference-log SQL pair"}
244
+ {"id": "ai_codegen_28", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_28), NVL(flag_28, 0) FROM ml.inference_logs WHERE run_id = 28", "target_sql": "SELECT COALESCE(score_28, 0), COALESCE(flag_28, 0) FROM ml.inference_logs WHERE run_id = 28", "notes": "AI inference-log SQL pair"}
245
+ {"id": "ai_codegen_29", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_29), NVL(flag_29, 0) FROM ml.inference_logs WHERE run_id = 29", "target_sql": "SELECT COALESCE(score_29, 0), COALESCE(flag_29, 0) FROM ml.inference_logs WHERE run_id = 29", "notes": "AI inference-log SQL pair"}
246
+ {"id": "ai_codegen_30", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_30), NVL(flag_30, 0) FROM ml.inference_logs WHERE run_id = 30", "target_sql": "SELECT COALESCE(score_30, 0), COALESCE(flag_30, 0) FROM ml.inference_logs WHERE run_id = 30", "notes": "AI inference-log SQL pair"}
247
+ {"id": "ai_codegen_31", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_31), NVL(flag_31, 0) FROM ml.inference_logs WHERE run_id = 31", "target_sql": "SELECT COALESCE(score_31, 0), COALESCE(flag_31, 0) FROM ml.inference_logs WHERE run_id = 31", "notes": "AI inference-log SQL pair"}
248
+ {"id": "ai_codegen_32", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_32), NVL(flag_32, 0) FROM ml.inference_logs WHERE run_id = 32", "target_sql": "SELECT COALESCE(score_32, 0), COALESCE(flag_32, 0) FROM ml.inference_logs WHERE run_id = 32", "notes": "AI inference-log SQL pair"}
249
+ {"id": "ai_codegen_33", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_33), NVL(flag_33, 0) FROM ml.inference_logs WHERE run_id = 33", "target_sql": "SELECT COALESCE(score_33, 0), COALESCE(flag_33, 0) FROM ml.inference_logs WHERE run_id = 33", "notes": "AI inference-log SQL pair"}
250
+ {"id": "ai_codegen_34", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_34), NVL(flag_34, 0) FROM ml.inference_logs WHERE run_id = 34", "target_sql": "SELECT COALESCE(score_34, 0), COALESCE(flag_34, 0) FROM ml.inference_logs WHERE run_id = 34", "notes": "AI inference-log SQL pair"}
251
+ {"id": "ai_codegen_35", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_35), NVL(flag_35, 0) FROM ml.inference_logs WHERE run_id = 35", "target_sql": "SELECT COALESCE(score_35, 0), COALESCE(flag_35, 0) FROM ml.inference_logs WHERE run_id = 35", "notes": "AI inference-log SQL pair"}
252
+ {"id": "ai_codegen_36", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_36), NVL(flag_36, 0) FROM ml.inference_logs WHERE run_id = 36", "target_sql": "SELECT COALESCE(score_36, 0), COALESCE(flag_36, 0) FROM ml.inference_logs WHERE run_id = 36", "notes": "AI inference-log SQL pair"}
253
+ {"id": "ai_codegen_37", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_37), NVL(flag_37, 0) FROM ml.inference_logs WHERE run_id = 37", "target_sql": "SELECT COALESCE(score_37, 0), COALESCE(flag_37, 0) FROM ml.inference_logs WHERE run_id = 37", "notes": "AI inference-log SQL pair"}
254
+ {"id": "ai_codegen_38", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_38), NVL(flag_38, 0) FROM ml.inference_logs WHERE run_id = 38", "target_sql": "SELECT COALESCE(score_38, 0), COALESCE(flag_38, 0) FROM ml.inference_logs WHERE run_id = 38", "notes": "AI inference-log SQL pair"}
255
+ {"id": "ai_codegen_39", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_39), NVL(flag_39, 0) FROM ml.inference_logs WHERE run_id = 39", "target_sql": "SELECT COALESCE(score_39, 0), COALESCE(flag_39, 0) FROM ml.inference_logs WHERE run_id = 39", "notes": "AI inference-log SQL pair"}
256
+ {"id": "ai_codegen_40", "category": "function", "source_dialect": "vertica", "target_dialect": "snowflake", "source_sql": "SELECT ZEROIFNULL(score_40), NVL(flag_40, 0) FROM ml.inference_logs WHERE run_id = 40", "target_sql": "SELECT COALESCE(score_40, 0), COALESCE(flag_40, 0) FROM ml.inference_logs WHERE run_id = 40", "notes": "AI inference-log SQL pair"}
demo/__init__.py ADDED
File without changes
demo/handlers.py ADDED
@@ -0,0 +1,1376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio event handlers for MorphSQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import tempfile
8
+ import zipfile
9
+ from pathlib import Path
10
+
11
+ import plotly.graph_objects as go
12
+
13
+ from morphsql.assistant.copilot import MigrationCopilot
14
+ from morphsql.dbt_generator.decomposer import decompose_to_dbt, format_dbt_project, is_dbt_target
15
+ from morphsql.intelligence.lineage_viz import lineage_to_plotly
16
+ from morphsql.intelligence.rationalization import generate_rationalization
17
+ from morphsql.intelligence.runbook import generate_executive_summary, generate_runbook
18
+ from morphsql.lineage.builder import build_lineage_graph
19
+ from morphsql.models import Dialect, MigrationObject, MigrationReport, ObjectType
20
+ from morphsql.pipeline import MigrationPipeline
21
+ from morphsql.translator.engine import translate_sql
22
+ from morphsql.translator.pandas_codegen import is_pandas_target
23
+ from morphsql.translator.pyspark_codegen import is_pyspark_target
24
+ from demo.theme import C_MUTED, C_PANEL, C_TEXT
25
+
26
+ EXAMPLES_DIR = Path(__file__).parent.parent / "examples" / "vertica_legacy"
27
+ _copilot = MigrationCopilot()
28
+
29
+
30
+ def _sanitize_project(name: str) -> str:
31
+ return re.sub(r"[^a-zA-Z0-9_]", "_", name.lower()).strip("_") or "migration_project"
32
+
33
+
34
+ def _apply_dark_layout(fig: go.Figure, height: int = 260) -> go.Figure:
35
+ fig.update_layout(
36
+ template="plotly_dark",
37
+ height=height,
38
+ margin=dict(t=40, b=40, l=30, r=20),
39
+ paper_bgcolor=C_PANEL,
40
+ plot_bgcolor=C_PANEL,
41
+ font={"color": C_TEXT, "family": "Inter, system-ui, sans-serif"},
42
+ xaxis={"gridcolor": "#334155", "zerolinecolor": "#334155", "color": C_TEXT},
43
+ yaxis={"gridcolor": "#334155", "zerolinecolor": "#334155", "color": C_TEXT},
44
+ )
45
+ return fig
46
+
47
+
48
+ def _risk_gauge(score: int) -> go.Figure:
49
+ score = max(0, min(100, int(score or 0)))
50
+ color = "#22c55e" if score < 30 else "#eab308" if score < 60 else "#ef4444"
51
+ label = "Low" if score < 30 else "Medium" if score < 60 else "High"
52
+ fig = go.Figure(
53
+ go.Indicator(
54
+ mode="gauge+number",
55
+ value=score,
56
+ number={"suffix": " / 100", "font": {"color": C_TEXT, "size": 28}},
57
+ title={"text": f"Portfolio risk · {label}", "font": {"color": C_MUTED, "size": 13}},
58
+ gauge={
59
+ "axis": {"range": [0, 100], "tickcolor": C_MUTED, "tickfont": {"color": C_MUTED}},
60
+ "bar": {"color": color, "thickness": 0.8},
61
+ "bgcolor": "#0f172a",
62
+ "bordercolor": "#334155",
63
+ "steps": [
64
+ {"range": [0, 30], "color": "#14532d"},
65
+ {"range": [30, 60], "color": "#713f12"},
66
+ {"range": [60, 100], "color": "#7f1d1d"},
67
+ ],
68
+ },
69
+ )
70
+ )
71
+ return _apply_dark_layout(fig, height=280)
72
+
73
+
74
+ def _distribution_chart(report: MigrationReport) -> go.Figure:
75
+ d = report.dashboard
76
+ fig = go.Figure(
77
+ go.Bar(
78
+ x=["Auto-migrate", "Review", "Redesign", "Retire"],
79
+ y=[
80
+ d.auto_migratable,
81
+ d.requires_review,
82
+ d.requires_redesign,
83
+ d.recommended_retirement,
84
+ ],
85
+ marker_color=["#22c55e", "#eab308", "#ef4444", "#64748b"],
86
+ text=[
87
+ d.auto_migratable,
88
+ d.requires_review,
89
+ d.requires_redesign,
90
+ d.recommended_retirement,
91
+ ],
92
+ textposition="outside",
93
+ textfont={"color": C_TEXT, "size": 14},
94
+ )
95
+ )
96
+ fig.update_layout(title={"text": "Object distribution", "font": {"color": C_MUTED, "size": 13}})
97
+ return _apply_dark_layout(fig, height=280)
98
+
99
+
100
+ def _distribution_chart_empty() -> go.Figure:
101
+ fig = go.Figure(
102
+ go.Bar(
103
+ x=["Auto-migrate", "Review", "Redesign", "Retire"],
104
+ y=[0, 0, 0, 0],
105
+ marker_color=["#22c55e", "#eab308", "#ef4444", "#64748b"],
106
+ text=[0, 0, 0, 0],
107
+ textposition="outside",
108
+ textfont={"color": C_TEXT},
109
+ )
110
+ )
111
+ fig.update_layout(
112
+ title={"text": "Object distribution (run scan)", "font": {"color": C_MUTED, "size": 13}},
113
+ yaxis={"range": [0, 5]},
114
+ )
115
+ return _apply_dark_layout(fig, height=280)
116
+
117
+
118
+ def figure_to_html(fig: go.Figure) -> str:
119
+ """Reliable Plotly HTML embed for Gradio (avoids blank Plot widgets)."""
120
+ return fig.to_html(
121
+ include_plotlyjs="cdn",
122
+ full_html=False,
123
+ config={"displayModeBar": False, "responsive": True},
124
+ )
125
+
126
+
127
+ def empty_lineage_plot() -> go.Figure:
128
+ return lineage_to_plotly(build_lineage_graph([], Dialect.VERTICA))
129
+
130
+
131
+ def _resolve_repo_path(upload_file, use_sample: bool) -> Path | None:
132
+ if use_sample:
133
+ return EXAMPLES_DIR if EXAMPLES_DIR.exists() else None
134
+ if not upload_file:
135
+ return None
136
+
137
+ # Gradio File may return str path, Path, or tempfile-like object
138
+ if isinstance(upload_file, (list, tuple)) and upload_file:
139
+ upload_file = upload_file[0]
140
+ path = Path(str(upload_file.name if hasattr(upload_file, "name") else upload_file))
141
+ if not path.exists():
142
+ return None
143
+ if path.suffix.lower() == ".zip":
144
+ tmp = Path(tempfile.mkdtemp(prefix="morphsql_"))
145
+ with zipfile.ZipFile(path) as zf:
146
+ zf.extractall(tmp)
147
+ return tmp
148
+ if path.is_dir():
149
+ return path
150
+ return None
151
+
152
+
153
+ def _report_to_dict(report: MigrationReport | None) -> dict | None:
154
+ if report is None:
155
+ return None
156
+ return report.model_dump(mode="json")
157
+
158
+
159
+ def _dict_to_report(data: dict | MigrationReport | None) -> MigrationReport | None:
160
+ if data is None:
161
+ return None
162
+ if isinstance(data, MigrationReport):
163
+ return data
164
+ if isinstance(data, dict):
165
+ try:
166
+ return MigrationReport.model_validate(data)
167
+ except Exception:
168
+ return None
169
+ return None
170
+
171
+
172
+ def run_migration_workbench(
173
+ upload_file,
174
+ use_sample: bool,
175
+ source: str,
176
+ target: str,
177
+ ) -> tuple:
178
+ """Full migration intelligence pipeline for workbench tab."""
179
+ empty_msg = (
180
+ "Enable **Use sample repository** or upload a zip file, "
181
+ "then click **Run migration intelligence**."
182
+ )
183
+ empty_risk = _risk_gauge(0)
184
+ empty_dist = _distribution_chart_empty()
185
+ empty_lineage = empty_lineage_plot()
186
+ empty = (
187
+ empty_msg,
188
+ "No objects yet.",
189
+ "No rationalization yet.",
190
+ "No runbook yet.",
191
+ "No dbt preview yet.",
192
+ "No validation results yet.",
193
+ "Portfolio risk: —",
194
+ empty_risk,
195
+ empty_dist,
196
+ empty_lineage,
197
+ json.dumps({"status": "waiting_for_input"}, indent=2),
198
+ None,
199
+ )
200
+
201
+ try:
202
+ repo_path = _resolve_repo_path(upload_file, bool(use_sample))
203
+ except Exception as exc:
204
+ return (
205
+ f"Failed to open repository: {exc}",
206
+ *empty[1:],
207
+ )
208
+
209
+ if repo_path is None:
210
+ return empty
211
+
212
+ try:
213
+ source_d = Dialect(source)
214
+ wants_dbt = is_dbt_target(target)
215
+ if wants_dbt:
216
+ target_d = Dialect.SNOWFLAKE
217
+ elif is_pandas_target(target):
218
+ target_d = Dialect.PANDAS
219
+ elif is_pyspark_target(target):
220
+ target_d = Dialect.PYSPARK
221
+ else:
222
+ target_d = Dialect(target)
223
+
224
+ pipeline = MigrationPipeline(source=source_d, target=target_d)
225
+ report = pipeline.analyze(str(repo_path))
226
+ report = pipeline.convert(report)
227
+ report = pipeline.validate(report)
228
+ if wants_dbt:
229
+ report.target_dialect = Dialect.DBT_SNOWFLAKE
230
+
231
+ exec_summary = generate_executive_summary(report)
232
+ runbook = generate_runbook(report)
233
+ rationalization = generate_rationalization(report)
234
+
235
+ dbt_preview = "No suitable object for dbt decomposition."
236
+ candidates = sorted(report.objects, key=lambda o: o.complexity_score, reverse=True)
237
+ dbt_parts = ["### dbt project preview (Snowflake)\n"]
238
+ shown = 0
239
+ for obj in candidates:
240
+ if obj.object_type.value not in ("stored_procedure", "sql_script", "view"):
241
+ continue
242
+ files = decompose_to_dbt(obj, source_d, project_name=_sanitize_project(obj.name))
243
+ dbt_parts.append(f"#### `{obj.name}` → {len(files)} files\n")
244
+ # Show model SQL files first
245
+ model_files = [p for p in sorted(files) if p.startswith("models/") and p.endswith(".sql")]
246
+ for rel in model_files[:5]:
247
+ dbt_parts.append(f"**{rel}**\n```sql\n{files[rel][:1200]}\n```\n")
248
+ shown += 1
249
+ if shown >= 2:
250
+ break
251
+ if shown:
252
+ dbt_preview = "\n".join(dbt_parts)
253
+ elif not wants_dbt:
254
+ dbt_preview = (
255
+ "Target is not **dbt-snowflake**. "
256
+ "Select **dbt-snowflake** as the target to generate staging / intermediate / mart models, "
257
+ "or open Architecture (dbt) after a dbt-snowflake run."
258
+ )
259
+
260
+ val_lines = ["### Validation results\n"]
261
+ passed = sum(1 for r in report.validation_results if r.passed)
262
+ total = len(report.validation_results)
263
+ val_lines.append(f"**{passed}/{total}** checks passed\n")
264
+ for r in report.validation_results[:20]:
265
+ status = "PASS" if r.passed else "FAIL"
266
+ val_lines.append(f"- **[{status}]** {r.object_name} · {r.check_name}")
267
+ if r.root_cause:
268
+ val_lines.append(f" - {r.root_cause}")
269
+ if r.recommendation and not r.passed:
270
+ val_lines.append(f" - Fix: {r.recommendation}")
271
+ validation_md = "\n".join(val_lines) if total else "No validation checks generated."
272
+
273
+ obj_lines = [
274
+ "### Discovered objects\n",
275
+ "| Object | Type | Complexity | Risk | Confidence | Category |",
276
+ "|--------|------|------------|------|------------|----------|",
277
+ ]
278
+ for obj in report.objects:
279
+ obj_lines.append(
280
+ f"| {obj.name} | {obj.object_type.value} | {obj.complexity_score} "
281
+ f"| {obj.risk_level.value} | {obj.conversion_confidence:.0f}% "
282
+ f"| {obj.migration_category.value.replace('_', ' ')} |"
283
+ )
284
+ if not report.objects:
285
+ obj_lines.append("| — | — | — | — | — | No objects found |")
286
+
287
+ metrics_md = (
288
+ f"**Portfolio risk:** {int(report.dashboard.migration_risk_score)} / 100 · "
289
+ f"**Objects:** {report.dashboard.total_objects} · "
290
+ f"**Auto-migrate:** {report.dashboard.auto_migratable} · "
291
+ f"**Review:** {report.dashboard.requires_review} · "
292
+ f"**Redesign:** {report.dashboard.requires_redesign} · "
293
+ f"**Retire:** {report.dashboard.recommended_retirement}"
294
+ )
295
+
296
+ graph = build_lineage_graph(report.objects, source_d)
297
+ lineage_fig = lineage_to_plotly(graph, report.objects)
298
+ risk_fig = _risk_gauge(int(report.dashboard.migration_risk_score))
299
+ dist_fig = _distribution_chart(report)
300
+
301
+ export = json.dumps(
302
+ {
303
+ "repository": str(repo_path),
304
+ "source": source,
305
+ "target": target,
306
+ "dashboard": report.dashboard.model_dump(mode="json"),
307
+ "objects": [
308
+ {
309
+ "name": o.name,
310
+ "type": o.object_type.value,
311
+ "complexity": o.complexity_score,
312
+ "risk": o.risk_level.value,
313
+ "confidence": o.conversion_confidence,
314
+ "category": o.migration_category.value,
315
+ }
316
+ for o in report.objects
317
+ ],
318
+ },
319
+ indent=2,
320
+ )
321
+
322
+ return (
323
+ exec_summary + "\n\n" + metrics_md,
324
+ "\n".join(obj_lines),
325
+ rationalization,
326
+ runbook,
327
+ dbt_preview,
328
+ validation_md,
329
+ metrics_md,
330
+ risk_fig,
331
+ dist_fig,
332
+ lineage_fig,
333
+ export,
334
+ _report_to_dict(report),
335
+ )
336
+ except Exception as exc:
337
+ return (
338
+ f"**Workbench error:** {type(exc).__name__}: {exc}",
339
+ *empty[1:],
340
+ )
341
+
342
+
343
+ def analyze_sql_object(sql: str, source: str, target: str) -> tuple[str, go.Figure, str, str, str]:
344
+ """Assess + convert a single SQL object.
345
+
346
+ Returns: analysis_md, risk_fig, badge, converted_sql_or_dbt, notes_md
347
+ """
348
+ if not (sql or "").strip():
349
+ return (
350
+ "Paste SQL above, then click **Assess & Convert**.",
351
+ _risk_gauge(0),
352
+ "—",
353
+ "",
354
+ "Waiting for SQL input.",
355
+ )
356
+
357
+ try:
358
+ from morphsql.parser.sql_parser import count_sql_complexity, detect_unsupported_features
359
+ from morphsql.risk.scorer import extract_business_rules, score_object
360
+ from morphsql.validation.reconciliation import generate_incremental_strategy
361
+
362
+ source_d = Dialect(source)
363
+ wants_dbt = is_dbt_target(target)
364
+ wants_pandas = is_pandas_target(target)
365
+ wants_pyspark = is_pyspark_target(target)
366
+ if wants_dbt:
367
+ target_d = Dialect.SNOWFLAKE
368
+ elif wants_pandas:
369
+ target_d = Dialect.PANDAS
370
+ elif wants_pyspark:
371
+ target_d = Dialect.PYSPARK
372
+ else:
373
+ target_d = Dialect(target)
374
+
375
+ # Infer object type for better dbt decomposition
376
+ obj_type = ObjectType.SQL_SCRIPT
377
+ if re.search(r"\bCREATE\s+(?:OR\s+REPLACE\s+)?PROCEDURE\b", sql, re.I):
378
+ obj_type = ObjectType.STORED_PROCEDURE
379
+ elif re.search(r"\bCREATE\s+(?:OR\s+REPLACE\s+)?VIEW\b", sql, re.I):
380
+ obj_type = ObjectType.VIEW
381
+
382
+ score_target = (
383
+ Dialect.SNOWFLAKE if (wants_pandas or wants_pyspark) else target_d
384
+ )
385
+ obj = MigrationObject(name="input_object", object_type=obj_type, source_sql=sql)
386
+ obj = score_object(obj, source_d, score_target)
387
+ complexity = count_sql_complexity(sql, source_d)
388
+ unsupported = detect_unsupported_features(sql, source_d, score_target)
389
+ rules = extract_business_rules(sql)
390
+ incremental = generate_incremental_strategy(sql)
391
+
392
+ converted, conf, auto, review = translate_sql(sql, source_d, target_d)
393
+ obj.target_sql = converted
394
+ obj.conversion_confidence = conf
395
+ obj.auto_converted = auto
396
+ obj.requires_review = review
397
+
398
+ output_sql = converted
399
+ dbt_note = ""
400
+ if wants_dbt:
401
+ files = decompose_to_dbt(obj, source_d, project_name="input_object")
402
+ output_sql = format_dbt_project(files)
403
+ dbt_note = (
404
+ f"**dbt project generated:** {len(files)} files "
405
+ f"({len(obj.dbt_models)} models) — staging / intermediate / marts\n\n"
406
+ )
407
+
408
+ lines = [
409
+ "### Object assessment",
410
+ "",
411
+ "| Field | Value |",
412
+ "|-------|-------|",
413
+ f"| Complexity | {obj.complexity_score} / 100 |",
414
+ f"| Risk | {obj.risk_level.value.title()} |",
415
+ f"| Category | {obj.migration_category.value.replace('_', ' ').title()} |",
416
+ f"| Conversion confidence | {conf:.0f}% |",
417
+ f"| Output | {'dbt models (Snowflake)' if wants_dbt else target} |",
418
+ f"| Lines | {complexity.get('lines', 0)} |",
419
+ f"| CTEs | {complexity.get('ctes', 0)} |",
420
+ f"| Joins | {complexity.get('joins', 0)} |",
421
+ f"| Temp tables | {complexity.get('temp_tables', 0)} |",
422
+ f"| Window functions | {complexity.get('window_functions', 0)} |",
423
+ "",
424
+ ]
425
+ if wants_dbt and obj.dbt_models:
426
+ lines += ["**dbt models**", ""] + [f"- `{m}`" for m in obj.dbt_models] + [""]
427
+ if obj.risk_factors:
428
+ lines += ["**Risk factors**", ""] + [f"- {rf.description}" for rf in obj.risk_factors] + [""]
429
+ if unsupported:
430
+ lines += ["**Unsupported syntax**", ""] + [f"- {u}" for u in unsupported] + [""]
431
+ if rules:
432
+ lines += ["**Business rules**", ""] + [f"- {r}" for r in rules[:5]] + [""]
433
+ if incremental:
434
+ lines += ["**Incremental pattern**", ""]
435
+ for k, v in incremental.items():
436
+ lines.append(f"- {k.replace('_', ' ').title()}: {v}")
437
+
438
+ notes = [
439
+ f"**Route:** {source} → {target}",
440
+ f"**Confidence:** {conf:.0f}%",
441
+ "",
442
+ ]
443
+ if dbt_note:
444
+ notes.append(dbt_note)
445
+ if auto:
446
+ notes.append("**SQL transformations**")
447
+ notes.extend(f"- {a}" for a in auto[:10])
448
+ notes.append("")
449
+ if review:
450
+ notes.append("**Manual review**")
451
+ notes.extend(f"- {r}" for r in review[:10])
452
+
453
+ badge = f"{obj.complexity_score} / 100 · {obj.risk_level.value.title()} · {conf:.0f}% conf"
454
+ return (
455
+ "\n".join(lines),
456
+ _risk_gauge(obj.complexity_score),
457
+ badge,
458
+ output_sql,
459
+ "\n".join(notes),
460
+ )
461
+ except Exception as exc:
462
+ return (
463
+ f"**Assessment error:** {type(exc).__name__}: {exc}",
464
+ _risk_gauge(0),
465
+ "Error",
466
+ "",
467
+ str(exc),
468
+ )
469
+
470
+
471
+ def get_sample_workbench() -> tuple:
472
+ """Precompute sample workbench outputs so UI is never blank on load."""
473
+ return run_migration_workbench(None, True, "vertica", "snowflake")
474
+
475
+
476
+ def copilot_chat(
477
+ message: str,
478
+ history: list | None,
479
+ report_data: dict | MigrationReport | None,
480
+ sql: str,
481
+ source: str,
482
+ target: str,
483
+ ) -> tuple[list, str]:
484
+ history = list(history or [])
485
+ if not (message or "").strip():
486
+ return history, ""
487
+
488
+ report = _dict_to_report(report_data)
489
+ normalized: list[dict] = []
490
+ for turn in history:
491
+ if isinstance(turn, dict) and "role" in turn and "content" in turn:
492
+ normalized.append({"role": turn["role"], "content": str(turn["content"])})
493
+ elif isinstance(turn, (list, tuple)) and len(turn) == 2:
494
+ if turn[0]:
495
+ normalized.append({"role": "user", "content": str(turn[0])})
496
+ if turn[1]:
497
+ normalized.append({"role": "assistant", "content": str(turn[1])})
498
+
499
+ reply = _copilot.respond(message.strip(), normalized, report, sql or "", source, target)
500
+ normalized.append({"role": "user", "content": message.strip()})
501
+ normalized.append({"role": "assistant", "content": reply})
502
+ return normalized, ""
503
+
504
+
505
+ def report_to_context(report_data: dict | MigrationReport | None) -> str:
506
+ report = _dict_to_report(report_data)
507
+ if report is None:
508
+ return (
509
+ "No repository scan loaded yet. "
510
+ "Open Migration Workbench, run a scan, then return here — "
511
+ "the copilot will use that context."
512
+ )
513
+ ctx = _copilot.build_context(report)
514
+ # Avoid markdown code fences that can go white-on-white in Gradio themes
515
+ safe = ctx[:2500].replace("`", "'")
516
+ return f"### Active scan context\n\n{safe}"
517
+
518
+
519
+ HERO_EXAMPLE = (
520
+ "SELECT customer_id, COALESCE(order_amount, 0) AS order_amount, "
521
+ "COALESCE(discount, 0) AS discount FROM staging.orders "
522
+ "WHERE order_date >= CURRENT_DATE - 30"
523
+ )
524
+
525
+ FEATURE_SQL_PATH = Path(__file__).parent.parent / "examples" / "ml_features" / "churn_feature_sql.sql"
526
+
527
+ # Human-readable labels (defined before convert helpers)
528
+ # Snowflake first — most common warehouse for DS/AI users
529
+ SOURCE_LABELS = {
530
+ "snowflake": "Snowflake",
531
+ "bigquery": "BigQuery",
532
+ "redshift": "Redshift",
533
+ "oracle": "Oracle",
534
+ "vertica": "Vertica",
535
+ }
536
+ TARGET_LABELS = {
537
+ "pandas": "Python (pandas)",
538
+ "pyspark": "Python (PySpark)",
539
+ "snowflake": "Snowflake SQL",
540
+ "bigquery": "BigQuery SQL",
541
+ "dbt-snowflake": "dbt project (Snowflake)",
542
+ }
543
+ SOURCE_DROPDOWN = [(label, key) for key, label in SOURCE_LABELS.items()]
544
+ TARGET_DROPDOWN = [
545
+ ("Python (pandas) — recommended for notebooks", "pandas"),
546
+ ("Python (PySpark) — Spark DataFrame API", "pyspark"),
547
+ ("Snowflake SQL", "snowflake"),
548
+ ("BigQuery SQL", "bigquery"),
549
+ ("dbt project (Snowflake)", "dbt-snowflake"),
550
+ ]
551
+
552
+
553
+ def run_hero_agent(sql: str, source: str, target: str) -> tuple[str, str, str, str]:
554
+ """Convert SQL and return (notes_md, output, status, share_md)."""
555
+ from morphsql.risk.scorer import score_object
556
+
557
+ sql = sql or HERO_EXAMPLE
558
+ source_d = Dialect(source)
559
+ wants_dbt = is_dbt_target(target)
560
+ wants_pandas = is_pandas_target(target)
561
+ wants_pyspark = is_pyspark_target(target)
562
+ if wants_dbt:
563
+ target_d = Dialect.SNOWFLAKE
564
+ elif wants_pandas:
565
+ target_d = Dialect.PANDAS
566
+ elif wants_pyspark:
567
+ target_d = Dialect.PYSPARK
568
+ else:
569
+ target_d = Dialect(target)
570
+
571
+ converted, conf, auto, review = translate_sql(sql, source_d, target_d)
572
+ score_target = (
573
+ Dialect.SNOWFLAKE if (wants_pandas or wants_pyspark) else target_d
574
+ )
575
+ obj = MigrationObject(
576
+ name="hero_query",
577
+ object_type=ObjectType.SQL_SCRIPT,
578
+ source_sql=sql,
579
+ target_sql=converted,
580
+ )
581
+ obj = score_object(obj, source_d, score_target)
582
+
583
+ output = converted
584
+ if wants_dbt:
585
+ files = decompose_to_dbt(obj, source_d, project_name="morphsql_dbt")
586
+ output = format_dbt_project(files, max_files=12)
587
+
588
+ source_label = SOURCE_LABELS.get(source, source)
589
+ target_label = TARGET_LABELS.get(target, target)
590
+ if wants_pandas:
591
+ kind = "Python (pandas)"
592
+ next_steps = [
593
+ "1. Review the generated Python on the right.",
594
+ "2. Check the **sample preview** (synthetic tables).",
595
+ "3. Download the `.py` file or open the notebook starter cell.",
596
+ "4. Point `tables['…']` at your real DataFrames (`read_parquet` / `read_sql`).",
597
+ ]
598
+ elif wants_pyspark:
599
+ kind = "Python (PySpark)"
600
+ next_steps = [
601
+ "1. Review the generated PySpark code on the right.",
602
+ "2. Check the **sample preview** (same query logic on synthetic tables).",
603
+ "3. Download the `.py` file or open the Spark notebook starter.",
604
+ "4. Provide `tables['…']` or replace with `spark.table` / `spark.read`.",
605
+ ]
606
+ elif wants_dbt:
607
+ kind = "dbt project"
608
+ next_steps = [
609
+ "1. Copy the generated dbt files.",
610
+ "2. Check the **sample preview** for query-shape sanity.",
611
+ "3. Drop models into a dbt project and run `dbt run`.",
612
+ "4. Review models marked for manual checks.",
613
+ ]
614
+ else:
615
+ kind = f"{target_label} SQL"
616
+ next_steps = [
617
+ "1. Copy the converted SQL on the right.",
618
+ "2. Check the **sample preview** (same query logic on synthetic tables).",
619
+ "3. Run the SQL on the target warehouse.",
620
+ "4. Check any items under Needs review.",
621
+ ]
622
+
623
+ changes: list[str] = []
624
+ for a in auto[:8]:
625
+ if any(
626
+ k in a
627
+ for k in (
628
+ "→",
629
+ "Removed",
630
+ "Converted",
631
+ "JOIN",
632
+ "WHERE",
633
+ "GROUP",
634
+ "SELECT",
635
+ "pandas",
636
+ "Dialect",
637
+ "FROM dual",
638
+ )
639
+ ):
640
+ changes.append(f"- {a}")
641
+
642
+ notes = [
643
+ f"### {source_label} → {target_label}",
644
+ "",
645
+ f"**{conf:.0f}%** confidence · output: **{kind}**",
646
+ "",
647
+ "**Next steps**",
648
+ *next_steps,
649
+ "",
650
+ ]
651
+ if changes:
652
+ notes.append("**What changed**")
653
+ notes.extend(changes)
654
+ notes.append("")
655
+ if review:
656
+ notes.append("**Needs review**")
657
+ notes.extend(f"- {r}" for r in review[:5])
658
+ notes.append("")
659
+
660
+ status = f"{conf:.0f}% · {kind}"
661
+ space_url = "https://huggingface.co/spaces/dgvj-work/morphsql"
662
+ share = (
663
+ f"MorphSQL converted **{source_label} → {target_label}** "
664
+ f"({conf:.0f}% confidence).\n\n"
665
+ f"[Open Space]({space_url}) · "
666
+ f"[GitHub](https://github.com/dgvj-work/morphsql)"
667
+ )
668
+ return "\n".join(notes), output, status, share
669
+
670
+
671
+ def _infer_columns_from_sql(sql: str) -> list[str]:
672
+ cols: list[str] = []
673
+ # AS aliases
674
+ cols.extend(re.findall(r"\bAS\s+([A-Za-z_][\w]*)", sql or "", flags=re.I))
675
+ # bare identifiers after SELECT / commas (best-effort)
676
+ cols.extend(re.findall(r"(?:SELECT|,)\s*(?:[\w.]+\.)?([A-Za-z_][\w]*)\b", sql or "", flags=re.I))
677
+ # common warehouse columns used in demos
678
+ cols.extend(
679
+ [
680
+ "customer_id",
681
+ "user_id",
682
+ "order_amount",
683
+ "discount",
684
+ "order_date",
685
+ "amount",
686
+ "name",
687
+ "id",
688
+ "a",
689
+ "b",
690
+ "x",
691
+ "dt",
692
+ "dept",
693
+ "event_value",
694
+ "event_ts",
695
+ "event_type",
696
+ "value",
697
+ "score",
698
+ ]
699
+ )
700
+ # de-dupe preserving order, drop SQL keywords
701
+ skip = {
702
+ "select",
703
+ "from",
704
+ "where",
705
+ "group",
706
+ "by",
707
+ "order",
708
+ "limit",
709
+ "as",
710
+ "and",
711
+ "or",
712
+ "on",
713
+ "join",
714
+ "left",
715
+ "right",
716
+ "inner",
717
+ "outer",
718
+ "case",
719
+ "when",
720
+ "then",
721
+ "else",
722
+ "end",
723
+ "null",
724
+ "not",
725
+ "is",
726
+ "in",
727
+ "distinct",
728
+ "count",
729
+ "sum",
730
+ "avg",
731
+ "min",
732
+ "max",
733
+ "coalesce",
734
+ "nvl",
735
+ "zeroifnull",
736
+ "ifnull",
737
+ "current_date",
738
+ "sysdate",
739
+ "getdate",
740
+ }
741
+ out: list[str] = []
742
+ for c in cols:
743
+ if c.lower() in skip:
744
+ continue
745
+ if c not in out:
746
+ out.append(c)
747
+ return out[:24]
748
+
749
+
750
+ def build_sample_tables(code: str, sql: str = "") -> dict:
751
+ """Build tiny demo DataFrames so generated pandas can execute in the Space."""
752
+ import pandas as pd
753
+
754
+ keys = list(dict.fromkeys(re.findall(r"tables\[['\"]([^'\"]+)['\"]\]", code or "")))
755
+ if not keys and sql:
756
+ # Fallback: FROM / JOIN identifiers in the source SQL
757
+ keys = list(
758
+ dict.fromkeys(
759
+ re.findall(
760
+ r"(?:FROM|JOIN)\s+([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)?)",
761
+ sql,
762
+ flags=re.I,
763
+ )
764
+ )
765
+ )
766
+ keys = [k for k in keys if k.lower() not in {"dual", "select"}]
767
+ cols = _infer_columns_from_sql(sql)
768
+ # Include columns referenced in generated code (e.g. procedure params used as cols)
769
+ for c in re.findall(r"\[['\"]([^'\"]+)['\"]\]", code or ""):
770
+ if "." in c or c in keys:
771
+ continue # table keys like staging.orders
772
+ if c not in cols:
773
+ cols.append(c)
774
+ if not cols:
775
+ cols = ["id", "value"]
776
+
777
+ n = 5
778
+ today = pd.Timestamp.today().normalize()
779
+ frames: dict = {}
780
+ for key in keys:
781
+ data: dict = {}
782
+ for i, col in enumerate(cols):
783
+ cl = col.lower()
784
+ if any(t in cl for t in ("date", "ts", "time", "dt")):
785
+ data[col] = [today - pd.Timedelta(days=j) for j in range(n)]
786
+ elif any(t in cl for t in ("id", "count", "nunique", "flag", "label")):
787
+ data[col] = list(range(1, n + 1))
788
+ elif any(t in cl for t in ("amount", "value", "score", "avg", "sum", "discount")):
789
+ data[col] = [None if j == 0 else float(10 * (j + 1)) for j in range(n)]
790
+ elif any(t in cl for t in ("name", "type", "dept")):
791
+ data[col] = [f"item_{j}" for j in range(n)]
792
+ else:
793
+ data[col] = [j + 1 for j in range(n)]
794
+ frames[key] = pd.DataFrame(data)
795
+ return frames
796
+
797
+
798
+ def _preview_code_for_target(output: str, target: str, sql: str, source: str) -> tuple[str, str]:
799
+ """
800
+ Return (python_code, via_label) used to produce a sample DataFrame preview.
801
+
802
+ Pandas targets execute the generated code. All other targets reuse the same
803
+ source SQL → pandas path so preview works without Spark / a warehouse.
804
+ """
805
+ from morphsql.translator.pandas_codegen import sql_to_pandas
806
+
807
+ code = output or ""
808
+ if is_pandas_target(target) and "import pandas" in code:
809
+ return code, "generated pandas"
810
+
811
+ if not (sql or "").strip():
812
+ return "", ""
813
+
814
+ try:
815
+ source_d = Dialect(source)
816
+ except ValueError:
817
+ source_d = Dialect.SNOWFLAKE
818
+
819
+ preview_code, _conf, _auto, _review = sql_to_pandas(sql, source_d)
820
+ label = TARGET_LABELS.get(target, target)
821
+ return preview_code, f"pandas runtime · same query logic (output above is {label})"
822
+
823
+
824
+ def run_sample_preview(
825
+ output: str,
826
+ target: str,
827
+ sql: str = "",
828
+ source: str = "snowflake",
829
+ ) -> tuple:
830
+ """
831
+ Execute query logic against synthetic tables for a sample DataFrame preview.
832
+
833
+ Works for every Convert target: pandas runs the generated code; PySpark / SQL /
834
+ dbt reuse a pandas translation of the source SQL so the Space can show rows
835
+ without Spark or a warehouse connection.
836
+ Returns (dataframe_or_none, note_md).
837
+ """
838
+ import pandas as pd
839
+
840
+ preview_code, via = _preview_code_for_target(output, target, sql, source)
841
+ if not preview_code or "import pandas" not in preview_code:
842
+ return None, "_Could not build a sample preview for this input._"
843
+
844
+ tables = build_sample_tables(preview_code, sql)
845
+ if not tables and "tables[" in preview_code:
846
+ return None, "_Could not infer input tables for a live preview._"
847
+ if not tables:
848
+ tables = {}
849
+
850
+ ns: dict = {"pd": pd, "np": __import__("numpy"), "tables": tables}
851
+ try:
852
+ exec(preview_code, ns, ns) # noqa: S102 — intentional demo sandbox for generated code
853
+ except Exception as exc:
854
+ return None, f"_Preview could not run automatically:_ `{exc}`"
855
+
856
+ result = ns.get("result")
857
+ if not isinstance(result, pd.DataFrame):
858
+ # Procedures may leave the last SELECT under result_N
859
+ for key, val in reversed(list(ns.items())):
860
+ if key.startswith("result") and isinstance(val, pd.DataFrame):
861
+ result = val
862
+ break
863
+ if not isinstance(result, pd.DataFrame):
864
+ return None, "_Preview ran, but no `result` DataFrame was produced._"
865
+
866
+ note = (
867
+ f"**Sample preview** · {via} on {len(tables)} synthetic table(s) "
868
+ f"→ `{result.shape[0]}` rows × `{result.shape[1]}` cols. "
869
+ "Replace inputs with your real data when you run the converted output."
870
+ )
871
+ return result.head(20), note
872
+
873
+
874
+ def write_output_download(
875
+ output: str,
876
+ target: str,
877
+ stem: str | None = None,
878
+ ) -> str:
879
+ """Write converted output to a downloadable temp file; return path."""
880
+ safe = re.sub(r"[^\w.\-]+", "_", (stem or "morphsql").strip())[:80] or "morphsql"
881
+ if is_pandas_target(target):
882
+ suffix, kind = ".py", "pandas"
883
+ elif is_pyspark_target(target):
884
+ suffix, kind = ".py", "pyspark"
885
+ elif is_dbt_target(target):
886
+ suffix, kind = ".txt", "dbt"
887
+ else:
888
+ suffix, kind = ".sql", str(target).replace("-", "_")
889
+ path = Path(tempfile.gettempdir()) / f"{safe}_{kind}{suffix}"
890
+ # Avoid collisions when converting multiple files in one session
891
+ if path.exists():
892
+ path = Path(tempfile.gettempdir()) / f"{safe}_{kind}_{abs(hash(output)) % 100_000}{suffix}"
893
+ path.write_text(output or "", encoding="utf-8")
894
+ return str(path)
895
+
896
+
897
+ def _resolve_upload_path(upload_file) -> Path | None:
898
+ if not upload_file:
899
+ return None
900
+ if isinstance(upload_file, (list, tuple)) and upload_file:
901
+ upload_file = upload_file[0]
902
+ path = Path(str(getattr(upload_file, "name", upload_file)))
903
+ return path if path.exists() else None
904
+
905
+
906
+ def _collect_sql_files(upload_file) -> list[Path]:
907
+ """Return SQL/text files from an upload (.sql/.txt or .zip of those)."""
908
+ path = _resolve_upload_path(upload_file)
909
+ if path is None:
910
+ return []
911
+ suffix = path.suffix.lower()
912
+ if suffix in {".sql", ".txt", ".ddl", ".prc", ".pkb", ".pks"}:
913
+ return [path]
914
+ if suffix == ".zip":
915
+ tmp = Path(tempfile.mkdtemp(prefix="morphsql_upload_"))
916
+ with zipfile.ZipFile(path) as zf:
917
+ zf.extractall(tmp)
918
+ files = sorted(
919
+ p
920
+ for p in tmp.rglob("*")
921
+ if p.is_file() and p.suffix.lower() in {".sql", ".txt", ".ddl", ".prc"}
922
+ )
923
+ return files
924
+ # Unknown extension — try reading as text SQL
925
+ return [path]
926
+
927
+
928
+ def load_sql_from_upload(upload_file, current_sql: str = "") -> str:
929
+ """Load uploaded SQL into the input box (first file if a zip). Keeps current text if empty."""
930
+ files = _collect_sql_files(upload_file)
931
+ if not files:
932
+ return current_sql or ""
933
+ try:
934
+ return files[0].read_text(encoding="utf-8", errors="replace")
935
+ except OSError:
936
+ return current_sql or ""
937
+
938
+
939
+ def convert_upload_for_ui(upload_file, sql: str, source: str, target: str):
940
+ """
941
+ Convert pasted SQL or an uploaded .sql/.txt/.zip.
942
+
943
+ Single file → normal Convert payload with a named download.
944
+ Zip with multiple SQL files → convert each and return a zip download.
945
+ Returns: (sql_in, notes, output, status, share, preview, download, notebook, api)
946
+ """
947
+ files = _collect_sql_files(upload_file)
948
+
949
+ if not files:
950
+ text = (sql or "").strip()
951
+ if not text:
952
+ empty = convert_for_ui("", source, target)
953
+ return ("", *empty)
954
+ notes, output, status, share, preview, download, nb, api = convert_for_ui(
955
+ text, source, target
956
+ )
957
+ return text, notes, output, status, share, preview, download, nb, api
958
+
959
+ if len(files) == 1:
960
+ text = files[0].read_text(encoding="utf-8", errors="replace")
961
+ notes, output, status, share, preview, _old_dl, nb, api = convert_for_ui(
962
+ text, source, target
963
+ )
964
+ download = write_output_download(output, target, stem=files[0].stem)
965
+ note = (
966
+ f"\n**Upload** · converted `{files[0].name}` → download "
967
+ f"`{Path(download).name}`.\n"
968
+ )
969
+ return text, notes + note, output, status, share, preview, download, nb, api
970
+
971
+ # Multi-file zip → convert each and package
972
+ out_dir = Path(tempfile.mkdtemp(prefix="morphsql_batch_"))
973
+ converted_paths: list[Path] = []
974
+ previews = []
975
+ combined_notes = [
976
+ f"### Batch upload · {len(files)} SQL file(s) → **{TARGET_LABELS.get(target, target)}**",
977
+ "",
978
+ ]
979
+ first_output = ""
980
+ first_sql = ""
981
+ statuses: list[str] = []
982
+
983
+ for fp in files:
984
+ try:
985
+ text = fp.read_text(encoding="utf-8", errors="replace")
986
+ except OSError as exc:
987
+ combined_notes.append(f"- `{fp.name}`: read error ({exc})")
988
+ continue
989
+ if not first_sql:
990
+ first_sql = text
991
+ notes, output, status, _share, preview, _dl, _nb, _api = convert_for_ui(
992
+ text, source, target
993
+ )
994
+ if not first_output:
995
+ first_output = output
996
+ if preview is not None:
997
+ previews.append(preview)
998
+ if is_pandas_target(target):
999
+ dest = out_dir / f"{fp.stem}_pandas.py"
1000
+ elif is_pyspark_target(target):
1001
+ dest = out_dir / f"{fp.stem}_pyspark.py"
1002
+ elif is_dbt_target(target):
1003
+ dest = out_dir / f"{fp.stem}_dbt.txt"
1004
+ else:
1005
+ dest = out_dir / f"{fp.stem}_{target.replace('-', '_')}.sql"
1006
+ dest.write_text(output or "", encoding="utf-8")
1007
+ converted_paths.append(dest)
1008
+ statuses.append(status)
1009
+ combined_notes.append(f"- `{fp.name}` → `{dest.name}` · {status}")
1010
+
1011
+ zip_path = Path(tempfile.gettempdir()) / f"morphsql_batch_{target.replace('-', '_')}.zip"
1012
+ with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
1013
+ for p in converted_paths:
1014
+ zf.write(p, arcname=p.name)
1015
+
1016
+ preview = previews[0] if previews else None
1017
+ status = statuses[0] if statuses else "Batch convert"
1018
+ share = (
1019
+ f"Converted **{len(converted_paths)}** file(s) to `{target}`. "
1020
+ f"Download the zip and drop files into your notebook / warehouse / dbt project.\n\n"
1021
+ f"[Space](https://huggingface.co/spaces/dgvj-work/morphsql) · "
1022
+ f"[GitHub](https://github.com/dgvj-work/morphsql)"
1023
+ )
1024
+ nb = notebook_cell(first_output, target)
1025
+ api = hf_pipeline_snippet(first_sql or HERO_EXAMPLE, source, target)
1026
+ notes = "\n".join(combined_notes) + (
1027
+ f"\n\n**Download** the zip (`{zip_path.name}`) for all converted files.\n"
1028
+ )
1029
+ return (
1030
+ first_sql,
1031
+ notes,
1032
+ first_output or "(see zip for all converted files)",
1033
+ status,
1034
+ share,
1035
+ preview,
1036
+ str(zip_path),
1037
+ nb,
1038
+ api,
1039
+ )
1040
+
1041
+
1042
+ def notebook_cell(output: str, target: str) -> str:
1043
+ """Short copy-paste cell for Jupyter / Colab / Databricks."""
1044
+ if is_pyspark_target(target):
1045
+ return (
1046
+ "# MorphSQL → PySpark (paste into a Databricks / Spark notebook)\n"
1047
+ "from pyspark.sql import SparkSession, functions as F, Window\n\n"
1048
+ "# spark = SparkSession.builder.getOrCreate()\n"
1049
+ "# 1) Provide input frames (or replace with spark.table / spark.read)\n"
1050
+ "# tables = {\n"
1051
+ "# 'staging.orders': spark.table('staging.orders'),\n"
1052
+ "# }\n\n"
1053
+ "# 2) Paste the generated MorphSQL code below (or %run the downloaded .py)\n"
1054
+ "# 3) Use `result` as your Spark DataFrame\n"
1055
+ )
1056
+ if not is_pandas_target(target):
1057
+ return (
1058
+ "# Output is SQL/dbt — paste into your warehouse client or dbt project.\n"
1059
+ "# Tip: switch Convert to → Python (pandas) or Python (PySpark).\n"
1060
+ )
1061
+ return (
1062
+ "# MorphSQL → pandas (paste into a notebook cell)\n"
1063
+ "import pandas as pd\n"
1064
+ "import numpy as np\n\n"
1065
+ "# 1) Load your real data (parquet / sql / HF datasets)\n"
1066
+ "# tables = {\n"
1067
+ "# 'staging.orders': pd.read_parquet('orders.parquet'),\n"
1068
+ "# }\n\n"
1069
+ "# 2) Paste the generated MorphSQL code below (or %run the downloaded .py)\n"
1070
+ "# 3) Use `result` as features for sklearn / XGBoost / embeddings pipelines\n"
1071
+ )
1072
+
1073
+
1074
+ def hf_pipeline_snippet(sql: str, source: str, target: str) -> str:
1075
+ """Hugging Face-style API snippet for AI / ML users."""
1076
+ sql_short = (sql or "").strip().replace("\\", "\\\\").replace('"""', "'''")
1077
+ if len(sql_short) > 180:
1078
+ sql_short = sql_short[:177] + "..."
1079
+ return (
1080
+ "from morphsql.ai import pipeline\n\n"
1081
+ "# Same API style as transformers.pipeline\n"
1082
+ 'pipe = pipeline("sql-migration")\n'
1083
+ "out = pipe(\n"
1084
+ f' """{sql_short}""",\n'
1085
+ f' source="{source}",\n'
1086
+ f' target="{target}",\n'
1087
+ ")\n"
1088
+ 'print(out["converted_sql"][:500]) # pandas code or SQL\n'
1089
+ 'print(out.get("risk")) # optional risk head\n'
1090
+ )
1091
+
1092
+
1093
+ def convert_for_ui(sql: str, source: str, target: str):
1094
+ """Full Convert-tab payload for the Space UI."""
1095
+ notes, output, status, share = run_hero_agent(sql, source, target)
1096
+ preview, preview_note = run_sample_preview(
1097
+ output, target, sql=sql or "", source=source or "snowflake"
1098
+ )
1099
+ download = write_output_download(output, target)
1100
+ nb = notebook_cell(output, target)
1101
+ api = hf_pipeline_snippet(sql or HERO_EXAMPLE, source, target)
1102
+ if preview_note:
1103
+ notes = notes + "\n" + preview_note + "\n"
1104
+ return notes, output, status, share, preview, download, nb, api
1105
+
1106
+
1107
+ PLAYGROUND_EXAMPLE_LABELS = [
1108
+ "DS: Snowflake orders → pandas (fillna / filters)",
1109
+ "DS: Snowflake orders → PySpark (F.coalesce / filter)",
1110
+ "AI: Feature aggregates → pandas (training features)",
1111
+ "DS: Vertica ZEROIFNULL → pandas",
1112
+ "DS: Oracle dual constants → pandas",
1113
+ "DS: Redshift window slice → pandas",
1114
+ "DS: BigQuery null handling → pandas",
1115
+ "Warehouse: Vertica → Snowflake SQL",
1116
+ "Warehouse: Vertica procedure → dbt",
1117
+ ]
1118
+
1119
+ PLAYGROUND_EXAMPLES = [
1120
+ [
1121
+ "SELECT customer_id, COALESCE(order_amount, 0) AS order_amount, COALESCE(discount, 0) AS discount FROM staging.orders WHERE order_date >= CURRENT_DATE - 30",
1122
+ "snowflake",
1123
+ "pandas",
1124
+ ],
1125
+ [
1126
+ "SELECT customer_id, COALESCE(order_amount, 0) AS order_amount, COALESCE(discount, 0) AS discount FROM staging.orders WHERE order_date >= CURRENT_DATE - 30",
1127
+ "snowflake",
1128
+ "pyspark",
1129
+ ],
1130
+ [
1131
+ "SELECT user_id, COUNT(*) AS event_count_90d, SUM(ZEROIFNULL(event_value)) AS value_sum_90d, AVG(ZEROIFNULL(event_value)) AS value_avg_90d FROM staging.product_events WHERE event_ts >= CURRENT_DATE - 90 GROUP BY user_id",
1132
+ "vertica",
1133
+ "pandas",
1134
+ ],
1135
+ [
1136
+ "SELECT customer_id, ZEROIFNULL(order_amount) AS order_amount, NVL(discount, 0) AS discount FROM staging.orders WHERE order_date >= CURRENT_DATE - 30",
1137
+ "vertica",
1138
+ "pandas",
1139
+ ],
1140
+ [
1141
+ "SELECT NVL(amount, 0) AS amount, SYSDATE AS ts FROM dual",
1142
+ "oracle",
1143
+ "pandas",
1144
+ ],
1145
+ [
1146
+ "SELECT GETDATE() AS ts, name FROM users WHERE id > 1 LIMIT 5",
1147
+ "redshift",
1148
+ "pandas",
1149
+ ],
1150
+ [
1151
+ "SELECT IFNULL(a, 0) AS a, b FROM t WHERE a IS NOT NULL",
1152
+ "bigquery",
1153
+ "pandas",
1154
+ ],
1155
+ [
1156
+ "SELECT customer_id, ZEROIFNULL(order_amount) AS order_amount FROM staging.orders",
1157
+ "vertica",
1158
+ "snowflake",
1159
+ ],
1160
+ [
1161
+ "CREATE OR REPLACE PROCEDURE p(load_date DATE) AS $$ BEGIN CREATE LOCAL TEMP TABLE tmp ON COMMIT PRESERVE ROWS AS SELECT customer_id, ZEROIFNULL(amount) AS amount FROM staging.orders WHERE order_date = load_date; INSERT INTO analytics.daily SELECT * FROM tmp; END; $$;",
1162
+ "vertica",
1163
+ "dbt-snowflake",
1164
+ ],
1165
+ ]
1166
+
1167
+
1168
+ def load_playground_example(index: int) -> tuple[str, str, str]:
1169
+ """Return (sql, source, target) for a preset example index."""
1170
+ idx = max(0, min(int(index), len(PLAYGROUND_EXAMPLES) - 1))
1171
+ sql, source, target = PLAYGROUND_EXAMPLES[idx]
1172
+ return sql, source, target
1173
+
1174
+
1175
+ def load_and_convert_example(index: int) -> tuple[str, str, str, str, str, str, str]:
1176
+ """Load a preset and immediately convert (legacy 7-tuple)."""
1177
+ sql, source, target = load_playground_example(index)
1178
+ explain, output, badge, share = run_hero_agent(sql, source, target)
1179
+ return sql, source, target, explain, output, badge, share
1180
+
1181
+
1182
+ def on_example_selected(label: str | None):
1183
+ """Dropdown helper used by the Convert tab (extended UI payload)."""
1184
+ if not label or label not in PLAYGROUND_EXAMPLE_LABELS:
1185
+ label = PLAYGROUND_EXAMPLE_LABELS[0]
1186
+ sql, source, target = load_playground_example(PLAYGROUND_EXAMPLE_LABELS.index(label))
1187
+ notes, output, status, share, preview, download, nb, api = convert_for_ui(
1188
+ sql, source, target
1189
+ )
1190
+ return sql, source, target, notes, output, status, share, preview, download, nb, api
1191
+
1192
+
1193
+ AGENT_PROMPTS = [
1194
+ (
1195
+ "Convert this SQL to pandas",
1196
+ PLAYGROUND_EXAMPLES[0][0],
1197
+ "snowflake",
1198
+ "pandas",
1199
+ ),
1200
+ (
1201
+ "Convert this SQL to PySpark",
1202
+ PLAYGROUND_EXAMPLES[1][0],
1203
+ "snowflake",
1204
+ "pyspark",
1205
+ ),
1206
+ (
1207
+ "Convert feature SQL to pandas",
1208
+ PLAYGROUND_EXAMPLES[2][0],
1209
+ "vertica",
1210
+ "pandas",
1211
+ ),
1212
+ (
1213
+ "Emit a dbt project from this procedure",
1214
+ PLAYGROUND_EXAMPLES[8][0],
1215
+ "vertica",
1216
+ "dbt-snowflake",
1217
+ ),
1218
+ ]
1219
+
1220
+
1221
+ def load_agent_example(index: int) -> tuple[str, str, str, str]:
1222
+ idx = max(0, min(int(index), len(AGENT_PROMPTS) - 1))
1223
+ return AGENT_PROMPTS[idx]
1224
+
1225
+
1226
+ def run_eval_suite(limit: int, category: str) -> tuple[str, str, dict]:
1227
+ """Run eval suite and return markdown summary, detail table, metrics dict."""
1228
+ from morphsql.eval.metrics import run_eval
1229
+ from morphsql.eval.pairs import ensure_pairs_file
1230
+
1231
+ ensure_pairs_file()
1232
+ cats = None if category in ("all", "", None) else [category]
1233
+ limit_i = int(limit) if limit else 50
1234
+ results, summary = run_eval(limit=limit_i, categories=cats)
1235
+
1236
+ lines = [
1237
+ "### Eval suite results",
1238
+ "",
1239
+ f"| Metric | Score |",
1240
+ f"|--------|-------|",
1241
+ f"| Pairs | {summary['n_pairs']} |",
1242
+ f"| Exact match | {100 * summary['exact_match']:.1f}% |",
1243
+ f"| Token F1 | {100 * summary['token_f1']:.1f}% |",
1244
+ f"| Fuzzy (Dice) | {100 * summary['fuzzy']:.1f}% |",
1245
+ f"| Pass rate | {100 * summary['pass_rate']:.1f}% |",
1246
+ "",
1247
+ "#### By category",
1248
+ "",
1249
+ "| Category | N | Exact | Token F1 | Pass |",
1250
+ "|----------|---|-------|----------|------|",
1251
+ ]
1252
+ for cat, stats in summary.get("by_category", {}).items():
1253
+ lines.append(
1254
+ f"| {cat} | {stats['n']} | {100 * stats['exact_match']:.0f}% "
1255
+ f"| {100 * stats['token_f1']:.0f}% | {100 * stats['pass_rate']:.0f}% |"
1256
+ )
1257
+
1258
+ detail = [
1259
+ "### Sample predictions",
1260
+ "",
1261
+ "| ID | Pass | F1 | Exact |",
1262
+ "|----|------|----|-------|",
1263
+ ]
1264
+ for r in results[:25]:
1265
+ detail.append(
1266
+ f"| {r.pair_id} | {'yes' if r.passed else 'no'} "
1267
+ f"| {100 * r.token_f1:.0f}% | {100 * r.exact_match:.0f}% |"
1268
+ )
1269
+ return "\n".join(lines), "\n".join(detail), summary
1270
+
1271
+
1272
+ def submit_eval_score(name: str, summary: dict | None) -> str:
1273
+ from morphsql.eval.leaderboard import format_leaderboard_md, submit_score
1274
+
1275
+ if not summary or not summary.get("n_pairs"):
1276
+ return format_leaderboard_md() + "\n\n_Run the eval suite before submitting._"
1277
+ board = submit_score(
1278
+ name=name or "anonymous",
1279
+ exact_match=summary.get("exact_match", 0),
1280
+ token_f1=summary.get("token_f1", 0),
1281
+ fuzzy=summary.get("fuzzy", 0),
1282
+ pass_rate=summary.get("pass_rate", 0),
1283
+ n_pairs=summary.get("n_pairs", 0),
1284
+ notes="MorphSQL hybrid translator",
1285
+ )
1286
+ return format_leaderboard_md(board)
1287
+
1288
+
1289
+ def run_behavior_rag(query: str, source: str, target: str) -> str:
1290
+ from morphsql.intelligence.rag import get_rag
1291
+
1292
+ return get_rag().answer(query or "NULL empty string timezone", source, target)
1293
+
1294
+
1295
+ def run_feature_migration(target: str) -> tuple[str, str]:
1296
+ """Convert ML feature SQL and optionally emit dbt feature mart."""
1297
+ sql = FEATURE_SQL_PATH.read_text(encoding="utf-8") if FEATURE_SQL_PATH.exists() else HERO_EXAMPLE
1298
+ wants_dbt = is_dbt_target(target) or target == "dbt-snowflake"
1299
+ target_d = Dialect.SNOWFLAKE
1300
+ converted, conf, auto, review = translate_sql(sql, Dialect.VERTICA, target_d)
1301
+ obj = MigrationObject(
1302
+ name="churn_features",
1303
+ object_type=ObjectType.SQL_SCRIPT,
1304
+ source_sql=sql,
1305
+ target_sql=converted,
1306
+ )
1307
+ if wants_dbt:
1308
+ files = decompose_to_dbt(obj, Dialect.VERTICA, project_name="ml_feature_mart")
1309
+ out = format_dbt_project(files, max_files=14)
1310
+ else:
1311
+ out = converted
1312
+
1313
+ md = [
1314
+ "### ML / DS feature SQL migration",
1315
+ "",
1316
+ "Legacy Vertica feature engineering SQL → Snowflake"
1317
+ + (" dbt feature mart" if wants_dbt else ""),
1318
+ "",
1319
+ f"**Confidence:** {conf:.0f}%",
1320
+ "",
1321
+ "This path is for **data scientists / ML engineers** migrating training-feature SQL "
1322
+ "into warehouse-native, versioned dbt models.",
1323
+ "",
1324
+ "**Transforms**",
1325
+ ]
1326
+ md.extend(f"- {a}" for a in auto[:10])
1327
+ if review:
1328
+ md.append("")
1329
+ md.append("**Review**")
1330
+ md.extend(f"- {r}" for r in review[:6])
1331
+ return "\n".join(md), out
1332
+
1333
+
1334
+ def analyze_sql_object_ui(sql: str, source: str, target: str) -> tuple:
1335
+ """Gradio-friendly object assess (Plotly → HTML)."""
1336
+ analysis, risk_fig, badge, output, notes = analyze_sql_object(sql, source, target)
1337
+ return analysis, figure_to_html(risk_fig), badge, output, notes
1338
+
1339
+
1340
+ def run_workbench_ui(upload_file, use_sample: bool, source: str, target: str) -> tuple:
1341
+ """Gradio-friendly workbench (Plotly figs → HTML)."""
1342
+ (
1343
+ summary,
1344
+ objects,
1345
+ rationalization,
1346
+ runbook,
1347
+ dbt_preview,
1348
+ validation,
1349
+ metrics,
1350
+ risk_fig,
1351
+ dist_fig,
1352
+ lineage_fig,
1353
+ export,
1354
+ report_data,
1355
+ ) = run_migration_workbench(upload_file, use_sample, source, target)
1356
+ return (
1357
+ summary,
1358
+ objects,
1359
+ rationalization,
1360
+ runbook,
1361
+ dbt_preview,
1362
+ validation,
1363
+ metrics,
1364
+ figure_to_html(risk_fig) if isinstance(risk_fig, go.Figure) else str(risk_fig),
1365
+ figure_to_html(dist_fig) if isinstance(dist_fig, go.Figure) else str(dist_fig),
1366
+ figure_to_html(lineage_fig) if isinstance(lineage_fig, go.Figure) else str(lineage_fig),
1367
+ export,
1368
+ report_data,
1369
+ )
1370
+
1371
+
1372
+ def get_leaderboard_md() -> str:
1373
+ from morphsql.eval.leaderboard import format_leaderboard_md
1374
+
1375
+ return format_leaderboard_md()
1376
+
demo/theme.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UI theme — dark shell with high-contrast tables/files."""
2
+
3
+ C_BG = "#0b1220"
4
+ C_PANEL = "#111827"
5
+ C_BORDER = "#334155"
6
+ C_TEXT = "#f1f5f9"
7
+ C_MUTED = "#cbd5e1"
8
+ C_ACCENT = "#3b82f6"
9
+ C_CODE_BG = "#020617"
10
+ C_CODE_FG = "#93c5fd"
11
+ C_TABLE_BG = "#0f172a"
12
+ C_TABLE_TEXT = "#f8fafc"
13
+ C_TABLE_HEADER = "#1e293b"
14
+
15
+ CUSTOM_CSS = f"""
16
+ .gradio-container {{
17
+ max-width: 1360px !important;
18
+ font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif !important;
19
+ background: {C_BG} !important;
20
+ color: {C_TEXT} !important;
21
+ }}
22
+
23
+ .header-block {{
24
+ border-bottom: 1px solid {C_BORDER};
25
+ padding-bottom: 1.1rem;
26
+ margin-bottom: 1rem;
27
+ }}
28
+ .header-block .eyebrow {{
29
+ font-size: 0.72rem;
30
+ letter-spacing: 0.14em;
31
+ font-weight: 600;
32
+ color: {C_ACCENT} !important;
33
+ margin-bottom: 0.4rem;
34
+ }}
35
+ .header-block h1 {{
36
+ font-size: 1.85rem;
37
+ font-weight: 700;
38
+ margin: 0;
39
+ color: {C_TEXT} !important;
40
+ letter-spacing: -0.02em;
41
+ }}
42
+ .header-block p {{
43
+ margin: 0.5rem 0 0;
44
+ color: {C_MUTED} !important;
45
+ font-size: 1.02rem;
46
+ max-width: 44rem;
47
+ line-height: 1.5;
48
+ }}
49
+ .footer-viral {{
50
+ color: {C_MUTED} !important;
51
+ font-size: 0.78rem;
52
+ margin-top: 1.5rem;
53
+ }}
54
+ .footer-viral a {{
55
+ color: {C_ACCENT} !important;
56
+ }}
57
+
58
+ /* Markdown / prose */
59
+ .prose, .markdown, .md, [class*="prose"] {{
60
+ color: {C_TEXT} !important;
61
+ }}
62
+ .prose h1, .prose h2, .prose h3, .prose h4,
63
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4 {{
64
+ color: {C_TEXT} !important;
65
+ }}
66
+ .prose p, .prose li, .markdown p, .markdown li {{
67
+ color: {C_TEXT} !important;
68
+ }}
69
+ .prose strong, .markdown strong {{
70
+ color: #ffffff !important;
71
+ }}
72
+ .prose a, .markdown a {{
73
+ color: {C_ACCENT} !important;
74
+ }}
75
+ .prose code, .markdown code, code {{
76
+ background: {C_CODE_BG} !important;
77
+ color: {C_CODE_FG} !important;
78
+ border: 1px solid {C_BORDER} !important;
79
+ border-radius: 4px !important;
80
+ padding: 0.1rem 0.35rem !important;
81
+ }}
82
+ .prose pre, .markdown pre, pre {{
83
+ background: {C_CODE_BG} !important;
84
+ color: {C_TEXT} !important;
85
+ border: 1px solid {C_BORDER} !important;
86
+ border-radius: 8px !important;
87
+ }}
88
+ .prose pre code, .markdown pre code, pre code {{
89
+ background: transparent !important;
90
+ border: none !important;
91
+ color: {C_TEXT} !important;
92
+ }}
93
+
94
+ /* Guide markdown tables stay dark */
95
+ .prose table, .markdown table {{
96
+ border-color: {C_BORDER} !important;
97
+ }}
98
+ .prose th, .markdown th {{
99
+ background: {C_TABLE_HEADER} !important;
100
+ color: {C_TABLE_TEXT} !important;
101
+ }}
102
+ .prose td, .markdown td {{
103
+ color: {C_TABLE_TEXT} !important;
104
+ border-color: {C_BORDER} !important;
105
+ background: {C_TABLE_BG} !important;
106
+ }}
107
+
108
+ /* Code / inputs */
109
+ .cm-editor, .cm-content, .cm-line {{
110
+ background: {C_CODE_BG} !important;
111
+ color: {C_TEXT} !important;
112
+ }}
113
+ textarea, input {{
114
+ color: {C_TEXT} !important;
115
+ }}
116
+
117
+ /*
118
+ * CRITICAL: Gradio Dataframe + File often render white cells.
119
+ * Global light text then becomes invisible — force dark surfaces + light text.
120
+ */
121
+ .preview-table table,
122
+ .preview-table th,
123
+ .preview-table td,
124
+ .preview-table .table-wrap,
125
+ .preview-table [data-testid="dataframe"] table,
126
+ .preview-table [data-testid="dataframe"] th,
127
+ .preview-table [data-testid="dataframe"] td {{
128
+ background: {C_TABLE_BG} !important;
129
+ color: {C_TABLE_TEXT} !important;
130
+ border-color: {C_BORDER} !important;
131
+ }}
132
+ .preview-table th {{
133
+ background: {C_TABLE_HEADER} !important;
134
+ font-weight: 600 !important;
135
+ }}
136
+ .preview-table tr:nth-child(even) td {{
137
+ background: #111827 !important;
138
+ }}
139
+
140
+ .download-box,
141
+ .download-box *,
142
+ .download-box a,
143
+ .download-box span,
144
+ .download-box label,
145
+ .download-box .file-preview,
146
+ .download-box [data-testid="file"] {{
147
+ color: {C_TABLE_TEXT} !important;
148
+ }}
149
+ .download-box .file,
150
+ .download-box .file-preview,
151
+ .download-box [data-testid="file"],
152
+ .download-box .wrap,
153
+ .download-box .or {{
154
+ background: {C_TABLE_BG} !important;
155
+ border: 1px solid {C_BORDER} !important;
156
+ border-radius: 8px !important;
157
+ color: {C_TABLE_TEXT} !important;
158
+ }}
159
+ .download-box a {{
160
+ color: {C_ACCENT} !important;
161
+ }}
162
+
163
+ /* Keep Convert / Upload actions compact — don't stretch to file-drop height */
164
+ .action-btn button,
165
+ .upload-action-btn button {{
166
+ height: 42px !important;
167
+ min-height: 42px !important;
168
+ max-height: 48px !important;
169
+ width: 100% !important;
170
+ white-space: normal !important;
171
+ line-height: 1.25 !important;
172
+ padding: 0.55rem 0.85rem !important;
173
+ }}
174
+ .upload-action-col {{
175
+ display: flex !important;
176
+ flex-direction: column !important;
177
+ justify-content: flex-start !important;
178
+ padding-top: 1.55rem !important; /* align with file label */
179
+ }}
180
+ .sql-upload-box {{
181
+ flex-grow: 1 !important;
182
+ }}
183
+ .sql-upload-box [data-testid="file"],
184
+ .sql-upload-box .wrap,
185
+ .sql-upload-box .upload-container {{
186
+ min-height: 7.5rem !important;
187
+ }}
188
+
189
+ /* Fallback for unscoped Gradio dataframe/file widgets */
190
+ [data-testid="dataframe"] table,
191
+ [data-testid="dataframe"] th,
192
+ [data-testid="dataframe"] td {{
193
+ background: {C_TABLE_BG} !important;
194
+ color: {C_TABLE_TEXT} !important;
195
+ border-color: {C_BORDER} !important;
196
+ }}
197
+ [data-testid="file"] .file-preview,
198
+ [data-testid="file"] .file {{
199
+ background: {C_TABLE_BG} !important;
200
+ color: {C_TABLE_TEXT} !important;
201
+ border: 1px solid {C_BORDER} !important;
202
+ }}
203
+
204
+ footer {{ display: none !important; }}
205
+ """
206
+
207
+
208
+ def build_theme():
209
+ import gradio as gr
210
+
211
+ return gr.themes.Base(
212
+ primary_hue=gr.themes.colors.blue,
213
+ neutral_hue=gr.themes.colors.slate,
214
+ font=[gr.themes.GoogleFont("IBM Plex Sans"), "ui-sans-serif", "system-ui", "sans-serif"],
215
+ font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace"],
216
+ ).set(
217
+ body_background_fill=C_BG,
218
+ body_background_fill_dark=C_BG,
219
+ background_fill_primary=C_PANEL,
220
+ background_fill_primary_dark=C_PANEL,
221
+ background_fill_secondary="#0f172a",
222
+ background_fill_secondary_dark="#0f172a",
223
+ border_color_primary=C_BORDER,
224
+ border_color_primary_dark=C_BORDER,
225
+ body_text_color=C_TEXT,
226
+ body_text_color_dark=C_TEXT,
227
+ body_text_color_subdued=C_MUTED,
228
+ body_text_color_subdued_dark=C_MUTED,
229
+ block_background_fill=C_PANEL,
230
+ block_background_fill_dark=C_PANEL,
231
+ block_label_text_color=C_MUTED,
232
+ block_label_text_color_dark=C_MUTED,
233
+ block_title_text_color=C_TEXT,
234
+ block_title_text_color_dark=C_TEXT,
235
+ input_background_fill=C_CODE_BG,
236
+ input_background_fill_dark=C_CODE_BG,
237
+ input_border_color=C_BORDER,
238
+ input_border_color_dark=C_BORDER,
239
+ input_placeholder_color=C_MUTED,
240
+ input_placeholder_color_dark=C_MUTED,
241
+ button_primary_background_fill=C_ACCENT,
242
+ button_primary_background_fill_dark=C_ACCENT,
243
+ button_primary_text_color="#ffffff",
244
+ button_primary_text_color_dark="#ffffff",
245
+ button_secondary_background_fill="#1e293b",
246
+ button_secondary_background_fill_dark="#1e293b",
247
+ button_secondary_text_color=C_TEXT,
248
+ button_secondary_text_color_dark=C_TEXT,
249
+ link_text_color=C_ACCENT,
250
+ link_text_color_dark=C_ACCENT,
251
+ code_background_fill=C_CODE_BG,
252
+ code_background_fill_dark=C_CODE_BG,
253
+ table_even_background_fill=C_TABLE_BG,
254
+ table_even_background_fill_dark=C_TABLE_BG,
255
+ table_odd_background_fill="#111827",
256
+ table_odd_background_fill_dark="#111827",
257
+ table_row_focus=C_TABLE_HEADER,
258
+ table_row_focus_dark=C_TABLE_HEADER,
259
+ table_text_color=C_TABLE_TEXT,
260
+ table_text_color_dark=C_TABLE_TEXT,
261
+ table_radius="8px",
262
+ )
263
+
264
+
265
+ DARK_THEME = None
examples/ml_features/churn_feature_sql.sql ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ML / DS feature engineering SQL (Vertica legacy)
2
+ -- Migrates to Snowflake / dbt feature mart for model training
3
+
4
+ CREATE LOCAL TEMP TABLE tmp_user_events ON COMMIT PRESERVE ROWS AS
5
+ SELECT
6
+ user_id,
7
+ event_ts,
8
+ event_type,
9
+ ZEROIFNULL(event_value) AS event_value
10
+ FROM staging.product_events
11
+ WHERE event_ts >= CURRENT_DATE - 90;
12
+
13
+ WITH user_agg AS (
14
+ SELECT
15
+ user_id,
16
+ COUNT(*) AS event_count_90d,
17
+ COUNT(DISTINCT event_type) AS event_type_nunique,
18
+ SUM(event_value) AS value_sum_90d,
19
+ AVG(event_value) AS value_avg_90d,
20
+ DATEDIFF('day', MAX(event_ts), CURRENT_DATE) AS days_since_last_event
21
+ FROM tmp_user_events
22
+ GROUP BY user_id
23
+ ),
24
+ labels AS (
25
+ SELECT
26
+ user_id,
27
+ CASE WHEN ZEROIFNULL(churned_flag) = 1 THEN 1 ELSE 0 END AS churn_label
28
+ FROM ml.churn_labels
29
+ WHERE label_date = CURRENT_DATE - 1
30
+ )
31
+ SELECT
32
+ a.user_id,
33
+ a.event_count_90d,
34
+ a.event_type_nunique,
35
+ a.value_sum_90d,
36
+ a.value_avg_90d,
37
+ a.days_since_last_event,
38
+ l.churn_label
39
+ FROM user_agg a
40
+ JOIN labels l ON a.user_id = l.user_id;
examples/vertica_legacy/procedures/SP_BUILD_CUSTOMER_DAILY.sql ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Vertica legacy customer metrics stored procedure
2
+ -- Migration candidate: SP_BUILD_CUSTOMER_DAILY
3
+
4
+ CREATE OR REPLACE PROCEDURE SP_BUILD_CUSTOMER_DAILY(load_date DATE)
5
+ AS $$
6
+ BEGIN
7
+ -- Stage customer transactions
8
+ CREATE LOCAL TEMP TABLE tmp_customer_txns ON COMMIT PRESERVE ROWS AS
9
+ SELECT
10
+ customer_id,
11
+ order_id,
12
+ order_date,
13
+ order_amount,
14
+ ZEROIFNULL(discount_amount) AS discount_amount,
15
+ order_amount - ZEROIFNULL(discount_amount) AS net_amount
16
+ FROM staging.customer_transactions
17
+ WHERE order_date = load_date;
18
+
19
+ -- Build daily customer aggregates
20
+ CREATE LOCAL TEMP TABLE tmp_customer_daily ON COMMIT PRESERVE ROWS AS
21
+ SELECT
22
+ customer_id,
23
+ load_date AS activity_date,
24
+ COUNT(DISTINCT order_id) AS order_count,
25
+ SUM(net_amount) AS total_spend,
26
+ MAX(order_date) AS last_order_date,
27
+ AVG(net_amount) AS avg_order_value
28
+ FROM tmp_customer_txns
29
+ GROUP BY customer_id, load_date;
30
+
31
+ -- Classify customer segments
32
+ DELETE FROM analytics.customer_daily WHERE activity_date = load_date;
33
+
34
+ INSERT INTO analytics.customer_daily
35
+ SELECT
36
+ customer_id,
37
+ activity_date,
38
+ order_count,
39
+ total_spend,
40
+ last_order_date,
41
+ avg_order_value,
42
+ CASE
43
+ WHEN order_count >= 5
44
+ AND last_order_date >= load_date - 90
45
+ AND total_spend > 500
46
+ THEN 'HIGH_VALUE'
47
+ WHEN order_count >= 2
48
+ AND total_spend > 100
49
+ THEN 'MEDIUM_VALUE'
50
+ ELSE 'STANDARD'
51
+ END AS customer_segment,
52
+ CASE
53
+ WHEN order_count = 1 THEN 'NEW'
54
+ WHEN last_order_date >= load_date - 30 THEN 'ACTIVE'
55
+ ELSE 'DORMANT'
56
+ END AS activity_status
57
+ FROM tmp_customer_daily;
58
+
59
+ COMMIT;
60
+ END;
61
+ $$;
examples/vertica_legacy/queries/customer_lifetime_value.sql ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Legacy customer lifetime value calculation
2
+ -- 4 CTEs, multiple joins, window functions
3
+
4
+ WITH customer_orders AS (
5
+ SELECT
6
+ customer_id,
7
+ order_id,
8
+ order_date,
9
+ order_amount - ZEROIFNULL(discount_amount) AS net_amount
10
+ FROM staging.customer_transactions
11
+ WHERE order_date >= CURRENT_DATE - 365
12
+ ),
13
+ customer_refunds AS (
14
+ SELECT
15
+ customer_id,
16
+ SUM(refund_amount) AS total_refunds
17
+ FROM staging.refunds
18
+ WHERE refund_date >= CURRENT_DATE - 365
19
+ GROUP BY customer_id
20
+ ),
21
+ customer_summary AS (
22
+ SELECT
23
+ co.customer_id,
24
+ COUNT(DISTINCT co.order_id) AS order_count,
25
+ SUM(co.net_amount) AS gross_spend,
26
+ ZEROIFNULL(cr.total_refunds) AS total_refunds,
27
+ SUM(co.net_amount) - ZEROIFNULL(cr.total_refunds) AS net_spend,
28
+ MAX(co.order_date) AS last_order_date,
29
+ MIN(co.order_date) AS first_order_date
30
+ FROM customer_orders co
31
+ LEFT JOIN customer_refunds cr
32
+ ON co.customer_id = cr.customer_id
33
+ GROUP BY co.customer_id, cr.total_refunds
34
+ ),
35
+ customer_lifetime AS (
36
+ SELECT
37
+ cs.*,
38
+ DATEDIFF('day', cs.first_order_date, cs.last_order_date) AS customer_lifespan_days,
39
+ CASE
40
+ WHEN cs.order_count >= 10 AND cs.net_spend > 1000 THEN 'PLATINUM'
41
+ WHEN cs.order_count >= 5 AND cs.net_spend > 500 THEN 'GOLD'
42
+ WHEN cs.order_count >= 2 THEN 'SILVER'
43
+ ELSE 'BRONZE'
44
+ END AS loyalty_tier
45
+ FROM customer_summary cs
46
+ )
47
+ SELECT
48
+ cl.customer_id,
49
+ c.customer_name,
50
+ cl.order_count,
51
+ cl.gross_spend,
52
+ cl.total_refunds,
53
+ cl.net_spend AS customer_lifetime_value,
54
+ cl.loyalty_tier,
55
+ cl.last_order_date,
56
+ cl.customer_lifespan_days
57
+ FROM customer_lifetime cl
58
+ JOIN staging.customers c
59
+ ON cl.customer_id = c.customer_id
60
+ ORDER BY cl.net_spend DESC;
examples/vertica_legacy/tables/customer_tables.sql ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Customer transactions staging table
2
+ CREATE TABLE staging.customer_transactions (
3
+ customer_id INT NOT NULL,
4
+ order_id INT NOT NULL,
5
+ order_date DATE NOT NULL,
6
+ order_amount DECIMAL(18,2),
7
+ discount_amount DECIMAL(18,2),
8
+ payment_method VARCHAR(50),
9
+ created_at TIMESTAMP DEFAULT SYSDATE
10
+ )
11
+ SEGMENTED BY HASH(customer_id) ALL NODES
12
+ ORDER BY customer_id, order_date;
13
+
14
+ -- Customers dimension
15
+ CREATE TABLE staging.customers (
16
+ customer_id INT NOT NULL PRIMARY KEY,
17
+ customer_name VARCHAR(200),
18
+ email VARCHAR(200),
19
+ registration_date DATE,
20
+ country_code VARCHAR(3),
21
+ is_active INT DEFAULT 1
22
+ )
23
+ SEGMENTED BY HASH(customer_id) ALL NODES;
24
+
25
+ -- Customer daily fact table
26
+ CREATE TABLE analytics.customer_daily (
27
+ customer_id INT NOT NULL,
28
+ activity_date DATE NOT NULL,
29
+ order_count INT,
30
+ total_spend DECIMAL(18,2),
31
+ last_order_date DATE,
32
+ avg_order_value DECIMAL(18,2),
33
+ customer_segment VARCHAR(20),
34
+ activity_status VARCHAR(20)
35
+ )
36
+ SEGMENTED BY HASH(customer_id) ALL NODES
37
+ ORDER BY activity_date, customer_id;
examples/vertica_legacy/tables/refunds.sql ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Refunds staging table (referenced by customer_lifetime_value query)
2
+ CREATE TABLE staging.refunds (
3
+ refund_id INT NOT NULL,
4
+ customer_id INT NOT NULL,
5
+ order_id INT,
6
+ refund_amount DECIMAL(18,2) NOT NULL,
7
+ refund_date DATE NOT NULL,
8
+ reason_code VARCHAR(50),
9
+ created_at TIMESTAMP DEFAULT SYSDATE
10
+ )
11
+ SEGMENTED BY HASH(customer_id) ALL NODES
12
+ ORDER BY refund_date, customer_id;
examples/vertica_legacy/views/V_CUSTOMER_METRICS.sql ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Customer metrics view built on daily aggregates
2
+ CREATE OR REPLACE VIEW analytics.v_customer_metrics AS
3
+ SELECT
4
+ cd.customer_id,
5
+ cd.activity_date,
6
+ cd.order_count,
7
+ cd.total_spend,
8
+ cd.customer_segment,
9
+ cd.activity_status,
10
+ c.customer_name,
11
+ c.registration_date,
12
+ DATEDIFF('day', c.registration_date, cd.activity_date) AS customer_tenure_days,
13
+ SUM(cd.total_spend) OVER (
14
+ PARTITION BY cd.customer_id
15
+ ORDER BY cd.activity_date
16
+ ROWS UNBOUNDED PRECEDING
17
+ ) AS lifetime_value,
18
+ ROW_NUMBER() OVER (
19
+ PARTITION BY cd.customer_id
20
+ ORDER BY cd.activity_date DESC
21
+ ) AS recency_rank
22
+ FROM analytics.customer_daily cd
23
+ JOIN staging.customers c
24
+ ON cd.customer_id = c.customer_id
25
+ WHERE cd.activity_date >= CURRENT_DATE - 365;
model/README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ license: apache-2.0
4
+ library_name: morphsql
5
+ pipeline_tag: text-classification
6
+ tags:
7
+ - code
8
+ - sql
9
+ - agent
10
+ - text-generation
11
+ - text-classification
12
+ - rag
13
+ - sklearn
14
+ - migration
15
+ - snowflake
16
+ - dbt
17
+ datasets:
18
+ - dgvj-work/vertica-snowflake-pairs
19
+ metrics:
20
+ - accuracy
21
+ ---
22
+
23
+ # MorphSQL risk classifier
24
+
25
+ Small **TF-IDF + LogisticRegression** model that scores SQL migration risk (`low` / `medium` / `high`).
26
+
27
+ Used by **MorphSQL** ([Space](https://huggingface.co/spaces/dgvj-work/morphsql)).
28
+ Python package import: `morphsql` (PyPI: `morphsql`).
29
+
30
+ ## Files
31
+ | File | Purpose |
32
+ |------|---------|
33
+ | `risk_classifier.joblib` | sklearn pipeline |
34
+ | `rewrite_vocabulary.json` | common SQL rewrite map |
35
+ | `config.json` | dialects + metadata |
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from huggingface_hub import hf_hub_download
41
+ import joblib
42
+
43
+ path = hf_hub_download(repo_id="dgvj-work/morphsql", filename="risk_classifier.joblib")
44
+ clf = joblib.load(path)
45
+ print(clf.predict(["EXECUTE IMMEDIATE 'SELECT 1'"]))
46
+ ```
47
+
48
+ ```python
49
+ from morphsql.ai import pipeline
50
+ print(pipeline("sql-risk-classification")("CREATE PROCEDURE p AS BEGIN NULL; END;"))
51
+ print(pipeline("sql-migration")("SELECT ZEROIFNULL(a) FROM t"))
52
+ ```
53
+
54
+ ## Links
55
+ - Space: https://huggingface.co/spaces/dgvj-work/morphsql
56
+ - Dataset: https://huggingface.co/datasets/dgvj-work/vertica-snowflake-pairs
57
+ - GitHub: https://github.com/dgvj-work/morphsql
58
+
59
+ Author: Digvijay Waghela · Apache-2.0
model/config.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "morphsql-agent",
3
+ "product_name": "MorphSQL",
4
+ "task": "text-classification",
5
+ "labels": [
6
+ "low",
7
+ "medium",
8
+ "high"
9
+ ],
10
+ "version": "0.4.0",
11
+ "description": "TF-IDF + LogisticRegression risk classifier for MorphSQL",
12
+ "vertica_syntax_patterns": [
13
+ "\\bSEGMENTED\\s+BY\\s+HASH\\s*\\([^)]+\\)\\s*ALL\\s*NODES",
14
+ "\\bENCODED\\s+BY\\s+[^;\\n]+",
15
+ "\\bINCLUDE\\s+SCHEMA\\s+PRIVILEGES\\b",
16
+ "\\bON\\s+COMMIT\\s+PRESERVE\\s+ROWS\\b",
17
+ "\\bPROJECTION\\s+\\w+\\b",
18
+ "(CREATE\\s+(?:OR\\s+REPLACE\\s+)?(?:LOCAL\\s+TEMP|TEMP|TEMPORARY)?\\s*TABLE\\s+[\\w.]+\\s*\\([^;]+\\))\\s*ORDER\\s+BY[^;]+"
19
+ ],
20
+ "supported_sources": [
21
+ "vertica",
22
+ "oracle",
23
+ "redshift",
24
+ "bigquery",
25
+ "snowflake"
26
+ ],
27
+ "supported_targets": [
28
+ "pandas",
29
+ "pyspark",
30
+ "snowflake",
31
+ "dbt-snowflake",
32
+ "bigquery"
33
+ ]
34
+ }
model/rewrite_vocabulary.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ZEROIFNULL": "COALESCE(expr, 0)",
3
+ "NVL": "COALESCE",
4
+ "ISNULL": "COALESCE",
5
+ "STRING_AGG": "LISTAGG",
6
+ "APPROXIMATE_COUNT_DISTINCT": "APPROX_COUNT_DISTINCT",
7
+ "GETDATE": "CURRENT_TIMESTAMP()",
8
+ "SYSDATE": "CURRENT_TIMESTAMP()",
9
+ "IFNULL": "COALESCE",
10
+ "IFF": "IF",
11
+ "LISTAGG": "STRING_AGG"
12
+ }
model/risk_classifier.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:28fed7e2d246113aa81d9c51590e57713c7ec5efd2e9e54ad159118406bad47e
3
+ size 70084
morphsql/__init__.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MorphSQL — AI SQL Migration Agent."""
2
+
3
+ __version__ = "0.4.0"
4
+ __product_name__ = "MorphSQL"
5
+
6
+ from morphsql.models import (
7
+ Dialect,
8
+ MigrationObject,
9
+ MigrationReport,
10
+ ObjectType,
11
+ RiskLevel,
12
+ )
13
+
14
+ __all__ = [
15
+ "__version__",
16
+ "__product_name__",
17
+ "Dialect",
18
+ "MigrationObject",
19
+ "MigrationReport",
20
+ "ObjectType",
21
+ "RiskLevel",
22
+ ]
morphsql/ai/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AI package — agent, risk model, HF pipeline."""
2
+
3
+ from morphsql.ai.agent import SQLMigrationAgent
4
+ from morphsql.ai.pipeline import migrate_sql, pipeline
5
+ from morphsql.ai.risk_model import MigrationRiskModel, get_risk_model, train_and_save
6
+
7
+ __all__ = [
8
+ "SQLMigrationAgent",
9
+ "pipeline",
10
+ "migrate_sql",
11
+ "MigrationRiskModel",
12
+ "get_risk_model",
13
+ "train_and_save",
14
+ ]
morphsql/ai/agent.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tool-calling SQL Migration Agent (AI-first interface)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from dataclasses import asdict, dataclass, field
8
+ from typing import Any, Callable
9
+
10
+ from morphsql.ai.risk_model import get_risk_model
11
+ from morphsql.dbt_generator.decomposer import decompose_to_dbt, format_dbt_project, is_dbt_target
12
+ from morphsql.intelligence.rag import get_rag
13
+ from morphsql.models import Dialect, MigrationObject, ObjectType
14
+ from morphsql.translator.engine import translate_sql
15
+ from morphsql.translator.pandas_codegen import is_pandas_target
16
+ from morphsql.translator.pyspark_codegen import is_pyspark_target
17
+
18
+
19
+ @dataclass
20
+ class AgentMessage:
21
+ role: str
22
+ content: str
23
+ tool: str | None = None
24
+ data: dict[str, Any] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class AgentResult:
29
+ answer: str
30
+ converted_sql: str = ""
31
+ risk: dict[str, Any] = field(default_factory=dict)
32
+ tools_used: list[str] = field(default_factory=list)
33
+ messages: list[AgentMessage] = field(default_factory=list)
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return {
37
+ "answer": self.answer,
38
+ "converted_sql": self.converted_sql,
39
+ "risk": self.risk,
40
+ "tools_used": self.tools_used,
41
+ "messages": [asdict(m) for m in self.messages],
42
+ }
43
+
44
+
45
+ class SQLMigrationAgent:
46
+ """
47
+ AI agent that plans tool calls for SQL migration.
48
+
49
+ Tools: convert_sql, predict_risk, retrieve_behavior, emit_dbt, explain
50
+ """
51
+
52
+ def __init__(self):
53
+ self.risk_model = get_risk_model()
54
+ self.rag = get_rag()
55
+
56
+ def run(
57
+ self,
58
+ prompt: str,
59
+ sql: str = "",
60
+ source: str = "snowflake",
61
+ target: str = "pandas",
62
+ ) -> AgentResult:
63
+ prompt = (prompt or "").strip()
64
+ sql = (sql or "").strip()
65
+ messages: list[AgentMessage] = [
66
+ AgentMessage(role="user", content=prompt or "Migrate this SQL", data={"sql": sql})
67
+ ]
68
+ tools_used: list[str] = []
69
+ converted = ""
70
+ risk: dict[str, Any] = {}
71
+
72
+ intent = self._detect_intent(prompt, sql)
73
+
74
+ if sql and intent in {"convert", "migrate", "dbt", "full", "auto"}:
75
+ converted, conf, auto, review = self._tool_convert(sql, source, target)
76
+ tools_used.append("convert_sql")
77
+ messages.append(
78
+ AgentMessage(
79
+ role="tool",
80
+ tool="convert_sql",
81
+ content=f"Converted with {conf:.0f}% confidence",
82
+ data={"auto": auto, "review": review, "sql": converted},
83
+ )
84
+ )
85
+
86
+ if sql and intent in {"risk", "full", "auto", "convert", "migrate", "dbt"}:
87
+ risk = self._tool_risk(sql)
88
+ tools_used.append("predict_risk")
89
+ messages.append(
90
+ AgentMessage(
91
+ role="tool",
92
+ tool="predict_risk",
93
+ content=f"Risk={risk['label']} ({risk['score']:.2f})",
94
+ data=risk,
95
+ )
96
+ )
97
+
98
+ if intent in {"rag", "behavior", "full", "auto", "explain"} or (
99
+ not sql and prompt
100
+ ):
101
+ rag_q = prompt or sql
102
+ hits = self.rag.retrieve(rag_q, top_k=4, source=source, target=target)
103
+ tools_used.append("retrieve_behavior")
104
+ messages.append(
105
+ AgentMessage(
106
+ role="tool",
107
+ tool="retrieve_behavior",
108
+ content=f"Retrieved {len(hits)} behavior docs",
109
+ data={"hits": [asdict(h) if hasattr(h, "__dataclass_fields__") else h.__dict__ for h in hits]},
110
+ )
111
+ )
112
+
113
+ dbt_out = ""
114
+ if sql and (intent == "dbt" or is_dbt_target(target)):
115
+ dbt_out = self._tool_dbt(sql, converted or sql, source)
116
+ tools_used.append("emit_dbt")
117
+ converted = dbt_out or converted
118
+ messages.append(
119
+ AgentMessage(
120
+ role="tool",
121
+ tool="emit_dbt",
122
+ content="Emitted dbt project scaffold",
123
+ data={"preview": dbt_out[:500]},
124
+ )
125
+ )
126
+
127
+ answer = self._compose_answer(prompt, source, target, converted, risk, messages, intent)
128
+ messages.append(AgentMessage(role="assistant", content=answer))
129
+ return AgentResult(
130
+ answer=answer,
131
+ converted_sql=converted,
132
+ risk=risk,
133
+ tools_used=list(dict.fromkeys(tools_used)),
134
+ messages=messages,
135
+ )
136
+
137
+ def _detect_intent(self, prompt: str, sql: str) -> str:
138
+ p = (prompt or "").lower()
139
+ wants_convert = bool(
140
+ re.search(r"\bconvert\b|\bmigrate\b|\btranslate\b|\btransform\b", p) or sql
141
+ )
142
+ if re.search(r"\bpandas\b|\bpyspark\b|\bspark\b|\bdataframe\b|\bpython\b", p):
143
+ # still convert; target dropdown should match pandas/pyspark
144
+ return "full"
145
+ if re.search(r"\bdbt\b|feature mart", p):
146
+ return "dbt"
147
+ if wants_convert and re.search(r"\brisk\b|complex|score|explain|null|behavior", p):
148
+ return "full"
149
+ if re.search(r"\brisk\b|complex|score", p) and not wants_convert:
150
+ return "risk"
151
+ if re.search(r"\bnull\b|timezone|behavior|rag|difference", p) and not sql:
152
+ return "rag"
153
+ if re.search(r"\bexplain\b|why|how", p) and not wants_convert:
154
+ return "explain"
155
+ if wants_convert:
156
+ return "full"
157
+ return "auto" if sql else "rag"
158
+
159
+ def _tool_convert(self, sql: str, source: str, target: str):
160
+ source_d = Dialect(source)
161
+ if is_pandas_target(target):
162
+ target_d = Dialect.PANDAS
163
+ elif is_pyspark_target(target):
164
+ target_d = Dialect.PYSPARK
165
+ elif is_dbt_target(target):
166
+ target_d = Dialect.SNOWFLAKE
167
+ else:
168
+ target_d = Dialect(target)
169
+ return translate_sql(sql, source_d, target_d)
170
+
171
+ def _tool_risk(self, sql: str) -> dict[str, Any]:
172
+ return self.risk_model.predict(sql)
173
+
174
+ def _tool_dbt(self, source_sql: str, target_sql: str, source: str) -> str:
175
+ obj = MigrationObject(
176
+ name="agent_object",
177
+ object_type=ObjectType.STORED_PROCEDURE
178
+ if "PROCEDURE" in source_sql.upper()
179
+ else ObjectType.SQL_SCRIPT,
180
+ source_sql=source_sql,
181
+ target_sql=target_sql,
182
+ )
183
+ files = decompose_to_dbt(obj, Dialect(source), project_name="ai_agent_project")
184
+ return format_dbt_project(files, max_files=10)
185
+
186
+ def _compose_answer(
187
+ self,
188
+ prompt: str,
189
+ source: str,
190
+ target: str,
191
+ converted: str,
192
+ risk: dict,
193
+ messages: list[AgentMessage],
194
+ intent: str,
195
+ ) -> str:
196
+ lines = [
197
+ f"**MorphSQL Agent** · `{source}` → `{target}`",
198
+ "",
199
+ ]
200
+ if risk:
201
+ lines.append(
202
+ f"**Risk model:** `{risk.get('label')}` "
203
+ f"(score={risk.get('score', 0):.2f})"
204
+ )
205
+ scores = risk.get("scores") or {}
206
+ if scores:
207
+ parts = ", ".join(f"{k}={v:.2f}" for k, v in scores.items())
208
+ lines.append(f"- class probs: {parts}")
209
+ lines.append("")
210
+ for m in messages:
211
+ if m.tool == "retrieve_behavior" and m.data.get("hits"):
212
+ lines.append("**Behavior RAG**")
213
+ for h in m.data["hits"][:3]:
214
+ name = h.get("name", "?")
215
+ rec = h.get("recommendation", "")
216
+ lines.append(f"- **{name}**: {rec}")
217
+ lines.append("")
218
+ if m.tool == "convert_sql" and m.data.get("auto"):
219
+ lines.append("**Transforms**")
220
+ for a in m.data["auto"][:8]:
221
+ lines.append(f"- {a}")
222
+ lines.append("")
223
+ if converted:
224
+ lines.append("**Converted output ready** in the panel (SQL or dbt project).")
225
+ elif not converted and intent == "rag":
226
+ lines.append("Ask a conversion question with SQL pasted, or use Convert mode.")
227
+ if prompt:
228
+ lines.append("")
229
+ lines.append(f"_User intent:_ {prompt}")
230
+ return "\n".join(lines)
231
+
232
+
233
+ def chat_agent(
234
+ message: str,
235
+ history: list | None,
236
+ sql: str,
237
+ source: str,
238
+ target: str,
239
+ ) -> tuple[list, str, str, str]:
240
+ """Gradio chat adapter: returns history, cleared input, converted sql, risk badge."""
241
+ history = list(history or [])
242
+ agent = SQLMigrationAgent()
243
+ result = agent.run(message, sql=sql, source=source, target=target)
244
+ history.append({"role": "user", "content": message or "(run tools on SQL)"})
245
+ history.append({"role": "assistant", "content": result.answer})
246
+ badge = ""
247
+ if result.risk:
248
+ badge = f"{result.risk.get('label', '?')} · {result.risk.get('score', 0):.2f}"
249
+ tools = ", ".join(result.tools_used) or "none"
250
+ badge = f"{badge} · tools: {tools}".strip(" ·")
251
+ return history, "", result.converted_sql, badge
morphsql/ai/pipeline.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face-style pipeline API for SQL migration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from morphsql.ai.agent import SQLMigrationAgent
8
+ from morphsql.ai.risk_model import get_risk_model
9
+ from morphsql.models import Dialect
10
+ from morphsql.translator.engine import translate_sql
11
+ from morphsql.translator.pandas_codegen import is_pandas_target
12
+ from morphsql.translator.pyspark_codegen import is_pyspark_target
13
+
14
+
15
+ def pipeline(task: str = "sql-migration", **kwargs):
16
+ """
17
+ Mimic transformers.pipeline entrypoint for Hub users.
18
+
19
+ Examples:
20
+ pipe = pipeline("sql-migration")
21
+ pipe("SELECT COALESCE(a, 0) FROM t", source="snowflake", target="pandas")
22
+
23
+ pipe = pipeline("sql-risk-classification")
24
+ pipe("CREATE PROCEDURE ...")
25
+ """
26
+ task = (task or "sql-migration").lower().replace("_", "-")
27
+ if task in {"sql-migration", "sql-translation", "text2sql-migration", "sql-to-pandas"}:
28
+ return SQLMigrationPipeline(**kwargs)
29
+ if task in {"sql-risk-classification", "text-classification", "risk"}:
30
+ return SQLRiskPipeline(**kwargs)
31
+ raise ValueError(
32
+ f"Unknown task '{task}'. Use 'sql-migration' or 'sql-risk-classification'."
33
+ )
34
+
35
+
36
+ class SQLMigrationPipeline:
37
+ def __init__(self, source: str = "snowflake", target: str = "pandas"):
38
+ self.source = source
39
+ self.target = target
40
+ self.agent = SQLMigrationAgent()
41
+
42
+ def __call__(
43
+ self,
44
+ sql: str,
45
+ source: str | None = None,
46
+ target: str | None = None,
47
+ prompt: str = "Convert and explain",
48
+ **_: Any,
49
+ ) -> dict[str, Any]:
50
+ source = source or self.source
51
+ target = target or self.target
52
+ result = self.agent.run(prompt, sql=sql, source=source, target=target)
53
+ return result.to_dict()
54
+
55
+
56
+ class SQLRiskPipeline:
57
+ def __init__(self):
58
+ self.model = get_risk_model()
59
+
60
+ def __call__(self, sql: str, **_: Any) -> dict[str, Any]:
61
+ return self.model.predict(sql)
62
+
63
+
64
+ def migrate_sql(sql: str, source: str = "snowflake", target: str = "pandas") -> dict[str, Any]:
65
+ """One-liner helper used in model card examples."""
66
+ if is_pandas_target(target):
67
+ out_target = Dialect.PANDAS
68
+ elif is_pyspark_target(target):
69
+ out_target = Dialect.PYSPARK
70
+ elif target == "dbt-snowflake":
71
+ out_target = Dialect.SNOWFLAKE
72
+ else:
73
+ out_target = Dialect(target)
74
+ out_sql, conf, auto, review = translate_sql(sql, Dialect(source), out_target)
75
+ risk = get_risk_model().predict(sql)
76
+ return {
77
+ "converted_sql": out_sql,
78
+ "confidence": conf,
79
+ "transforms": auto,
80
+ "review": review,
81
+ "risk": risk,
82
+ }
morphsql/ai/risk_model.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train and load the SQL migration risk classifier (downloadable HF model artifact)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ import joblib
10
+ import numpy as np
11
+ from sklearn.feature_extraction.text import TfidfVectorizer
12
+ from sklearn.linear_model import LogisticRegression
13
+ from sklearn.pipeline import Pipeline as SkPipeline
14
+
15
+ MODEL_DIR = Path(__file__).resolve().parent.parent.parent / "model"
16
+ MODEL_PATH = MODEL_DIR / "risk_classifier.joblib"
17
+ CONFIG_PATH = MODEL_DIR / "config.json"
18
+
19
+ LABELS = ["low", "medium", "high"]
20
+
21
+
22
+ def _synthetic_corpus() -> tuple[list[str], list[str]]:
23
+ """Build labeled SQL snippets for a lightweight risk classifier."""
24
+ texts: list[str] = []
25
+ labels: list[str] = []
26
+
27
+ low = [
28
+ "SELECT a, b FROM t",
29
+ "SELECT COUNT(*) FROM orders WHERE dt = CURRENT_DATE",
30
+ "SELECT COALESCE(x, 0) FROM staging.facts",
31
+ "CREATE VIEW v AS SELECT id FROM customers",
32
+ "SELECT * FROM users LIMIT 100",
33
+ ]
34
+ medium = [
35
+ "SELECT ZEROIFNULL(amount) FROM t WHERE d >= CURRENT_DATE - 30",
36
+ "WITH cte AS (SELECT * FROM a JOIN b ON a.id=b.id) SELECT * FROM cte",
37
+ "SELECT STRING_AGG(name, ',') FROM users GROUP BY dept",
38
+ "CREATE LOCAL TEMP TABLE tmp AS SELECT * FROM staging.x",
39
+ "SELECT NVL(a,0), SYSDATE FROM dual",
40
+ ]
41
+ high = [
42
+ "CREATE OR REPLACE PROCEDURE p(load_date DATE) AS $$ BEGIN "
43
+ "EXECUTE IMMEDIATE 'SELECT 1'; OPEN cur FOR SELECT * FROM t; "
44
+ "EXCEPTION WHEN OTHERS THEN NULL; END; $$;",
45
+ "CREATE PACKAGE pkg AS PROCEDURE run; END;",
46
+ "MERGE INTO tgt USING src ON tgt.id=src.id WHEN MATCHED THEN UPDATE "
47
+ "WHEN NOT MATCHED THEN INSERT VALUES (src.id)",
48
+ "SELECT * FROM t START WITH id=1 CONNECT BY PRIOR id = parent_id",
49
+ "CREATE OR REPLACE PROCEDURE sp AS $$ BEGIN "
50
+ "FOR r IN cur LOOP EXECUTE IMMEDIATE dyn; END LOOP; END; $$;",
51
+ ]
52
+
53
+ for i in range(40):
54
+ for s in low:
55
+ texts.append(f"{s} -- sample {i}")
56
+ labels.append("low")
57
+ for s in medium:
58
+ texts.append(f"{s} /* var {i} */")
59
+ labels.append("medium")
60
+ for s in high:
61
+ texts.append(f"{s}\n-- complexity {i}")
62
+ labels.append("high")
63
+
64
+ # Extra keyword-heavy highs
65
+ for kw in ("CURSOR", "EXECUTE IMMEDIATE", "CONNECT BY", "PACKAGE", "EXCEPTION WHEN"):
66
+ for i in range(20):
67
+ texts.append(f"SELECT 1 FROM t -- {kw} marker {i}")
68
+ labels.append("high" if kw != "SELECT" else "low")
69
+
70
+ return texts, labels
71
+
72
+
73
+ def train_and_save(model_dir: Path | None = None, *, force: bool = False) -> Path:
74
+ global _risk_singleton
75
+ model_dir = Path(model_dir or MODEL_DIR)
76
+ model_dir.mkdir(parents=True, exist_ok=True)
77
+ out = model_dir / "risk_classifier.joblib"
78
+ if out.exists() and not force:
79
+ return out
80
+ texts, labels = _synthetic_corpus()
81
+ clf = SkPipeline(
82
+ steps=[
83
+ (
84
+ "tfidf",
85
+ TfidfVectorizer(
86
+ analyzer="char_wb",
87
+ ngram_range=(3, 5),
88
+ min_df=2,
89
+ max_features=12000,
90
+ ),
91
+ ),
92
+ (
93
+ "lr",
94
+ LogisticRegression(
95
+ max_iter=400,
96
+ class_weight="balanced",
97
+ ),
98
+ ),
99
+ ]
100
+ )
101
+ clf.fit(texts, labels)
102
+ joblib.dump(clf, model_dir / "risk_classifier.joblib")
103
+
104
+ # Export rewrite rule “weights” as a downloadable JSON artifact
105
+ from morphsql.translator.engine import VERTICA_SYNTAX_REPLACEMENTS
106
+
107
+ rules = {
108
+ "model_type": "morphsql-agent",
109
+ "product_name": "MorphSQL",
110
+ "task": "text-classification",
111
+ "labels": LABELS,
112
+ "version": "0.4.0",
113
+ "description": "TF-IDF + LogisticRegression risk classifier for MorphSQL",
114
+ "vertica_syntax_patterns": [p for p, _ in VERTICA_SYNTAX_REPLACEMENTS],
115
+ "supported_sources": ["vertica", "oracle", "redshift", "bigquery", "snowflake"],
116
+ "supported_targets": ["pandas", "pyspark", "snowflake", "dbt-snowflake", "bigquery"],
117
+ }
118
+ (model_dir / "config.json").write_text(json.dumps(rules, indent=2), encoding="utf-8")
119
+ (model_dir / "rewrite_vocabulary.json").write_text(
120
+ json.dumps(
121
+ {
122
+ "ZEROIFNULL": "COALESCE(expr, 0)",
123
+ "NVL": "COALESCE",
124
+ "ISNULL": "COALESCE",
125
+ "STRING_AGG": "LISTAGG",
126
+ "APPROXIMATE_COUNT_DISTINCT": "APPROX_COUNT_DISTINCT",
127
+ "GETDATE": "CURRENT_TIMESTAMP()",
128
+ "SYSDATE": "CURRENT_TIMESTAMP()",
129
+ "IFNULL": "COALESCE",
130
+ "IFF": "IF",
131
+ "LISTAGG": "STRING_AGG",
132
+ },
133
+ indent=2,
134
+ ),
135
+ encoding="utf-8",
136
+ )
137
+ _risk_singleton = None
138
+ return model_dir / "risk_classifier.joblib"
139
+
140
+
141
+ def _load_compatible_model(path: Path):
142
+ """Load the classifier, retraining when sklearn pickle versions disagree."""
143
+ if not path.exists():
144
+ train_and_save(path.parent)
145
+ try:
146
+ model = joblib.load(path)
147
+ model.predict_proba(["SELECT 1"])
148
+ return model
149
+ except Exception:
150
+ train_and_save(path.parent, force=True)
151
+ return joblib.load(path)
152
+
153
+
154
+ class MigrationRiskModel:
155
+ """Loadable AI risk head used by the agent and HF model repo."""
156
+
157
+ def __init__(self, model_path: Path | None = None):
158
+ path = Path(model_path or MODEL_PATH)
159
+ self.model = _load_compatible_model(path)
160
+ self.labels = LABELS
161
+
162
+ def predict(self, sql: str) -> dict:
163
+ sql = sql or ""
164
+ proba = self.model.predict_proba([sql])[0]
165
+ classes = list(self.model.classes_)
166
+ idx = int(np.argmax(proba))
167
+ label = classes[idx]
168
+ # Heuristic boost for known hard patterns
169
+ hard = bool(
170
+ re.search(
171
+ r"EXECUTE\s+IMMEDIATE|\bCURSOR\b|CONNECT\s+BY|\bPACKAGE\b|EXCEPTION\s+WHEN",
172
+ sql,
173
+ re.I,
174
+ )
175
+ )
176
+ if hard and label != "high":
177
+ label = "high"
178
+ proba = np.array([0.05, 0.15, 0.80])
179
+ classes = LABELS
180
+ idx = 2
181
+ return {
182
+ "label": str(label),
183
+ "score": float(proba[idx]),
184
+ "scores": {str(c): float(p) for c, p in zip(classes, proba)},
185
+ }
186
+
187
+
188
+ _risk_singleton: MigrationRiskModel | None = None
189
+
190
+
191
+ def get_risk_model() -> MigrationRiskModel:
192
+ global _risk_singleton
193
+ if _risk_singleton is None:
194
+ _risk_singleton = MigrationRiskModel()
195
+ return _risk_singleton
morphsql/assistant/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Migration copilot assistant."""
2
+
3
+ from morphsql.assistant.copilot import MigrationCopilot
4
+
5
+ __all__ = ["MigrationCopilot"]
morphsql/assistant/copilot.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM-powered migration copilot with Hugging Face Inference API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+
8
+ from morphsql.knowledge.behavior import BEHAVIOR_DIFFERENCES
9
+ from morphsql.models import MigrationReport
10
+ from morphsql.risk.scorer import recommend_workload_action
11
+
12
+ DEFAULT_MODEL = os.getenv(
13
+ "MORPHSQL_MODEL",
14
+ os.getenv("MIGRATIONIQ_MODEL", "Qwen/Qwen2.5-3B-Instruct"),
15
+ )
16
+
17
+ SYSTEM_PROMPT = """You are MorphSQL Copilot, an expert data platform migration advisor.
18
+
19
+ You help data engineers plan warehouse migrations (Vertica, Oracle, Redshift → Snowflake, dbt, BigQuery).
20
+
21
+ Expertise:
22
+ - Repository discovery, dependency lineage, impact analysis
23
+ - Migration risk scoring and workload rationalization (migrate / rewrite / retire)
24
+ - Hybrid SQL translation and stored-procedure → dbt decomposition
25
+ - Semantic validation, reconciliation testing, platform behavior differences
26
+ - Cutover planning, incremental strategies, cost estimation
27
+
28
+ Rules:
29
+ - Answer like a senior migration consultant: clear, structured, actionable.
30
+ - Ground answers in the migration context when provided.
31
+ - Never claim automatic correctness for procedural SQL, dynamic SQL, or cursors.
32
+ - Prefer numbered steps and short tables over fluff.
33
+ """
34
+
35
+
36
+ class MigrationCopilot:
37
+ """Grounded migration copilot using HF Inference API with structured fallback."""
38
+
39
+ def __init__(self, model: str | None = None):
40
+ self.model = model or DEFAULT_MODEL
41
+ self._client = None
42
+ self._client_checked = False
43
+
44
+ @property
45
+ def client(self):
46
+ if self._client_checked:
47
+ return self._client
48
+ self._client_checked = True
49
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
50
+ if not token:
51
+ self._client = False
52
+ return self._client
53
+ try:
54
+ from huggingface_hub import InferenceClient
55
+
56
+ self._client = InferenceClient(token=token)
57
+ except Exception:
58
+ self._client = False
59
+ return self._client
60
+
61
+ def build_context(
62
+ self,
63
+ report: MigrationReport | None = None,
64
+ sql_snippet: str = "",
65
+ source: str = "vertica",
66
+ target: str = "snowflake",
67
+ ) -> str:
68
+ parts = [f"Migration route: {source} → {target}"]
69
+
70
+ if sql_snippet.strip():
71
+ snippet = sql_snippet.strip()[:3000]
72
+ parts.append(f"\n### Active SQL (truncated)\n```sql\n{snippet}\n```")
73
+
74
+ if report is None:
75
+ parts.append("\nNo repository scan loaded.")
76
+ return "\n".join(parts)
77
+
78
+ d = report.dashboard
79
+ parts.append(
80
+ f"\n### Repository scan: {report.repository_path}\n"
81
+ f"- Objects discovered: {d.total_objects}\n"
82
+ f"- Auto-migratable: {d.auto_migratable}\n"
83
+ f"- Needs review: {d.requires_review}\n"
84
+ f"- Manual redesign: {d.requires_redesign}\n"
85
+ f"- Retire/consolidate: {d.recommended_retirement}\n"
86
+ f"- Avg risk score: {d.migration_risk_score:.0f}/100\n"
87
+ f"- Lineage coverage: {d.lineage_coverage_pct:.0f}%\n"
88
+ f"- Validation pass: {d.validation_passed_pct:.0f}%\n"
89
+ f"- Est. annual savings: ${d.estimated_annual_savings_usd[0]:,.0f}–"
90
+ f"${d.estimated_annual_savings_usd[1]:,.0f}"
91
+ )
92
+
93
+ if report.objects:
94
+ parts.append("\n### Top objects by complexity")
95
+ for obj in sorted(report.objects, key=lambda o: o.complexity_score, reverse=True)[:10]:
96
+ action = recommend_workload_action(obj)
97
+ parts.append(
98
+ f"- **{obj.name}** ({obj.object_type.value}): "
99
+ f"complexity {obj.complexity_score}/100, "
100
+ f"risk {obj.risk_level.value}, "
101
+ f"confidence {obj.conversion_confidence:.0f}%, "
102
+ f"action={action}"
103
+ )
104
+
105
+ if report.retirement_candidates:
106
+ parts.append("\n### Retirement candidates")
107
+ for r in report.retirement_candidates[:8]:
108
+ parts.append(f"- {r}")
109
+
110
+ if report.behavior_warnings:
111
+ parts.append("\n### Behavior warnings")
112
+ for w in report.behavior_warnings[:6]:
113
+ parts.append(f"- {w[:220]}")
114
+
115
+ return "\n".join(parts)
116
+
117
+ def respond(
118
+ self,
119
+ message: str,
120
+ history: list[dict[str, str]],
121
+ report: MigrationReport | None = None,
122
+ sql_snippet: str = "",
123
+ source: str = "vertica",
124
+ target: str = "snowflake",
125
+ ) -> str:
126
+ if not message.strip():
127
+ return (
128
+ "Ask about migration scope, what to migrate first, lineage risk, "
129
+ "dbt strategy, validation, or cutover planning."
130
+ )
131
+
132
+ context = self.build_context(report, sql_snippet, source, target)
133
+ full_system = f"{SYSTEM_PROMPT}\n\n## Current session context\n{context}"
134
+
135
+ llm_reply = self._call_llm(full_system, history, message.strip())
136
+ if llm_reply:
137
+ return llm_reply
138
+
139
+ return self._fallback(message.strip(), report, sql_snippet, source, target)
140
+
141
+ def _call_llm(
142
+ self,
143
+ system: str,
144
+ history: list[dict[str, str]],
145
+ message: str,
146
+ ) -> str | None:
147
+ if not self.client:
148
+ return None
149
+
150
+ messages: list[dict[str, str]] = [{"role": "system", "content": system}]
151
+ for turn in history[-8:]:
152
+ role = turn.get("role", "user")
153
+ content = turn.get("content", "")
154
+ if role in ("user", "assistant") and content:
155
+ messages.append({"role": role, "content": content})
156
+ messages.append({"role": "user", "content": message})
157
+
158
+ try:
159
+ response = self.client.chat_completion(
160
+ model=self.model,
161
+ messages=messages,
162
+ max_tokens=900,
163
+ temperature=0.2,
164
+ )
165
+ content = response.choices[0].message.content
166
+ return content.strip() if content else None
167
+ except Exception:
168
+ return None
169
+
170
+ def _fallback(
171
+ self,
172
+ message: str,
173
+ report: MigrationReport | None,
174
+ sql_snippet: str,
175
+ source: str,
176
+ target: str,
177
+ ) -> str:
178
+ """Grounded consultant-style answers when HF token is not available."""
179
+ msg = message.lower()
180
+ sections: list[str] = []
181
+
182
+ # Always acknowledge route
183
+ sections.append(f"Looking at **{source} → {target}**.")
184
+
185
+ if report:
186
+ d = report.dashboard
187
+ ranked = sorted(report.objects, key=lambda o: o.complexity_score, reverse=True)
188
+
189
+ if any(k in msg for k in ("first", "priority", "start", "order", "sequence")):
190
+ low = [o for o in ranked if o.complexity_score < 30][::-1] or ranked[::-1]
191
+ high = [o for o in ranked if o.complexity_score >= 50]
192
+ sections.append(
193
+ f"\n**Recommended order** based on your scan of {d.total_objects} objects:\n"
194
+ "1. Convert low-risk objects first (build confidence and pipeline)\n"
195
+ "2. Parallel-run with reconciliation tests\n"
196
+ "3. Redesign high-complexity procedures last\n"
197
+ )
198
+ if low:
199
+ sections.append("**Start with:**")
200
+ for o in low[:4]:
201
+ sections.append(
202
+ f"- `{o.name}` — complexity {o.complexity_score}/100, "
203
+ f"{o.risk_level.value} risk"
204
+ )
205
+ if high:
206
+ sections.append("\n**Defer / redesign:**")
207
+ for o in high[:4]:
208
+ sections.append(
209
+ f"- `{o.name}` — complexity {o.complexity_score}/100 "
210
+ f"({recommend_workload_action(o).replace('_', ' ')})"
211
+ )
212
+ return "\n".join(sections)
213
+
214
+ if any(k in msg for k in ("scope", "summary", "overview", "dashboard", "status")):
215
+ sections.append(
216
+ f"\n**Current portfolio**\n"
217
+ f"- Objects: **{d.total_objects}**\n"
218
+ f"- Auto-migratable: **{d.auto_migratable}**\n"
219
+ f"- Needs review: **{d.requires_review}**\n"
220
+ f"- Redesign: **{d.requires_redesign}**\n"
221
+ f"- Retire/consolidate: **{d.recommended_retirement}**\n"
222
+ f"- Avg risk: **{d.migration_risk_score:.0f}/100**\n"
223
+ f"- Est. savings: **${d.estimated_annual_savings_usd[0]:,.0f}–"
224
+ f"${d.estimated_annual_savings_usd[1]:,.0f}/yr**"
225
+ )
226
+ if ranked:
227
+ sections.append("\n**Highest complexity objects:**")
228
+ for o in ranked[:5]:
229
+ sections.append(
230
+ f"- `{o.name}` ({o.object_type.value}) — "
231
+ f"{o.complexity_score}/100, confidence {o.conversion_confidence:.0f}%"
232
+ )
233
+ return "\n".join(sections)
234
+
235
+ if any(k in msg for k in ("retire", "rational", "consolidat", "unused", "orphan")):
236
+ sections.append("\n**Workload rationalization**")
237
+ for o in ranked:
238
+ action = recommend_workload_action(o)
239
+ if action in ("retire", "manual_redesign", "rewrite"):
240
+ sections.append(
241
+ f"- `{o.name}` → **{action.replace('_', ' ')}** "
242
+ f"(complexity {o.complexity_score})"
243
+ )
244
+ if report.retirement_candidates:
245
+ sections.append("\n**Retirement candidates from lineage:**")
246
+ for r in report.retirement_candidates[:6]:
247
+ sections.append(f"- {r}")
248
+ if len(sections) == 2:
249
+ sections.append("No strong retirement candidates in this sample. Focus on rewrite candidates.")
250
+ return "\n".join(sections)
251
+
252
+ if "lineage" in msg or "depend" in msg or "impact" in msg:
253
+ sections.append(
254
+ f"\nLineage coverage is **{d.lineage_coverage_pct:.0f}%**. "
255
+ "Open **More → Repository workbench** for the interactive lineage graph.\n\n"
256
+ "**Cutover risk tip:** migrate objects with few downstream dependents first; "
257
+ "leave hubs (high fan-out) for later phases with extra validation."
258
+ )
259
+ return "\n".join(sections)
260
+
261
+ if any(k in msg for k in ("validat", "reconcil", "test", "checksum")):
262
+ sections.append(
263
+ f"\nValidation pass rate in last scan: **{d.validation_passed_pct:.0f}%**.\n\n"
264
+ "**Minimum reconciliation suite:**\n"
265
+ "1. Row counts (source vs target)\n"
266
+ "2. Null rates on string columns (empty-string vs NULL)\n"
267
+ "3. Aggregate metric tolerance for financial columns\n"
268
+ "4. Incremental replay for delete+reload patterns\n"
269
+ "5. Spot-check business-rule CASE classifications"
270
+ )
271
+ return "\n".join(sections)
272
+
273
+ # SQL / dialect questions
274
+ if re.search(r"zeroifnull|nvl|isnull", msg):
275
+ sections.append(
276
+ "\n**Function mapping**\n"
277
+ "- `ZEROIFNULL(x)` → `COALESCE(x, 0)`\n"
278
+ "- `NVL(x, y)` / `ISNULL(x, y)` → `COALESCE(x, y)`\n\n"
279
+ "After conversion, validate NULL rates — Vertica/Oracle empty-string "
280
+ "semantics can differ from Snowflake."
281
+ )
282
+ return "\n".join(sections)
283
+
284
+ if any(k in msg for k in ("datediff", "dateadd", "date arithmetic", "timezone")):
285
+ sections.append(
286
+ "\n**Date handling**\n"
287
+ "- Vertica `DATEDIFF('day', a, b)` → Snowflake `DATEDIFF(day, a, b)`\n"
288
+ "- `col - 90` → `DATEADD(day, -90, col)`\n"
289
+ "- Normalize timestamps to UTC before period aggregations"
290
+ )
291
+ return "\n".join(sections)
292
+
293
+ if any(k in msg for k in ("procedure", "stored proc", "pl/sql")):
294
+ sections.append(
295
+ "\n**Procedure migration**\n"
296
+ "1. Convert wrapper to Snowflake `LANGUAGE SQL` with `:PARAM` bindings\n"
297
+ "2. Replace `LOCAL TEMP` with `CREATE OR REPLACE TEMPORARY TABLE`\n"
298
+ "3. Prefer decomposing into dbt staging → intermediate → mart\n"
299
+ "4. Manually review cursors, dynamic SQL, and exception handlers"
300
+ )
301
+ return "\n".join(sections)
302
+
303
+ if "dbt" in msg:
304
+ sections.append(
305
+ "\n**dbt architecture approach**\n"
306
+ "1. Sources for landing/staging tables\n"
307
+ "2. Staging models = 1:1 cleansed sources\n"
308
+ "3. Intermediate models = business transforms (CTEs)\n"
309
+ "4. Marts = consumer-facing tables\n"
310
+ "5. Incremental strategy: delete+insert for day-reload patterns; merge for CDC\n"
311
+ "6. Add schema tests (`unique`, `not_null`) on keys"
312
+ )
313
+ return "\n".join(sections)
314
+
315
+ if any(k in msg for k in ("cutover", "plan", "roadmap", "phase")):
316
+ sections.append(
317
+ "\n**Suggested cutover plan**\n"
318
+ "1. Discovery + lineage (**More → Repository workbench**)\n"
319
+ "2. Migrate low-risk objects + generate dbt scaffold\n"
320
+ "3. Parallel run with reconciliation tests\n"
321
+ "4. Redesign high-risk procedures\n"
322
+ "5. Retire orphaned / unused assets\n"
323
+ "6. Production cutover with rollback window"
324
+ )
325
+ if report:
326
+ d = report.dashboard
327
+ sections.append(
328
+ f"\nWith your current scan: start with **{d.auto_migratable}** auto-migratable "
329
+ f"objects, then schedule **{d.requires_review + d.requires_redesign}** for engineering."
330
+ )
331
+ return "\n".join(sections)
332
+
333
+ if sql_snippet.strip() and any(k in msg for k in ("this", "sql", "convert", "what", "explain")):
334
+ sections.append(
335
+ "\nI see SQL loaded in context. Use **More → Object assess & convert** "
336
+ "for dialect output and risk score. Ask specifically about functions, "
337
+ "incremental strategy, or dbt decomposition for this object."
338
+ )
339
+ return "\n".join(sections)
340
+
341
+ # Default helpful response
342
+ diffs = [d for d in BEHAVIOR_DIFFERENCES if d.source_platform == source][:3]
343
+ sections.append(
344
+ "\nI can answer like a migration consultant on:\n"
345
+ "- What to migrate first / portfolio risk\n"
346
+ "- Lineage impact and retirement candidates\n"
347
+ "- ZEROIFNULL / dates / procedures / dbt strategy\n"
348
+ "- Validation and cutover planning\n"
349
+ )
350
+ if report:
351
+ sections.append(
352
+ f"A scan is loaded ({report.dashboard.total_objects} objects). "
353
+ "Try: *What should we migrate first?*"
354
+ )
355
+ else:
356
+ sections.append(
357
+ "No scan loaded yet — run **More → Repository workbench** first for grounded answers, "
358
+ "or ask a dialect/process question."
359
+ )
360
+ if diffs:
361
+ sections.append("\n**Platform behaviors to watch:**")
362
+ for d in diffs:
363
+ sections.append(f"- {d.description}")
364
+
365
+ if not (os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")):
366
+ sections.append(
367
+ "\n_Tip: set `HF_TOKEN` for full LLM responses via Hugging Face Inference._"
368
+ )
369
+
370
+ return "\n".join(sections)
morphsql/cli.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MorphSQL CLI — `morphsql` command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from enum import Enum
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+ from morphsql import __product_name__, __version__
16
+ from morphsql.models import Dialect
17
+ from morphsql.pipeline import MigrationPipeline
18
+ from morphsql.translator.engine import translate_sql
19
+
20
+ app = typer.Typer(
21
+ name="morphsql",
22
+ help=f"{__product_name__} — AI-powered SQL migration toolkit",
23
+ add_completion=False,
24
+ )
25
+ console = Console()
26
+
27
+
28
+ class SourceDialect(str, Enum):
29
+ snowflake = "snowflake"
30
+ bigquery = "bigquery"
31
+ redshift = "redshift"
32
+ oracle = "oracle"
33
+ vertica = "vertica"
34
+
35
+
36
+ class TargetDialect(str, Enum):
37
+ pandas = "pandas"
38
+ pyspark = "pyspark"
39
+ snowflake = "snowflake"
40
+ dbt_snowflake = "dbt-snowflake"
41
+ bigquery = "bigquery"
42
+
43
+
44
+ def _to_dialect(d: SourceDialect | TargetDialect) -> Dialect:
45
+ return Dialect(d.value)
46
+
47
+
48
+ @app.callback()
49
+ def main():
50
+ """MorphSQL — migration intelligence toolkit."""
51
+ pass
52
+
53
+
54
+ @app.command()
55
+ def analyze(
56
+ path: str = typer.Argument(..., help="Path to SQL repository (directory or .zip)"),
57
+ source: SourceDialect = typer.Option(SourceDialect.snowflake, "--source", "-s"),
58
+ target: TargetDialect = typer.Option(TargetDialect.pandas, "--target", "-t"),
59
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output directory"),
60
+ ):
61
+ """Analyze a legacy SQL repository for migration complexity and risk."""
62
+ console.print(Panel(f"[bold cyan]{__product_name__}[/] v{__version__} — Analyze", expand=False))
63
+
64
+ pipeline = MigrationPipeline(source=_to_dialect(source), target=_to_dialect(target))
65
+ report = pipeline.analyze(path)
66
+
67
+ _print_dashboard(report)
68
+
69
+ table = Table(title="Object Assessment")
70
+ table.add_column("Object", style="cyan")
71
+ table.add_column("Type")
72
+ table.add_column("Complexity", justify="right")
73
+ table.add_column("Risk")
74
+ table.add_column("Category")
75
+
76
+ for obj in report.objects:
77
+ risk_color = {"low": "green", "medium": "yellow", "high": "red", "critical": "bold red"}
78
+ table.add_row(
79
+ obj.name,
80
+ obj.object_type.value,
81
+ f"{obj.complexity_score}/100",
82
+ f"[{risk_color.get(obj.risk_level.value, 'white')}]{obj.risk_level.value}[/]",
83
+ obj.migration_category.value.replace("_", " "),
84
+ )
85
+
86
+ console.print(table)
87
+
88
+ if output:
89
+ out = Path(output)
90
+ out.mkdir(parents=True, exist_ok=True)
91
+ pipeline.generate_report(report, out / "migration_report.html")
92
+ with open(out / "analysis.json", "w") as f:
93
+ json.dump(report.model_dump(), f, indent=2, default=str)
94
+ console.print(f"\n[green]✓[/] Report written to {out}")
95
+
96
+
97
+ @app.command()
98
+ def convert(
99
+ path: str = typer.Argument(..., help="Path to SQL file or repository"),
100
+ source: SourceDialect = typer.Option(SourceDialect.snowflake, "--source", "-s"),
101
+ target: TargetDialect = typer.Option(TargetDialect.pandas, "--target", "-t"),
102
+ output: Optional[str] = typer.Option(None, "--output", "-o"),
103
+ generate_tests: bool = typer.Option(False, "--generate-tests"),
104
+ generate_lineage: bool = typer.Option(False, "--generate-lineage"),
105
+ generate_dbt: bool = typer.Option(False, "--generate-dbt"),
106
+ ):
107
+ """Convert legacy SQL to target platform dialect."""
108
+ from morphsql.dbt_generator.decomposer import (
109
+ decompose_to_dbt,
110
+ format_dbt_project,
111
+ is_dbt_target,
112
+ write_dbt_project,
113
+ )
114
+ from morphsql.models import MigrationObject, ObjectType
115
+
116
+ wants_dbt = is_dbt_target(target.value) or generate_dbt
117
+ sql_target = Dialect.SNOWFLAKE if is_dbt_target(target.value) else _to_dialect(target)
118
+ pipeline = MigrationPipeline(source=_to_dialect(source), target=sql_target)
119
+
120
+ path_obj = Path(path)
121
+ if path_obj.is_file():
122
+ sql = path_obj.read_text()
123
+ translated, confidence, auto, review = translate_sql(
124
+ sql, _to_dialect(source), sql_target
125
+ )
126
+ console.print(Panel(f"Conversion confidence: [bold]{confidence:.0f}%[/]", expand=False))
127
+ if auto:
128
+ console.print("[green]Auto-converted:[/] " + ", ".join(auto[:5]))
129
+ if review:
130
+ console.print("[yellow]Requires review:[/] " + ", ".join(review[:5]))
131
+
132
+ if wants_dbt:
133
+ obj_type = ObjectType.SQL_SCRIPT
134
+ if "PROCEDURE" in sql.upper():
135
+ obj_type = ObjectType.STORED_PROCEDURE
136
+ elif "CREATE" in sql.upper() and "VIEW" in sql.upper():
137
+ obj_type = ObjectType.VIEW
138
+ obj = MigrationObject(
139
+ name=path_obj.stem,
140
+ object_type=obj_type,
141
+ source_sql=sql,
142
+ target_sql=translated,
143
+ )
144
+ files = decompose_to_dbt(obj, _to_dialect(source), project_name=path_obj.stem.lower())
145
+ console.print(f"\n[bold]dbt project[/] ({len(files)} files):\n")
146
+ console.print(format_dbt_project(files, max_files=8))
147
+ if output:
148
+ out_dir = Path(output)
149
+ write_dbt_project(files, out_dir)
150
+ console.print(f"[green]✓[/] Wrote dbt project → {out_dir}")
151
+ else:
152
+ console.print("\n[bold]Converted SQL:[/]\n")
153
+ console.print(translated)
154
+ if output:
155
+ out_path = Path(output)
156
+ if out_path.exists() and out_path.is_dir():
157
+ suffix = ".py" if sql_target in (Dialect.PANDAS, Dialect.PYSPARK) else ".sql"
158
+ out_path = out_path / f"{path_obj.stem}_converted{suffix}"
159
+ elif out_path.suffix == "":
160
+ out_path.mkdir(parents=True, exist_ok=True)
161
+ suffix = ".py" if sql_target in (Dialect.PANDAS, Dialect.PYSPARK) else ".sql"
162
+ out_path = out_path / f"{path_obj.stem}_converted{suffix}"
163
+ else:
164
+ out_path.parent.mkdir(parents=True, exist_ok=True)
165
+ out_path.write_text(translated, encoding="utf-8")
166
+ console.print(f"[green]✓[/] Wrote → {out_path}")
167
+ else:
168
+ report = pipeline.analyze(path)
169
+ report = pipeline.convert(report)
170
+ if generate_tests:
171
+ report = pipeline.validate(report)
172
+ out = Path(output or "migration-output")
173
+ out.mkdir(parents=True, exist_ok=True)
174
+ pipeline.generate_report(report, out / "migration_report.html")
175
+ if wants_dbt:
176
+ pipeline.generate_dbt(report, out / "dbt")
177
+ console.print(f"[green]✓[/] dbt projects written → {out / 'dbt'}")
178
+ if generate_lineage:
179
+ from morphsql.lineage import build_lineage_graph, format_lineage_tree
180
+
181
+ graph = build_lineage_graph(report.objects, _to_dialect(source))
182
+ lines = [
183
+ f"# MorphSQL lineage · {path}",
184
+ f"# nodes={graph.number_of_nodes()} edges={graph.number_of_edges()}",
185
+ "",
186
+ ]
187
+ roots = [n for n in graph.nodes() if graph.in_degree(n) == 0] or list(graph.nodes())[:5]
188
+ for root in roots[:20]:
189
+ try:
190
+ lines.append(format_lineage_tree(graph, root))
191
+ lines.append("")
192
+ except Exception:
193
+ lines.append(f"- {root}")
194
+ lineage_path = out / "lineage.txt"
195
+ lineage_path.write_text("\n".join(lines), encoding="utf-8")
196
+ console.print(f"[green]✓[/] Lineage written → {lineage_path}")
197
+ console.print(f"[green]✓[/] Converted {len(report.objects)} objects → {out}")
198
+
199
+
200
+ @app.command()
201
+ def validate(
202
+ path: str = typer.Argument(..., help="Path to SQL repository"),
203
+ source: SourceDialect = typer.Option(SourceDialect.snowflake, "--source", "-s"),
204
+ target: TargetDialect = typer.Option(TargetDialect.pandas, "--target", "-t"),
205
+ tolerance: float = typer.Option(0.01, "--tolerance"),
206
+ output: Optional[str] = typer.Option(None, "--output", "-o"),
207
+ ):
208
+ """Generate validation and reconciliation tests."""
209
+ pipeline = MigrationPipeline(source=_to_dialect(source), target=_to_dialect(target))
210
+ report = pipeline.analyze(path)
211
+ report = pipeline.convert(report)
212
+ report = pipeline.validate(report)
213
+
214
+ table = Table(title="Validation Results")
215
+ table.add_column("Object")
216
+ table.add_column("Check")
217
+ table.add_column("Status")
218
+ table.add_column("Details")
219
+
220
+ for result in report.validation_results:
221
+ status = "[green]PASS[/]" if result.passed else "[red]FAIL[/]"
222
+ detail = result.root_cause or result.recommendation or ""
223
+ table.add_row(result.object_name, result.check_name, status, detail[:60])
224
+
225
+ console.print(table)
226
+
227
+ if output:
228
+ out = Path(output)
229
+ out.mkdir(parents=True, exist_ok=True)
230
+ with open(out / "validation_results.json", "w") as f:
231
+ json.dump([r.model_dump() for r in report.validation_results], f, indent=2)
232
+ console.print(f"[green]✓[/] Validation results → {out}")
233
+
234
+
235
+ @app.command(name="migrate")
236
+ def migrate_cmd(
237
+ path: str = typer.Argument(..., help="Path to SQL repository"),
238
+ source: SourceDialect = typer.Option(SourceDialect.snowflake, "--source", "-s"),
239
+ target: TargetDialect = typer.Option(TargetDialect.pandas, "--target", "-t"),
240
+ output: str = typer.Option("migration-output", "--output", "-o"),
241
+ ):
242
+ """Run the complete migration pipeline: analyze → convert → validate → report."""
243
+ console.print(Panel(
244
+ f"[bold cyan]{__product_name__}[/] — Full Migration Pipeline\n"
245
+ f"{source.value} → {target.value}",
246
+ expand=False,
247
+ ))
248
+
249
+ pipeline = MigrationPipeline(source=_to_dialect(source), target=_to_dialect(target))
250
+ report = pipeline.run_full_pipeline(path, output)
251
+ _print_dashboard(report)
252
+ console.print(f"\n[green]✓[/] Complete migration package → [bold]{output}[/]")
253
+
254
+
255
+ @app.command()
256
+ def version():
257
+ """Show version information."""
258
+ console.print(f"{__product_name__} v{__version__}")
259
+
260
+
261
+ def _print_dashboard(report) -> None:
262
+ d = report.dashboard
263
+ console.print(Panel(
264
+ f"Objects: [bold]{d.total_objects}[/] | "
265
+ f"Auto-migrate: [green]{d.auto_migratable}[/] | "
266
+ f"Review: [yellow]{d.requires_review}[/] | "
267
+ f"Redesign: [red]{d.requires_redesign}[/] | "
268
+ f"Risk: [bold]{d.migration_risk_score:.0f}/100[/]",
269
+ title="Migration Dashboard",
270
+ expand=False,
271
+ ))
272
+
273
+
274
+ if __name__ == "__main__":
275
+ app()
morphsql/dbt_generator/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """dbt project generation package."""
2
+
3
+ from morphsql.dbt_generator.decomposer import decompose_to_dbt, write_dbt_project
4
+
5
+ __all__ = ["decompose_to_dbt", "write_dbt_project"]
morphsql/dbt_generator/decomposer.py ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Stored procedure and SQL → dbt project decomposition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import yaml
10
+
11
+ from morphsql.models import Dialect, MigrationObject, ObjectType
12
+ from morphsql.parser.sql_parser import count_sql_complexity, extract_tables
13
+
14
+
15
+ def is_dbt_target(target: str | Dialect) -> bool:
16
+ value = target.value if isinstance(target, Dialect) else str(target)
17
+ return value.lower() in {"dbt-snowflake", "dbt_snowflake"}
18
+
19
+
20
+ def decompose_to_dbt(
21
+ obj: MigrationObject,
22
+ source: Dialect,
23
+ project_name: str = "migration_project",
24
+ ) -> dict[str, str]:
25
+ """
26
+ Decompose a SQL object into a dbt project structure.
27
+
28
+ Uses converted Snowflake SQL (`obj.target_sql`) when available, otherwise source SQL.
29
+ Returns dict of {relative_path: file_content}.
30
+ """
31
+ files: dict[str, str] = {}
32
+ base_name = _sanitize_name(obj.name)
33
+ sql = (obj.target_sql or obj.source_sql or "").strip()
34
+ complexity = count_sql_complexity(obj.source_sql or sql, source)
35
+ source_name = source.value.replace("-", "_")
36
+
37
+ files["dbt_project.yml"] = yaml.dump(
38
+ {
39
+ "name": project_name,
40
+ "version": "1.0.0",
41
+ "profile": project_name,
42
+ "model-paths": ["models"],
43
+ "macro-paths": ["macros"],
44
+ "seed-paths": ["seeds"],
45
+ "test-paths": ["tests"],
46
+ "vars": {"load_date": "CURRENT_DATE"},
47
+ "models": {
48
+ project_name: {
49
+ "staging": {"+materialized": "view"},
50
+ "intermediate": {"+materialized": "view"},
51
+ "marts": {"+materialized": "table"},
52
+ }
53
+ },
54
+ },
55
+ default_flow_style=False,
56
+ )
57
+
58
+ files["profiles.yml.example"] = (
59
+ f"{project_name}:\n"
60
+ f" target: dev\n"
61
+ f" outputs:\n"
62
+ f" dev:\n"
63
+ f" type: snowflake\n"
64
+ f" account: <account>\n"
65
+ f" user: <user>\n"
66
+ f" password: <password>\n"
67
+ f" role: <role>\n"
68
+ f" database: <database>\n"
69
+ f" warehouse: <warehouse>\n"
70
+ f" schema: analytics\n"
71
+ f" threads: 4\n"
72
+ )
73
+
74
+ # Prefer physical source tables from original SQL
75
+ source_tables = extract_tables(obj.source_sql or sql, source)
76
+ # Drop obvious temp / intermediate names
77
+ physical = {
78
+ t
79
+ for t in source_tables
80
+ if not t.upper().startswith("TMP_")
81
+ and "TEMP" not in t.upper()
82
+ and not t.upper().startswith("TEMP_")
83
+ }
84
+ if physical:
85
+ files["models/staging/_sources.yml"] = yaml.dump(
86
+ {
87
+ "version": 2,
88
+ "sources": [
89
+ {
90
+ "name": source_name,
91
+ "description": f"Legacy {source.value} tables landed in Snowflake",
92
+ "database": "{{ var('source_database', 'RAW') }}",
93
+ "schema": "{{ var('source_schema', 'PUBLIC') }}",
94
+ "tables": [
95
+ {
96
+ "name": t.split(".")[-1].lower(),
97
+ "identifier": t.split(".")[-1],
98
+ }
99
+ for t in sorted(physical)
100
+ ],
101
+ }
102
+ ],
103
+ },
104
+ default_flow_style=False,
105
+ )
106
+
107
+ if _looks_like_procedure(sql) or obj.object_type == ObjectType.STORED_PROCEDURE:
108
+ models = _decompose_procedure(sql, base_name, source_name, obj.name)
109
+ elif _has_ctes(sql) and complexity.get("ctes", 0) >= 2:
110
+ models = _decompose_ctes(sql, base_name, source_name, obj.name)
111
+ else:
112
+ models = _simple_decomposition(sql, base_name, source_name, obj.name)
113
+
114
+ for path, content in models.items():
115
+ files[f"models/{path}"] = content
116
+
117
+ files[f"models/marts/_schema_{base_name}.yml"] = _generate_schema_yml(models, base_name, obj.name)
118
+ files["analyses/migration_validation.sql"] = _generate_validation_analysis(
119
+ obj, base_name, source
120
+ )
121
+
122
+ if re.search(r"ZEROIFNULL|NVL|ISNULL|COALESCE\([^,]+,\s*0\)", obj.source_sql or sql, re.I):
123
+ files["macros/null_safe.sql"] = _null_safe_macro()
124
+
125
+ files["README.md"] = _project_readme(obj, source, base_name, models)
126
+
127
+ obj.dbt_models = list(models.keys())
128
+ return files
129
+
130
+
131
+ def format_dbt_project(files: dict[str, str], max_files: int | None = None) -> str:
132
+ """Render a multi-file dbt project as a single readable text block for the UI."""
133
+ preferred = sorted(
134
+ files.keys(),
135
+ key=lambda p: (
136
+ 0 if p.startswith("models/") else 1 if p.endswith(".yml") else 2,
137
+ p,
138
+ ),
139
+ )
140
+ if max_files is not None:
141
+ preferred = preferred[:max_files]
142
+
143
+ parts = [
144
+ "-- MorphSQL dbt project (Snowflake)",
145
+ f"-- {len(files)} files generated · copy into a dbt project directory",
146
+ "",
147
+ ]
148
+ for rel in preferred:
149
+ content = files[rel].rstrip()
150
+ parts.append(f"-- ===== {rel} =====")
151
+ parts.append(content)
152
+ parts.append("")
153
+ if max_files is not None and len(files) > max_files:
154
+ remaining = sorted(set(files) - set(preferred))
155
+ parts.append(f"-- … and {len(remaining)} more files: {', '.join(remaining)}")
156
+ return "\n".join(parts).strip() + "\n"
157
+
158
+
159
+ def write_dbt_project(files: dict[str, str], output_dir: str | Path) -> Path:
160
+ """Write dbt project files to disk."""
161
+ output_dir = Path(output_dir)
162
+ for rel_path, content in files.items():
163
+ file_path = output_dir / rel_path
164
+ file_path.parent.mkdir(parents=True, exist_ok=True)
165
+ file_path.write_text(content, encoding="utf-8")
166
+ return output_dir
167
+
168
+
169
+ def _looks_like_procedure(sql: str) -> bool:
170
+ return bool(re.search(r"\bCREATE\s+(?:OR\s+REPLACE\s+)?PROCEDURE\b", sql, re.I))
171
+
172
+
173
+ def _has_ctes(sql: str) -> bool:
174
+ return bool(re.search(r"\bWITH\b", sql, re.I))
175
+
176
+
177
+ def _decompose_procedure(
178
+ sql: str,
179
+ base_name: str,
180
+ source_name: str,
181
+ original_name: str,
182
+ ) -> dict[str, str]:
183
+ """
184
+ Turn procedural ETL into layered dbt models.
185
+
186
+ CREATE TEMP TABLE x AS SELECT … → staging / intermediate models
187
+ INSERT INTO target SELECT … → mart model
188
+ """
189
+ models: dict[str, str] = {}
190
+ temp_model_names: dict[str, str] = {}
191
+
192
+ # CREATE [OR REPLACE] [LOCAL] TEMP[ORARY] TABLE name [ON COMMIT …] AS SELECT …;
193
+ temp_pat = re.compile(
194
+ r"CREATE\s+(?:OR\s+REPLACE\s+)?"
195
+ r"(?:LOCAL\s+)?TEMP(?:ORARY)?\s+TABLE\s+([\w.]+)"
196
+ r"(?:\s+ON\s+COMMIT\s+\w+\s+ROWS)?"
197
+ r"\s+AS\s+(SELECT\b.*?)(?:;|(?=\n\s*(?:CREATE|DELETE|INSERT|COMMIT|RETURN)))",
198
+ re.IGNORECASE | re.DOTALL,
199
+ )
200
+ temps = list(temp_pat.finditer(sql))
201
+ for i, match in enumerate(temps):
202
+ raw_name = match.group(1).split(".")[-1]
203
+ safe = _sanitize_name(raw_name)
204
+ layer = "staging" if i == 0 else "intermediate"
205
+ prefix = "stg" if layer == "staging" else "int"
206
+ model_key = f"{layer}/{prefix}_{base_name}_{safe}.sql"
207
+ model_ref = f"{prefix}_{base_name}_{safe}"
208
+ temp_model_names[raw_name.upper()] = model_ref
209
+ temp_model_names[safe.upper()] = model_ref
210
+
211
+ select_sql = match.group(2).strip().rstrip(";")
212
+ select_sql = _rewrite_sql_for_dbt(select_sql, source_name, temp_model_names)
213
+ select_sql = _replace_params_with_vars(select_sql)
214
+
215
+ models[model_key] = (
216
+ f"{{{{ config(materialized='view') }}}}\n\n"
217
+ f"-- From procedure {original_name} · temp table {raw_name}\n\n"
218
+ f"{select_sql}\n"
219
+ )
220
+
221
+ # INSERT INTO schema.table SELECT …
222
+ insert_pat = re.compile(
223
+ r"INSERT\s+INTO\s+([\w.]+)\s+(SELECT\b.*?)(?:;|(?=\n\s*(?:CREATE|DELETE|INSERT|COMMIT|RETURN))|\Z)",
224
+ re.IGNORECASE | re.DOTALL,
225
+ )
226
+ inserts = list(insert_pat.finditer(sql))
227
+ if inserts:
228
+ last = inserts[-1]
229
+ target_table = last.group(1).split(".")[-1]
230
+ mart_name = _sanitize_name(target_table) or base_name
231
+ select_sql = last.group(2).strip().rstrip(";")
232
+ select_sql = _rewrite_sql_for_dbt(select_sql, source_name, temp_model_names)
233
+ select_sql = _replace_params_with_vars(select_sql)
234
+ models[f"marts/{mart_name}.sql"] = (
235
+ f"{{{{ config(materialized='incremental', unique_key='customer_id', "
236
+ f"incremental_strategy='delete+insert') }}}}\n\n"
237
+ f"-- Mart loaded by legacy procedure {original_name}\n"
238
+ f"-- Target table: {last.group(1)}\n\n"
239
+ f"{select_sql}\n"
240
+ )
241
+ elif not models:
242
+ # Fallback: wrap whatever SELECT body we can find
243
+ models.update(_simple_decomposition(sql, base_name, source_name, original_name))
244
+ else:
245
+ # Staging/int models exist but no INSERT — emit a mart that selects from last int model
246
+ last_ref = list(temp_model_names.values())[-1]
247
+ models[f"marts/{base_name}.sql"] = (
248
+ f"{{{{ config(materialized='table') }}}}\n\n"
249
+ f"-- Final model for procedure {original_name}\n\n"
250
+ f"SELECT *\nFROM {{{{ ref('{last_ref}') }}}}\n"
251
+ )
252
+
253
+ return models
254
+
255
+
256
+ def _decompose_ctes(
257
+ sql: str,
258
+ base_name: str,
259
+ source_name: str,
260
+ original_name: str,
261
+ ) -> dict[str, str]:
262
+ """Split WITH cte AS (...) [, ...] SELECT into layered dbt models."""
263
+ models: dict[str, str] = {}
264
+ cte_map: dict[str, str] = {}
265
+
266
+ body = re.sub(
267
+ r"^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:VIEW|TABLE)\s+\S+\s+AS\s*",
268
+ "",
269
+ sql,
270
+ flags=re.I,
271
+ ).strip().rstrip(";")
272
+
273
+ if not re.match(r"WITH\b", body, re.I):
274
+ return _simple_decomposition(sql, base_name, source_name, original_name)
275
+
276
+ cte_iter = list(re.finditer(r"(\w+)\s+AS\s*\(", body, re.IGNORECASE))
277
+ if not cte_iter:
278
+ return _simple_decomposition(sql, base_name, source_name, original_name)
279
+
280
+ last_cte_end = 0
281
+ for idx, start in enumerate(cte_iter):
282
+ name = start.group(1)
283
+ prefix = body[: start.start()].rstrip().upper()
284
+ if idx == 0 and not prefix.endswith("WITH"):
285
+ continue
286
+ if idx > 0 and not (prefix.endswith(",") or prefix.endswith(")")):
287
+ # allow CTEs separated by commas
288
+ pass
289
+
290
+ open_paren = start.end() - 1
291
+ depth = 0
292
+ end = open_paren
293
+ for i in range(open_paren, len(body)):
294
+ if body[i] == "(":
295
+ depth += 1
296
+ elif body[i] == ")":
297
+ depth -= 1
298
+ if depth == 0:
299
+ end = i
300
+ break
301
+ cte_sql = body[open_paren + 1:end].strip()
302
+ last_cte_end = end + 1
303
+
304
+ safe = _sanitize_name(name)
305
+ layer = "staging" if idx == 0 else "intermediate"
306
+ model_prefix = "stg" if layer == "staging" else "int"
307
+ model_ref = f"{model_prefix}_{base_name}_{safe}"
308
+ cte_map[name.upper()] = model_ref
309
+
310
+ rewritten = _rewrite_sql_for_dbt(cte_sql, source_name, cte_map)
311
+ rewritten = _replace_params_with_vars(rewritten)
312
+ models[f"{layer}/{model_ref}.sql"] = (
313
+ f"{{{{ config(materialized='view') }}}}\n\n"
314
+ f"-- CTE `{name}` from {original_name}\n\n"
315
+ f"{rewritten}\n"
316
+ )
317
+
318
+ if not models:
319
+ return _simple_decomposition(sql, base_name, source_name, original_name)
320
+
321
+ rest = body[last_cte_end:].strip().lstrip(",").strip()
322
+ final_match = re.search(r"\bSELECT\b[\s\S]*$", rest, re.I)
323
+ if final_match:
324
+ final_sql = final_match.group(0).strip().rstrip(";")
325
+ else:
326
+ last_ref = list(cte_map.values())[-1]
327
+ final_sql = f"SELECT *\nFROM {{{{ ref('{last_ref}') }}}}"
328
+
329
+ final_sql = _rewrite_sql_for_dbt(final_sql, source_name, cte_map)
330
+ final_sql = _replace_cte_refs(final_sql, cte_map)
331
+ final_sql = _replace_params_with_vars(final_sql)
332
+ models[f"marts/{base_name}.sql"] = (
333
+ f"{{{{ config(materialized='table') }}}}\n\n"
334
+ f"-- Final SELECT from {original_name}\n\n"
335
+ f"{final_sql}\n"
336
+ )
337
+ return models
338
+
339
+
340
+ def _simple_decomposition(
341
+ sql: str,
342
+ base_name: str,
343
+ source_name: str,
344
+ original_name: str,
345
+ ) -> dict[str, str]:
346
+ cleaned = _extract_select_body(sql)
347
+ cleaned = _rewrite_sql_for_dbt(cleaned, source_name, {})
348
+ cleaned = _replace_params_with_vars(cleaned)
349
+ if not re.match(r"^\s*(WITH|SELECT)\b", cleaned, re.I):
350
+ cleaned = (
351
+ f"-- Procedural logic from {original_name} could not be fully auto-extracted.\n"
352
+ f"-- Review and rewrite as declarative dbt SQL.\n"
353
+ f"-- Original converted SQL (truncated):\n"
354
+ f"/*\n{sql[:1500]}\n*/\n"
355
+ f"SELECT 1 AS migration_placeholder\n"
356
+ )
357
+ return {
358
+ f"marts/{base_name}.sql": (
359
+ f"{{{{ config(materialized='table') }}}}\n\n"
360
+ f"-- Converted from: {original_name}\n\n"
361
+ f"{cleaned}\n"
362
+ )
363
+ }
364
+
365
+
366
+ def _extract_select_body(sql: str) -> str:
367
+ sql = sql.strip().rstrip(";")
368
+ sql = re.sub(
369
+ r"^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:VIEW|TABLE)\s+\S+\s+AS\s*",
370
+ "",
371
+ sql,
372
+ flags=re.I,
373
+ )
374
+ match = re.search(r"\b(WITH|SELECT)\b[\s\S]*$", sql, re.I)
375
+ return match.group(0).strip().rstrip(";") if match else sql
376
+
377
+
378
+ def _rewrite_sql_for_dbt(
379
+ sql: str,
380
+ source_name: str,
381
+ known_models: dict[str, str],
382
+ ) -> str:
383
+ """Replace physical tables with source()/ref() and map temp tables to refs."""
384
+ pattern = re.compile(r"\b(FROM|JOIN)\s+((?:[\w]+\.)?[\w]+)\b", re.IGNORECASE)
385
+
386
+ def from_join_repl(match: re.Match) -> str:
387
+ kw = match.group(1)
388
+ name = match.group(2)
389
+ if name.upper() in {"SELECT", "LATERAL", "UNNEST", "TABLE"}:
390
+ return match.group(0)
391
+ if "{{" in name:
392
+ return match.group(0)
393
+ key = name.upper()
394
+ short = name.split(".")[-1].upper()
395
+ if key in known_models or short in known_models:
396
+ ref = known_models.get(key) or known_models[short]
397
+ return f"{kw} {{{{ ref('{ref}') }}}}"
398
+ if short.startswith("TMP_") or short.startswith("TEMP_"):
399
+ return f"{kw} {{{{ ref('{_sanitize_name(short)}') }}}}"
400
+ table = name.split(".")[-1].lower()
401
+ return f"{kw} {{{{ source('{source_name}', '{table}') }}}}"
402
+
403
+ return pattern.sub(from_join_repl, sql)
404
+
405
+
406
+ def _replace_cte_refs(sql: str, cte_map: dict[str, str]) -> str:
407
+ """Replace remaining CTE name references in FROM/JOIN with ref()."""
408
+
409
+ def repl(match: re.Match) -> str:
410
+ kw, name = match.group(1), match.group(2)
411
+ key = name.upper()
412
+ if key in cte_map:
413
+ return f"{kw} {{{{ ref('{cte_map[key]}') }}}}"
414
+ return match.group(0)
415
+
416
+ return re.sub(r"\b(FROM|JOIN)\s+([\w]+)\b", repl, sql, flags=re.I)
417
+
418
+
419
+ def _replace_params_with_vars(sql: str) -> str:
420
+ """Map Snowflake :PARAM / load_date style params to dbt vars."""
421
+
422
+ def bind_repl(match: re.Match) -> str:
423
+ name = match.group(1)
424
+ if name.lower() in {"load_date", "as_of_date", "run_date"}:
425
+ return "{{ var('load_date') }}"
426
+ return "{{ var('" + name.lower() + "') }}"
427
+
428
+ sql = re.sub(r":([A-Za-z_][\w]*)", bind_repl, sql)
429
+
430
+ # Bare procedure params — skip text already inside Jinja
431
+ parts = re.split(r"(\{\{.*?\}\})", sql, flags=re.DOTALL)
432
+ out: list[str] = []
433
+ for part in parts:
434
+ if part.startswith("{{"):
435
+ out.append(part)
436
+ continue
437
+ for param in ("load_date", "as_of_date", "run_date"):
438
+ part = re.sub(
439
+ rf"(?<![\w.]){param}(?![\w])",
440
+ "{{ var('load_date') }}",
441
+ part,
442
+ flags=re.IGNORECASE,
443
+ )
444
+ out.append(part)
445
+ return "".join(out)
446
+
447
+
448
+ def _generate_schema_yml(models: dict[str, str], base_name: str, original_name: str) -> str:
449
+ mart_names = [
450
+ Path(p).stem for p in models if p.startswith("marts/") and p.endswith(".sql")
451
+ ]
452
+ schema_models = []
453
+ for name in mart_names or [base_name]:
454
+ schema_models.append(
455
+ {
456
+ "name": name,
457
+ "description": f"Migrated dbt model from legacy object {original_name}",
458
+ "columns": [
459
+ {
460
+ "name": "customer_id",
461
+ "description": "Primary business key (adjust as needed)",
462
+ "tests": ["not_null"],
463
+ }
464
+ ],
465
+ }
466
+ )
467
+ return yaml.dump({"version": 2, "models": schema_models}, default_flow_style=False)
468
+
469
+
470
+ def _generate_validation_analysis(obj: MigrationObject, base_name: str, source: Dialect) -> str:
471
+ source_name = source.value.replace("-", "_")
472
+ mart = base_name
473
+ if obj.dbt_models:
474
+ marts = [Path(m).stem for m in obj.dbt_models if m.startswith("marts/")]
475
+ if marts:
476
+ mart = marts[0]
477
+ return f"""-- Migration validation analysis for {obj.name}
478
+ -- Run after `dbt build` to compare source vs target
479
+
480
+ SELECT 'source' AS platform, COUNT(*) AS row_count
481
+ FROM {{{{ source('{source_name}', '{base_name}') }}}}
482
+ UNION ALL
483
+ SELECT 'target' AS platform, COUNT(*) AS row_count
484
+ FROM {{{{ ref('{mart}') }}}};
485
+ """
486
+
487
+
488
+ def _null_safe_macro() -> str:
489
+ return """{% macro null_safe(column, default=0) %}
490
+ COALESCE({{ column }}, {{ default }})
491
+ {% endmacro %}
492
+ """
493
+
494
+
495
+ def _project_readme(
496
+ obj: MigrationObject,
497
+ source: Dialect,
498
+ base_name: str,
499
+ models: dict[str, str],
500
+ ) -> str:
501
+ model_list = "\n".join(f"- `{m}`" for m in sorted(models))
502
+ return f"""# dbt project — {base_name}
503
+
504
+ Generated by MorphSQL from `{obj.name}` ({source.value} → dbt-snowflake).
505
+
506
+ ## Models
507
+ {model_list}
508
+
509
+ ## Run
510
+ ```bash
511
+ dbt deps
512
+ dbt build --vars '{{"load_date": "2024-01-15"}}'
513
+ ```
514
+
515
+ Review staging sources in `models/staging/_sources.yml` and adjust database/schema vars.
516
+ """
517
+
518
+
519
+ def _sanitize_name(name: str) -> str:
520
+ return re.sub(r"[^a-zA-Z0-9_]", "_", name.lower()).strip("_")
morphsql/eval/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Eval package exports."""
2
+
3
+ from morphsql.eval.leaderboard import format_leaderboard_md, load_leaderboard, submit_score
4
+ from morphsql.eval.metrics import run_eval
5
+ from morphsql.eval.pairs import ensure_pairs_file, load_pairs
6
+
7
+ __all__ = [
8
+ "ensure_pairs_file",
9
+ "load_pairs",
10
+ "run_eval",
11
+ "submit_score",
12
+ "load_leaderboard",
13
+ "format_leaderboard_md",
14
+ ]
morphsql/eval/leaderboard.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple local leaderboard for conversion quality scores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ LEADERBOARD_PATH = Path(__file__).resolve().parent.parent.parent / "datasets" / "leaderboard.json"
10
+
11
+
12
+ def load_leaderboard() -> list[dict]:
13
+ if not LEADERBOARD_PATH.exists():
14
+ return []
15
+ try:
16
+ return json.loads(LEADERBOARD_PATH.read_text(encoding="utf-8"))
17
+ except Exception:
18
+ return []
19
+
20
+
21
+ def submit_score(
22
+ name: str,
23
+ exact_match: float,
24
+ token_f1: float,
25
+ fuzzy: float,
26
+ pass_rate: float,
27
+ n_pairs: int,
28
+ notes: str = "",
29
+ ) -> list[dict]:
30
+ board = load_leaderboard()
31
+ entry = {
32
+ "name": (name or "anonymous").strip()[:64],
33
+ "exact_match": round(float(exact_match), 4),
34
+ "token_f1": round(float(token_f1), 4),
35
+ "fuzzy": round(float(fuzzy), 4),
36
+ "pass_rate": round(float(pass_rate), 4),
37
+ "n_pairs": int(n_pairs),
38
+ "notes": (notes or "").strip()[:200],
39
+ "ts": datetime.now(timezone.utc).isoformat(),
40
+ }
41
+ board.append(entry)
42
+ board.sort(key=lambda e: (e.get("pass_rate", 0), e.get("token_f1", 0)), reverse=True)
43
+ board = board[:50]
44
+ LEADERBOARD_PATH.parent.mkdir(parents=True, exist_ok=True)
45
+ LEADERBOARD_PATH.write_text(json.dumps(board, indent=2), encoding="utf-8")
46
+ return board
47
+
48
+
49
+ def format_leaderboard_md(board: list[dict] | None = None) -> str:
50
+ board = board if board is not None else load_leaderboard()
51
+ if not board:
52
+ return (
53
+ "### Leaderboard\n\n"
54
+ "No scores yet. Run **Eval Suite**, then submit a score."
55
+ )
56
+ lines = [
57
+ "### Leaderboard",
58
+ "",
59
+ "| Rank | Name | Pass rate | Token F1 | Exact | Pairs |",
60
+ "|------|------|-----------|----------|-------|-------|",
61
+ ]
62
+ for i, e in enumerate(board[:20], 1):
63
+ lines.append(
64
+ f"| {i} | {e.get('name', '?')} | {100 * e.get('pass_rate', 0):.1f}% "
65
+ f"| {100 * e.get('token_f1', 0):.1f}% | {100 * e.get('exact_match', 0):.1f}% "
66
+ f"| {e.get('n_pairs', 0)} |"
67
+ )
68
+ return "\n".join(lines)
morphsql/eval/metrics.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Translation quality metrics for SQL migration eval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import asdict, dataclass
7
+
8
+ from morphsql.eval.pairs import load_pairs
9
+ from morphsql.models import Dialect
10
+ from morphsql.translator.engine import translate_sql
11
+
12
+
13
+ def _normalize_sql(sql: str) -> str:
14
+ sql = sql.lower().strip()
15
+ sql = re.sub(r"--.*?$", "", sql, flags=re.M)
16
+ sql = re.sub(r"/\*.*?\*/", "", sql, flags=re.S)
17
+ sql = re.sub(r"\s+", " ", sql)
18
+ sql = sql.replace(";", "")
19
+ return sql.strip()
20
+
21
+
22
+ def _tokens(sql: str) -> list[str]:
23
+ return re.findall(r"[a-z_][a-z0-9_]*|\d+|[=<>!()*,.]", _normalize_sql(sql))
24
+
25
+
26
+ def exact_match(pred: str, gold: str) -> float:
27
+ return 1.0 if _normalize_sql(pred) == _normalize_sql(gold) else 0.0
28
+
29
+
30
+ def token_f1(pred: str, gold: str) -> float:
31
+ p, g = _tokens(pred), _tokens(gold)
32
+ if not p and not g:
33
+ return 1.0
34
+ if not p or not g:
35
+ return 0.0
36
+ from collections import Counter
37
+
38
+ pc, gc = Counter(p), Counter(g)
39
+ overlap = sum((pc & gc).values())
40
+ precision = overlap / max(sum(pc.values()), 1)
41
+ recall = overlap / max(sum(gc.values()), 1)
42
+ if precision + recall == 0:
43
+ return 0.0
44
+ return 2 * precision * recall / (precision + recall)
45
+
46
+
47
+ def fuzzy_ratio(pred: str, gold: str) -> float:
48
+ """Character-level Dice coefficient on normalized SQL."""
49
+ a, b = _normalize_sql(pred), _normalize_sql(gold)
50
+ if not a and not b:
51
+ return 1.0
52
+ if not a or not b:
53
+ return 0.0
54
+ # bigram Dice
55
+ def bigrams(s: str) -> set[str]:
56
+ if len(s) < 2:
57
+ return {s}
58
+ return {s[i : i + 2] for i in range(len(s) - 1)}
59
+
60
+ ba, bb = bigrams(a), bigrams(b)
61
+ return 2 * len(ba & bb) / max(len(ba) + len(bb), 1)
62
+
63
+
64
+ @dataclass
65
+ class EvalResult:
66
+ pair_id: str
67
+ category: str
68
+ source_dialect: str
69
+ target_dialect: str
70
+ exact_match: float
71
+ token_f1: float
72
+ fuzzy: float
73
+ passed: bool
74
+ predicted: str
75
+ gold: str
76
+
77
+
78
+ def llm_judge_optional(pred: str, gold: str, source_sql: str) -> float | None:
79
+ """Optional LLM-as-judge via HF Inference when HF_TOKEN is set. Returns 0..1 or None."""
80
+ import os
81
+
82
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
83
+ if not token:
84
+ return None
85
+ try:
86
+ from huggingface_hub import InferenceClient
87
+
88
+ client = InferenceClient(token=token)
89
+ prompt = (
90
+ "Score SQL migration quality from 0 to 1. Reply with ONLY a number.\n"
91
+ f"Source:\n{source_sql[:800]}\n\nGold:\n{gold[:800]}\n\nPred:\n{pred[:800]}\n"
92
+ )
93
+ text = client.text_generation(
94
+ prompt,
95
+ model=os.getenv("MORPHSQL_MODEL", "Qwen/Qwen2.5-3B-Instruct"),
96
+ max_new_tokens=8,
97
+ )
98
+ import re
99
+
100
+ m = re.search(r"0?\.\d+|1(?:\.0+)?|0", str(text))
101
+ if not m:
102
+ return None
103
+ return max(0.0, min(1.0, float(m.group(0))))
104
+ except Exception:
105
+ return None
106
+
107
+
108
+ def run_eval(
109
+ limit: int | None = None,
110
+ categories: list[str] | None = None,
111
+ ) -> tuple[list[EvalResult], dict]:
112
+ """Run translator against gold pairs and return rows + aggregate metrics."""
113
+ pairs = load_pairs(limit=limit)
114
+ if categories:
115
+ cats = {c.lower() for c in categories}
116
+ pairs = [p for p in pairs if p.get("category", "").lower() in cats]
117
+
118
+ results: list[EvalResult] = []
119
+ for pair in pairs:
120
+ try:
121
+ source = Dialect(pair["source_dialect"])
122
+ target = Dialect(pair["target_dialect"])
123
+ except Exception:
124
+ continue
125
+ predicted, _, _, _ = translate_sql(pair["source_sql"], source, target)
126
+ em = exact_match(predicted, pair["target_sql"])
127
+ f1 = token_f1(predicted, pair["target_sql"])
128
+ fuzzy = fuzzy_ratio(predicted, pair["target_sql"])
129
+ # Pass if exact or strong fuzzy/token agreement
130
+ passed = em >= 1.0 or (f1 >= 0.85 and fuzzy >= 0.75)
131
+ results.append(
132
+ EvalResult(
133
+ pair_id=pair["id"],
134
+ category=pair.get("category", "unknown"),
135
+ source_dialect=pair["source_dialect"],
136
+ target_dialect=pair["target_dialect"],
137
+ exact_match=em,
138
+ token_f1=f1,
139
+ fuzzy=fuzzy,
140
+ passed=passed,
141
+ predicted=predicted,
142
+ gold=pair["target_sql"],
143
+ )
144
+ )
145
+
146
+ n = max(len(results), 1)
147
+ summary = {
148
+ "n_pairs": len(results),
149
+ "exact_match": sum(r.exact_match for r in results) / n,
150
+ "token_f1": sum(r.token_f1 for r in results) / n,
151
+ "fuzzy": sum(r.fuzzy for r in results) / n,
152
+ "pass_rate": sum(1 for r in results if r.passed) / n,
153
+ "by_category": {},
154
+ }
155
+ cats: dict[str, list[EvalResult]] = {}
156
+ for r in results:
157
+ cats.setdefault(r.category, []).append(r)
158
+ for cat, rows in cats.items():
159
+ m = max(len(rows), 1)
160
+ summary["by_category"][cat] = {
161
+ "n": len(rows),
162
+ "exact_match": sum(x.exact_match for x in rows) / m,
163
+ "token_f1": sum(x.token_f1 for x in rows) / m,
164
+ "pass_rate": sum(1 for x in rows if x.passed) / m,
165
+ }
166
+ return results, summary
167
+
168
+
169
+ def results_to_dicts(results: list[EvalResult], limit: int = 40) -> list[dict]:
170
+ return [asdict(r) for r in results[:limit]]
morphsql/eval/pairs.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate and load Vertica ↔ Snowflake SQL migration pairs for HF dataset + evals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ DATA_DIR = Path(__file__).resolve().parent.parent.parent / "datasets"
9
+ PAIRS_PATH = DATA_DIR / "vertica_snowflake_pairs.jsonl"
10
+
11
+
12
+ TEMPLATES: list[dict] = [
13
+ {
14
+ "id": "zeroifnull_basic",
15
+ "category": "function",
16
+ "source_dialect": "vertica",
17
+ "target_dialect": "snowflake",
18
+ "source_sql": "SELECT ZEROIFNULL(amount) AS amount FROM staging.orders",
19
+ "target_sql": "SELECT COALESCE(amount, 0) AS amount FROM staging.orders",
20
+ "notes": "ZEROIFNULL → COALESCE(expr, 0)",
21
+ },
22
+ {
23
+ "id": "nvl_basic",
24
+ "category": "function",
25
+ "source_dialect": "vertica",
26
+ "target_dialect": "snowflake",
27
+ "source_sql": "SELECT NVL(discount, 0) AS discount FROM staging.orders",
28
+ "target_sql": "SELECT COALESCE(discount, 0) AS discount FROM staging.orders",
29
+ "notes": "NVL → COALESCE",
30
+ },
31
+ {
32
+ "id": "date_sub_days",
33
+ "category": "date",
34
+ "source_dialect": "vertica",
35
+ "target_dialect": "snowflake",
36
+ "source_sql": "SELECT * FROM t WHERE order_date >= CURRENT_DATE - 30",
37
+ "target_sql": "SELECT * FROM t WHERE order_date >= DATEADD(day, -30, CURRENT_DATE)",
38
+ "notes": "Date arithmetic → DATEADD",
39
+ },
40
+ {
41
+ "id": "local_temp",
42
+ "category": "ddl",
43
+ "source_dialect": "vertica",
44
+ "target_dialect": "snowflake",
45
+ "source_sql": "CREATE LOCAL TEMP TABLE tmp_x ON COMMIT PRESERVE ROWS AS SELECT 1 AS id",
46
+ "target_sql": "CREATE OR REPLACE TEMPORARY TABLE tmp_x AS SELECT 1 AS id",
47
+ "notes": "LOCAL TEMP → TEMPORARY TABLE",
48
+ },
49
+ {
50
+ "id": "string_agg",
51
+ "category": "aggregate",
52
+ "source_dialect": "vertica",
53
+ "target_dialect": "snowflake",
54
+ "source_sql": "SELECT STRING_AGG(name, ',') FROM users",
55
+ "target_sql": "SELECT LISTAGG(name, ',') FROM users",
56
+ "notes": "STRING_AGG → LISTAGG",
57
+ },
58
+ {
59
+ "id": "approx_distinct",
60
+ "category": "aggregate",
61
+ "source_dialect": "vertica",
62
+ "target_dialect": "snowflake",
63
+ "source_sql": "SELECT APPROXIMATE_COUNT_DISTINCT(user_id) FROM events",
64
+ "target_sql": "SELECT APPROX_COUNT_DISTINCT(user_id) FROM events",
65
+ "notes": "APPROXIMATE_COUNT_DISTINCT → APPROX_COUNT_DISTINCT",
66
+ },
67
+ {
68
+ "id": "datediff_quoted",
69
+ "category": "date",
70
+ "source_dialect": "vertica",
71
+ "target_dialect": "snowflake",
72
+ "source_sql": "SELECT DATEDIFF('day', start_dt, end_dt) FROM t",
73
+ "target_sql": "SELECT DATEDIFF(day, start_dt, end_dt) FROM t",
74
+ "notes": "Quoted DATEDIFF unit → unquoted",
75
+ },
76
+ {
77
+ "id": "sysdate",
78
+ "category": "function",
79
+ "source_dialect": "oracle",
80
+ "target_dialect": "snowflake",
81
+ "source_sql": "SELECT SYSDATE FROM dual",
82
+ "target_sql": "SELECT CURRENT_TIMESTAMP()",
83
+ "notes": "SYSDATE + drop DUAL",
84
+ },
85
+ {
86
+ "id": "getdate_redshift",
87
+ "category": "function",
88
+ "source_dialect": "redshift",
89
+ "target_dialect": "snowflake",
90
+ "source_sql": "SELECT GETDATE(), NVL(x, 0) FROM t",
91
+ "target_sql": "SELECT CURRENT_TIMESTAMP(), COALESCE(x, 0) FROM t",
92
+ "notes": "GETDATE/NVL mappings",
93
+ },
94
+ {
95
+ "id": "bigquery_ifnull",
96
+ "category": "function",
97
+ "source_dialect": "bigquery",
98
+ "target_dialect": "snowflake",
99
+ "source_sql": "SELECT IFNULL(a, 0), STRING_AGG(b, ',') FROM t GROUP BY a",
100
+ "target_sql": "SELECT COALESCE(a, 0), LISTAGG(b, ',') FROM t GROUP BY a",
101
+ "notes": "IFNULL/STRING_AGG → COALESCE/LISTAGG",
102
+ },
103
+ {
104
+ "id": "feature_rfm",
105
+ "category": "ml_feature",
106
+ "source_dialect": "vertica",
107
+ "target_dialect": "snowflake",
108
+ "source_sql": (
109
+ "SELECT customer_id, "
110
+ "ZEROIFNULL(SUM(order_amount)) AS monetary, "
111
+ "COUNT(DISTINCT order_id) AS frequency, "
112
+ "DATEDIFF('day', MAX(order_date), CURRENT_DATE) AS recency "
113
+ "FROM staging.orders GROUP BY customer_id"
114
+ ),
115
+ "target_sql": (
116
+ "SELECT customer_id, "
117
+ "COALESCE(SUM(order_amount), 0) AS monetary, "
118
+ "COUNT(DISTINCT order_id) AS frequency, "
119
+ "DATEDIFF(day, MAX(order_date), CURRENT_DATE) AS recency "
120
+ "FROM staging.orders GROUP BY customer_id"
121
+ ),
122
+ "notes": "ML RFM feature SQL migration",
123
+ },
124
+ {
125
+ "id": "feature_label",
126
+ "category": "ml_feature",
127
+ "source_dialect": "vertica",
128
+ "target_dialect": "snowflake",
129
+ "source_sql": (
130
+ "SELECT user_id, "
131
+ "CASE WHEN ZEROIFNULL(churn_score) > 0.7 THEN 1 ELSE 0 END AS churn_label "
132
+ "FROM ml.user_scores"
133
+ ),
134
+ "target_sql": (
135
+ "SELECT user_id, "
136
+ "CASE WHEN COALESCE(churn_score, 0) > 0.7 THEN 1 ELSE 0 END AS churn_label "
137
+ "FROM ml.user_scores"
138
+ ),
139
+ "notes": "Training label feature SQL",
140
+ },
141
+ ]
142
+
143
+
144
+ def _expand_pairs() -> list[dict]:
145
+ """Expand templates into a larger eval/dataset corpus."""
146
+ pairs = [dict(p) for p in TEMPLATES]
147
+ # Parametric expansions for volume (HF dataset appeal)
148
+ for days in (7, 14, 30, 60, 90, 180, 365):
149
+ pairs.append(
150
+ {
151
+ "id": f"date_sub_{days}",
152
+ "category": "date",
153
+ "source_dialect": "vertica",
154
+ "target_dialect": "snowflake",
155
+ "source_sql": f"SELECT * FROM events WHERE event_date >= CURRENT_DATE - {days}",
156
+ "target_sql": (
157
+ f"SELECT * FROM events WHERE event_date >= DATEADD(day, -{days}, CURRENT_DATE)"
158
+ ),
159
+ "notes": f"CURRENT_DATE - {days} → DATEADD",
160
+ }
161
+ )
162
+ for col in ("amount", "revenue", "fee", "tax", "balance", "score", "qty"):
163
+ pairs.append(
164
+ {
165
+ "id": f"zeroifnull_{col}",
166
+ "category": "function",
167
+ "source_dialect": "vertica",
168
+ "target_dialect": "snowflake",
169
+ "source_sql": f"SELECT ZEROIFNULL({col}) AS {col} FROM staging.facts",
170
+ "target_sql": f"SELECT COALESCE({col}, 0) AS {col} FROM staging.facts",
171
+ "notes": "ZEROIFNULL expansion",
172
+ }
173
+ )
174
+ for i in range(1, 81):
175
+ pairs.append(
176
+ {
177
+ "id": f"agg_mix_{i}",
178
+ "category": "aggregate",
179
+ "source_dialect": "vertica",
180
+ "target_dialect": "snowflake",
181
+ "source_sql": (
182
+ f"SELECT dim_{i % 5}, ZEROIFNULL(SUM(metric_{i})), "
183
+ f"APPROXIMATE_COUNT_DISTINCT(user_id) "
184
+ f"FROM fact_{i % 3} GROUP BY 1"
185
+ ),
186
+ "target_sql": (
187
+ f"SELECT dim_{i % 5}, COALESCE(SUM(metric_{i}), 0), "
188
+ f"APPROX_COUNT_DISTINCT(user_id) "
189
+ f"FROM fact_{i % 3} GROUP BY 1"
190
+ ),
191
+ "notes": "Synthetic aggregate pair",
192
+ }
193
+ )
194
+ for i in range(1, 61):
195
+ pairs.append(
196
+ {
197
+ "id": f"oracle_nvl_{i}",
198
+ "category": "function",
199
+ "source_dialect": "oracle",
200
+ "target_dialect": "snowflake",
201
+ "source_sql": f"SELECT NVL(col_{i}, 0) FROM dual",
202
+ "target_sql": f"SELECT COALESCE(col_{i}, 0)",
203
+ "notes": "Oracle NVL expansion",
204
+ }
205
+ )
206
+ for i in range(1, 51):
207
+ pairs.append(
208
+ {
209
+ "id": f"ml_feature_{i}",
210
+ "category": "ml_feature",
211
+ "source_dialect": "vertica",
212
+ "target_dialect": "snowflake",
213
+ "source_sql": (
214
+ f"SELECT entity_id, ZEROIFNULL(AVG(feature_{i})) AS f_{i} "
215
+ f"FROM ml.training_window WHERE ds >= CURRENT_DATE - {7 + i} "
216
+ f"GROUP BY 1"
217
+ ),
218
+ "target_sql": (
219
+ f"SELECT entity_id, COALESCE(AVG(feature_{i}), 0) AS f_{i} "
220
+ f"FROM ml.training_window WHERE ds >= DATEADD(day, -{7 + i}, CURRENT_DATE) "
221
+ f"GROUP BY 1"
222
+ ),
223
+ "notes": "ML feature window SQL",
224
+ }
225
+ )
226
+ # AI / LLM-eval oriented paraphrases
227
+ for i in range(1, 41):
228
+ pairs.append(
229
+ {
230
+ "id": f"ai_codegen_{i}",
231
+ "category": "function",
232
+ "source_dialect": "vertica",
233
+ "target_dialect": "snowflake",
234
+ "source_sql": (
235
+ f"SELECT ZEROIFNULL(score_{i}), NVL(flag_{i}, 0) "
236
+ f"FROM ml.inference_logs WHERE run_id = {i}"
237
+ ),
238
+ "target_sql": (
239
+ f"SELECT COALESCE(score_{i}, 0), COALESCE(flag_{i}, 0) "
240
+ f"FROM ml.inference_logs WHERE run_id = {i}"
241
+ ),
242
+ "notes": "AI inference-log SQL pair",
243
+ }
244
+ )
245
+ return pairs
246
+
247
+
248
+ def ensure_pairs_file() -> Path:
249
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
250
+ pairs = _expand_pairs()
251
+ with PAIRS_PATH.open("w", encoding="utf-8") as fh:
252
+ for row in pairs:
253
+ fh.write(json.dumps(row) + "\n")
254
+ return PAIRS_PATH
255
+
256
+
257
+ def load_pairs(limit: int | None = None) -> list[dict]:
258
+ if not PAIRS_PATH.exists():
259
+ ensure_pairs_file()
260
+ rows: list[dict] = []
261
+ with PAIRS_PATH.open(encoding="utf-8") as fh:
262
+ for line in fh:
263
+ line = line.strip()
264
+ if not line:
265
+ continue
266
+ rows.append(json.loads(line))
267
+ if limit is not None and len(rows) >= limit:
268
+ break
269
+ return rows
270
+
271
+
272
+ if __name__ == "__main__":
273
+ path = ensure_pairs_file()
274
+ print(f"Wrote {len(load_pairs())} pairs → {path}")
morphsql/intelligence/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Migration intelligence outputs beyond SQL translation."""
2
+
3
+ from morphsql.intelligence.lineage_viz import lineage_to_plotly
4
+ from morphsql.intelligence.rationalization import generate_rationalization
5
+ from morphsql.intelligence.runbook import generate_executive_summary, generate_runbook
6
+
7
+ __all__ = [
8
+ "generate_executive_summary",
9
+ "generate_runbook",
10
+ "generate_rationalization",
11
+ "lineage_to_plotly",
12
+ ]
morphsql/intelligence/lineage_viz.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Interactive lineage graph visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import networkx as nx
6
+ import plotly.graph_objects as go
7
+
8
+ from morphsql.models import Dialect, MigrationObject
9
+
10
+ C_PANEL = "#1a2332"
11
+ C_TEXT = "#e2e8f0"
12
+ C_MUTED = "#94a3b8"
13
+ C_BORDER = "#2d3a4f"
14
+
15
+
16
+ def lineage_to_plotly(
17
+ graph: nx.DiGraph,
18
+ objects: list[MigrationObject] | None = None,
19
+ ) -> go.Figure:
20
+ """Render dependency graph as interactive Plotly network."""
21
+ if graph.number_of_nodes() == 0:
22
+ return _empty_graph("No lineage data — run a repository scan first.")
23
+
24
+ # Use spring layout for positioning
25
+ try:
26
+ pos = nx.spring_layout(graph, k=1.8, iterations=50, seed=42)
27
+ except nx.NetworkXError:
28
+ return _empty_graph("Could not layout lineage graph.")
29
+
30
+ obj_names = {o.name for o in (objects or [])}
31
+
32
+ edge_x, edge_y = [], []
33
+ for u, v in graph.edges():
34
+ x0, y0 = pos[u]
35
+ x1, y1 = pos[v]
36
+ edge_x += [x0, x1, None]
37
+ edge_y += [y0, y1, None]
38
+
39
+ edge_trace = go.Scatter(
40
+ x=edge_x,
41
+ y=edge_y,
42
+ mode="lines",
43
+ line=dict(width=1.2, color="#475569"),
44
+ hoverinfo="none",
45
+ showlegend=False,
46
+ )
47
+
48
+ node_x, node_y, node_text, node_color, node_size = [], [], [], [], []
49
+ for node in graph.nodes():
50
+ x, y = pos[node]
51
+ node_x.append(x)
52
+ node_y.append(y)
53
+ node_text.append(node)
54
+ obj_type = graph.nodes[node].get("object_type", "table")
55
+ if node in obj_names:
56
+ node_color.append("#3b82f6")
57
+ node_size.append(14)
58
+ elif obj_type in ("stored_procedure", "sql_script", "view"):
59
+ node_color.append("#8b5cf6")
60
+ node_size.append(12)
61
+ else:
62
+ node_color.append("#64748b")
63
+ node_size.append(10)
64
+
65
+ node_trace = go.Scatter(
66
+ x=node_x,
67
+ y=node_y,
68
+ mode="markers+text",
69
+ text=node_text,
70
+ textposition="top center",
71
+ textfont=dict(size=9, color=C_TEXT),
72
+ marker=dict(
73
+ size=node_size,
74
+ color=node_color,
75
+ line=dict(width=1, color=C_BORDER),
76
+ ),
77
+ hovertemplate="%{text}<extra></extra>",
78
+ showlegend=False,
79
+ )
80
+
81
+ fig = go.Figure(data=[edge_trace, node_trace])
82
+ fig.update_layout(
83
+ template="plotly_dark",
84
+ title=dict(text="Dependency lineage", font=dict(size=13, color=C_MUTED)),
85
+ showlegend=False,
86
+ hovermode="closest",
87
+ height=420,
88
+ paper_bgcolor=C_PANEL,
89
+ plot_bgcolor=C_PANEL,
90
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
91
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
92
+ margin=dict(t=40, b=20, l=20, r=20),
93
+ font=dict(family="Inter, system-ui, sans-serif", color=C_TEXT),
94
+ )
95
+ return fig
96
+
97
+
98
+ def _empty_graph(message: str) -> go.Figure:
99
+ fig = go.Figure()
100
+ fig.update_layout(
101
+ template="plotly_dark",
102
+ height=420,
103
+ paper_bgcolor=C_PANEL,
104
+ plot_bgcolor=C_PANEL,
105
+ xaxis=dict(visible=False),
106
+ yaxis=dict(visible=False),
107
+ annotations=[
108
+ dict(
109
+ text=message,
110
+ xref="paper",
111
+ yref="paper",
112
+ x=0.5,
113
+ y=0.5,
114
+ showarrow=False,
115
+ font=dict(size=13, color=C_MUTED),
116
+ )
117
+ ],
118
+ )
119
+ return fig
morphsql/intelligence/rag.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RAG over platform behavior knowledge base (embeddings optional)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from dataclasses import dataclass
8
+
9
+ from morphsql.knowledge.behavior import BEHAVIOR_DIFFERENCES, BehaviorDifference
10
+
11
+
12
+ @dataclass
13
+ class RetrievedDoc:
14
+ name: str
15
+ score: float
16
+ source_platform: str
17
+ target_platform: str
18
+ description: str
19
+ recommendation: str
20
+ severity: str
21
+
22
+
23
+ def _tokenize(text: str) -> set[str]:
24
+ return {t for t in re.findall(r"[a-z0-9_]+", text.lower()) if len(t) > 2}
25
+
26
+
27
+ def _keyword_score(query: str, doc: BehaviorDifference) -> float:
28
+ q = _tokenize(query)
29
+ if not q:
30
+ return 0.0
31
+ blob = " ".join(
32
+ [
33
+ doc.name,
34
+ doc.description,
35
+ doc.impact,
36
+ doc.recommendation,
37
+ doc.source_platform,
38
+ doc.target_platform,
39
+ doc.detection_pattern,
40
+ ]
41
+ )
42
+ d = _tokenize(blob)
43
+ if not d:
44
+ return 0.0
45
+ overlap = len(q & d)
46
+ return overlap / math.sqrt(len(q) * len(d))
47
+
48
+
49
+ class BehaviorRAG:
50
+ """Retrieve relevant behavior diffs for a natural-language or SQL query."""
51
+
52
+ def __init__(self):
53
+ self._embeddings = None
54
+ self._matrix = None
55
+ self._docs = list(BEHAVIOR_DIFFERENCES)
56
+ self._backend = "keyword"
57
+
58
+ def _try_load_embeddings(self) -> None:
59
+ if self._embeddings is not None:
60
+ return
61
+ try:
62
+ from sentence_transformers import SentenceTransformer
63
+ import numpy as np
64
+
65
+ model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
66
+ texts = [
67
+ f"{d.name}. {d.description}. {d.recommendation}. "
68
+ f"{d.source_platform} to {d.target_platform}."
69
+ for d in self._docs
70
+ ]
71
+ self._matrix = model.encode(texts, normalize_embeddings=True)
72
+ self._embeddings = model
73
+ self._backend = "sentence-transformers"
74
+ _ = np # silence unused if encode returns list
75
+ except Exception:
76
+ self._embeddings = False
77
+ self._backend = "keyword"
78
+
79
+ def retrieve(
80
+ self,
81
+ query: str,
82
+ top_k: int = 5,
83
+ source: str | None = None,
84
+ target: str | None = None,
85
+ ) -> list[RetrievedDoc]:
86
+ query = (query or "").strip()
87
+ if not query:
88
+ return []
89
+
90
+ self._try_load_embeddings()
91
+ docs = self._docs
92
+ if source:
93
+ docs = [d for d in docs if d.source_platform == source]
94
+ if target:
95
+ # accept snowflake for dbt-snowflake
96
+ tgt = "snowflake" if target.startswith("dbt") else target
97
+ docs = [d for d in docs if d.target_platform == tgt]
98
+ if not docs:
99
+ docs = self._docs
100
+
101
+ scored: list[tuple[float, BehaviorDifference]] = []
102
+ if self._backend == "sentence-transformers" and self._embeddings and self._matrix is not None:
103
+ import numpy as np
104
+
105
+ q = self._embeddings.encode([query], normalize_embeddings=True)[0]
106
+ # map filtered docs back to indices
107
+ idx_map = {id(d): i for i, d in enumerate(self._docs)}
108
+ for d in docs:
109
+ i = idx_map.get(id(d))
110
+ if i is None:
111
+ continue
112
+ score = float(np.dot(self._matrix[i], q))
113
+ scored.append((score, d))
114
+ else:
115
+ for d in docs:
116
+ scored.append((_keyword_score(query, d), d))
117
+
118
+ scored.sort(key=lambda x: x[0], reverse=True)
119
+ out: list[RetrievedDoc] = []
120
+ for score, d in scored[:top_k]:
121
+ if score <= 0 and self._backend == "keyword":
122
+ continue
123
+ out.append(
124
+ RetrievedDoc(
125
+ name=d.name,
126
+ score=round(score, 4),
127
+ source_platform=d.source_platform,
128
+ target_platform=d.target_platform,
129
+ description=d.description,
130
+ recommendation=d.recommendation,
131
+ severity=d.severity,
132
+ )
133
+ )
134
+ return out
135
+
136
+ def answer(self, query: str, source: str = "vertica", target: str = "snowflake") -> str:
137
+ hits = self.retrieve(query, top_k=5, source=source, target=target)
138
+ if not hits:
139
+ hits = self.retrieve(query, top_k=5)
140
+ lines = [
141
+ f"### Behavior RAG ({self._backend})",
142
+ "",
143
+ f"**Query:** {query}",
144
+ f"**Route filter:** {source} → {target}",
145
+ "",
146
+ ]
147
+ if not hits:
148
+ lines.append("No matching behavior differences found. Try keywords like NULL, timezone, ROWNUM, MERGE.")
149
+ return "\n".join(lines)
150
+ for i, h in enumerate(hits, 1):
151
+ lines.append(
152
+ f"**{i}. [{h.severity.upper()}] {h.name}** "
153
+ f"({h.source_platform}→{h.target_platform}) · score={h.score}"
154
+ )
155
+ lines.append(f"- {h.description}")
156
+ lines.append(f"- Recommendation: {h.recommendation}")
157
+ lines.append("")
158
+ return "\n".join(lines)
159
+
160
+
161
+ _rag_singleton: BehaviorRAG | None = None
162
+
163
+
164
+ def get_rag() -> BehaviorRAG:
165
+ global _rag_singleton
166
+ if _rag_singleton is None:
167
+ _rag_singleton = BehaviorRAG()
168
+ return _rag_singleton
morphsql/intelligence/rationalization.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Workload rationalization recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from morphsql.models import MigrationCategory, MigrationReport
6
+ from morphsql.risk.scorer import recommend_workload_action
7
+
8
+
9
+ def generate_rationalization(report: MigrationReport) -> str:
10
+ """Generate workload rationalization analysis."""
11
+ migrate: list[str] = []
12
+ review: list[str] = []
13
+ rewrite: list[str] = []
14
+ retire: list[str] = []
15
+
16
+ for obj in report.objects:
17
+ action = recommend_workload_action(obj)
18
+ entry = f"**{obj.name}** ({obj.object_type.value}, complexity {obj.complexity_score}/100)"
19
+ if action == "retire" or obj.migration_category == MigrationCategory.RETIRE:
20
+ retire.append(entry)
21
+ elif action in ("manual_redesign", "rewrite"):
22
+ rewrite.append(entry)
23
+ elif action == "review_and_migrate":
24
+ review.append(entry)
25
+ else:
26
+ migrate.append(entry)
27
+
28
+ lines = [
29
+ "### Workload rationalization",
30
+ "",
31
+ "Enterprise migrations often fail by moving everything. "
32
+ "This analysis categorizes each object by recommended action.",
33
+ "",
34
+ f"| Action | Count |",
35
+ f"|--------|-------|",
36
+ f"| Migrate as-is | {len(migrate)} |",
37
+ f"| Review then migrate | {len(review)} |",
38
+ f"| Rewrite / redesign | {len(rewrite)} |",
39
+ f"| Retire / consolidate | {len(retire) + len(report.retirement_candidates)} |",
40
+ "",
41
+ ]
42
+
43
+ if migrate:
44
+ lines += ["#### Migrate", ""] + [f"- {e}" for e in migrate[:10]] + [""]
45
+ if review:
46
+ lines += ["#### Review required", ""] + [f"- {e}" for e in review[:10]] + [""]
47
+ if rewrite:
48
+ lines += ["#### Rewrite", ""] + [f"- {e}" for e in rewrite[:10]] + [""]
49
+ if retire or report.retirement_candidates:
50
+ lines += ["#### Retire / consolidate", ""]
51
+ for e in retire[:8]:
52
+ lines.append(f"- {e}")
53
+ for r in report.retirement_candidates[:5]:
54
+ lines.append(f"- {r}")
55
+
56
+ scope_reduction = len(retire) + len(report.retirement_candidates)
57
+ if scope_reduction:
58
+ lines += [
59
+ "",
60
+ f"**Scope reduction opportunity:** {scope_reduction} objects can be "
61
+ f"retired or consolidated, reducing migration effort by approximately "
62
+ f"{scope_reduction / max(len(report.objects), 1) * 100:.0f}%.",
63
+ ]
64
+
65
+ return "\n".join(lines)
morphsql/intelligence/runbook.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Migration runbook and executive summary generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from morphsql.models import MigrationReport
6
+ from morphsql.risk.scorer import recommend_workload_action
7
+
8
+
9
+ def generate_runbook(report: MigrationReport) -> str:
10
+ """Generate a markdown migration runbook from a scan report."""
11
+ d = report.dashboard
12
+ src = report.source_dialect.value.upper()
13
+ tgt = report.target_dialect.value.upper()
14
+
15
+ lines = [
16
+ f"# Migration Runbook",
17
+ f"",
18
+ f"**Source:** {src} · **Target:** {tgt} · **Repository:** `{report.repository_path}`",
19
+ f"",
20
+ f"## Executive summary",
21
+ f"",
22
+ f"| Metric | Value |",
23
+ f"|--------|-------|",
24
+ f"| Objects discovered | {d.total_objects} |",
25
+ f"| Auto-migratable | {d.auto_migratable} |",
26
+ f"| Requires review | {d.requires_review} |",
27
+ f"| Manual redesign | {d.requires_redesign} |",
28
+ f"| Retire / consolidate | {d.recommended_retirement} |",
29
+ f"| Migration risk score | {d.migration_risk_score:.0f} / 100 |",
30
+ f"| Lineage coverage | {d.lineage_coverage_pct:.0f}% |",
31
+ f"| Validation pass rate | {d.validation_passed_pct:.0f}% |",
32
+ f"| Est. annual savings | ${d.estimated_annual_savings_usd[0]:,.0f} – ${d.estimated_annual_savings_usd[1]:,.0f} |",
33
+ f"",
34
+ f"## Recommended phases",
35
+ f"",
36
+ f"### Phase 1 — Discovery (complete)",
37
+ f"- Scanned {d.total_objects} objects",
38
+ f"- Built dependency lineage ({d.lineage_coverage_pct:.0f}% coverage)",
39
+ f"- Identified {d.recommended_retirement} retirement candidates",
40
+ f"",
41
+ f"### Phase 2 — Low-risk conversion",
42
+ f"- Migrate {d.auto_migratable} automatically migratable objects",
43
+ f"- Generate dbt staging/intermediate/mart structure",
44
+ f"- Run reconciliation tests (row count, null rate, checksums)",
45
+ f"",
46
+ f"### Phase 3 — Review queue",
47
+ f"- Engineering review for {d.requires_review} objects",
48
+ f"- Address behavior warnings ({len(report.behavior_warnings)} detected)",
49
+ f"",
50
+ f"### Phase 4 — Redesign",
51
+ f"- Manual redesign for {d.requires_redesign} high-complexity objects",
52
+ f"- Decompose stored procedures into modular pipelines",
53
+ f"",
54
+ f"### Phase 5 — Cutover",
55
+ f"- Parallel run source vs target",
56
+ f"- Validate business metrics within tolerance",
57
+ f"- Retire legacy platform after sign-off",
58
+ f"",
59
+ f"## Object action plan",
60
+ f"",
61
+ f"| Object | Type | Action | Complexity | Risk |",
62
+ f"|--------|------|--------|------------|------|",
63
+ ]
64
+
65
+ for obj in sorted(report.objects, key=lambda o: o.complexity_score, reverse=True):
66
+ action = recommend_workload_action(obj).replace("_", " ")
67
+ lines.append(
68
+ f"| {obj.name} | {obj.object_type.value} | {action} "
69
+ f"| {obj.complexity_score} | {obj.risk_level.value} |"
70
+ )
71
+
72
+ if report.behavior_warnings:
73
+ lines += ["", "## Platform behavior risks", ""]
74
+ for w in report.behavior_warnings:
75
+ lines.append(f"- {w}")
76
+
77
+ if report.retirement_candidates:
78
+ lines += ["", "## Retirement candidates", ""]
79
+ for r in report.retirement_candidates:
80
+ lines.append(f"- {r}")
81
+
82
+ lines += [
83
+ "",
84
+ "## Validation checklist",
85
+ "",
86
+ "- [ ] Row count reconciliation per object",
87
+ "- [ ] Null rate comparison on string columns",
88
+ "- [ ] Aggregate metric tolerance (financial columns)",
89
+ "- [ ] Incremental load replay test",
90
+ "- [ ] Procedure parameter binding verified",
91
+ "- [ ] dbt schema tests generated and passing",
92
+ "",
93
+ "---",
94
+ "*Generated by MorphSQL*",
95
+ ]
96
+
97
+ return "\n".join(lines)
98
+
99
+
100
+ def generate_executive_summary(report: MigrationReport) -> str:
101
+ """Short executive summary for dashboard display."""
102
+ d = report.dashboard
103
+ savings = d.estimated_annual_savings_usd
104
+ return (
105
+ f"Scanned **{d.total_objects} objects** from `{report.repository_path}`. "
106
+ f"**{d.auto_migratable}** are ready for automatic migration; "
107
+ f"**{d.requires_review}** need engineering review; "
108
+ f"**{d.requires_redesign}** require redesign. "
109
+ f"Average risk score **{d.migration_risk_score:.0f}/100**. "
110
+ f"Estimated annual savings **${savings[0]:,.0f}–${savings[1]:,.0f}** "
111
+ f"after moving from {report.source_dialect.value.upper()} to "
112
+ f"{report.target_dialect.value.upper()}."
113
+ )
morphsql/knowledge/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Knowledge base package."""
2
+
3
+ from morphsql.knowledge.behavior import (
4
+ BEHAVIOR_DIFFERENCES,
5
+ BehaviorDifference,
6
+ format_behavior_warning,
7
+ get_behavior_warnings,
8
+ )
9
+
10
+ __all__ = [
11
+ "BEHAVIOR_DIFFERENCES",
12
+ "BehaviorDifference",
13
+ "format_behavior_warning",
14
+ "get_behavior_warnings",
15
+ ]
morphsql/knowledge/behavior.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Platform behavior difference knowledge base."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass
9
+ class BehaviorDifference:
10
+ name: str
11
+ source_platform: str
12
+ target_platform: str
13
+ description: str
14
+ impact: str
15
+ detection_pattern: str
16
+ recommendation: str
17
+ severity: str = "medium"
18
+
19
+
20
+ BEHAVIOR_DIFFERENCES: list[BehaviorDifference] = [
21
+ BehaviorDifference(
22
+ name="empty_string_vs_null",
23
+ source_platform="oracle",
24
+ target_platform="snowflake",
25
+ description="Oracle treats empty strings as NULL; Snowflake preserves empty strings.",
26
+ impact="Row count and NULL rate differences in string columns.",
27
+ detection_pattern=r"''|EMPTY",
28
+ recommendation="Use NULLIF(TRIM(column), '') to normalize empty strings.",
29
+ severity="high",
30
+ ),
31
+ BehaviorDifference(
32
+ name="empty_string_vs_null_vertica",
33
+ source_platform="vertica",
34
+ target_platform="snowflake",
35
+ description="Vertica and Snowflake may handle empty strings differently in comparisons.",
36
+ impact="Filter predicates may include/exclude different rows.",
37
+ detection_pattern=r"=\s*''|<> ''|IS NULL",
38
+ recommendation="Explicitly handle empty strings with NULLIF and COALESCE.",
39
+ severity="medium",
40
+ ),
41
+ BehaviorDifference(
42
+ name="integer_division",
43
+ source_platform="vertica",
44
+ target_platform="snowflake",
45
+ description="Integer division behavior may differ between platforms.",
46
+ impact="Numeric calculations may produce truncated vs decimal results.",
47
+ detection_pattern=r"/\s*\d+|/\s*\w+",
48
+ recommendation="Use explicit CAST to DECIMAL or FLOAT before division.",
49
+ severity="medium",
50
+ ),
51
+ BehaviorDifference(
52
+ name="case_sensitivity",
53
+ source_platform="vertica",
54
+ target_platform="snowflake",
55
+ description="Vertica is case-sensitive for quoted identifiers; Snowflake uppercases unquoted.",
56
+ impact="Column and table name resolution failures or wrong joins.",
57
+ detection_pattern=r'"[^"]+"',
58
+ recommendation="Standardize identifier casing and use consistent quoting.",
59
+ severity="high",
60
+ ),
61
+ BehaviorDifference(
62
+ name="timezone_behavior",
63
+ source_platform="vertica",
64
+ target_platform="snowflake",
65
+ description="TIMESTAMP WITH TIME ZONE handling differs across platforms.",
66
+ impact="Date boundary comparisons may shift by timezone offset.",
67
+ detection_pattern=r"TIMESTAMP|TIMEZONE|AT TIME ZONE",
68
+ recommendation="Normalize all timestamps to UTC using CONVERT_TIMEZONE.",
69
+ severity="high",
70
+ ),
71
+ BehaviorDifference(
72
+ name="date_trunc_semantics",
73
+ source_platform="vertica",
74
+ target_platform="snowflake",
75
+ description="DATE_TRUNC unit boundaries may differ at month/quarter/year edges.",
76
+ impact="Aggregated period metrics may not reconcile.",
77
+ detection_pattern=r"DATE_TRUNC|TRUNC\s*\(",
78
+ recommendation="Validate period boundaries with sample date edge cases.",
79
+ severity="medium",
80
+ ),
81
+ BehaviorDifference(
82
+ name="floating_point_precision",
83
+ source_platform="vertica",
84
+ target_platform="snowflake",
85
+ description="FLOAT/DOUBLE precision and rounding differ between engines.",
86
+ impact="Checksum and aggregate metric mismatches within tolerance.",
87
+ detection_pattern=r"\bFLOAT\b|\bDOUBLE\b|\bREAL\b",
88
+ recommendation="Use DECIMAL for financial metrics; set numeric tolerance in validation.",
89
+ severity="medium",
90
+ ),
91
+ BehaviorDifference(
92
+ name="default_ordering",
93
+ source_platform="vertica",
94
+ target_platform="snowflake",
95
+ description="Results without ORDER BY have no guaranteed row order.",
96
+ impact="Row-by-row comparison tests may show false differences.",
97
+ detection_pattern=r"SELECT(?!.*ORDER BY)",
98
+ recommendation="Always add deterministic ORDER BY in reconciliation queries.",
99
+ severity="low",
100
+ ),
101
+ BehaviorDifference(
102
+ name="merge_semantics",
103
+ source_platform="vertica",
104
+ target_platform="snowflake",
105
+ description="MERGE duplicate-match behavior may differ when multiple source rows match.",
106
+ impact="Upsert operations may update different rows.",
107
+ detection_pattern=r"\bMERGE\s+INTO\b",
108
+ recommendation="Deduplicate source before MERGE; add unique key constraints.",
109
+ severity="high",
110
+ ),
111
+ BehaviorDifference(
112
+ name="boolean_handling",
113
+ source_platform="vertica",
114
+ target_platform="snowflake",
115
+ description="Boolean type support and casting differ; Vertica often uses INT flags.",
116
+ impact="Filter conditions on boolean-like columns may behave differently.",
117
+ detection_pattern=r"\bBOOLEAN\b|=\s*[01]\s",
118
+ recommendation="Explicitly cast boolean flags: column::BOOLEAN or column = 1.",
119
+ severity="medium",
120
+ ),
121
+ BehaviorDifference(
122
+ name="zeroifnull_isnull",
123
+ source_platform="vertica",
124
+ target_platform="snowflake",
125
+ description="Vertica ZEROIFNULL/ISNULL functions map to COALESCE/NVL differently.",
126
+ impact="NULL handling in arithmetic expressions produces different results.",
127
+ detection_pattern=r"ZEROIFNULL|ISNULL|NVL",
128
+ recommendation="Replace with COALESCE(column, 0) and validate NULL cases.",
129
+ severity="medium",
130
+ ),
131
+ BehaviorDifference(
132
+ name="sequence_handling",
133
+ source_platform="oracle",
134
+ target_platform="snowflake",
135
+ description="Oracle sequences vs Snowflake sequences/auto-increment differ.",
136
+ impact="Generated key values will not match between source and target.",
137
+ detection_pattern=r"\bSEQUENCE\b|\.NEXTVAL|AUTO_INCREMENT",
138
+ recommendation="Use Snowflake sequences; do not reconcile auto-generated keys.",
139
+ severity="low",
140
+ ),
141
+ BehaviorDifference(
142
+ name="rownum_vs_qualify",
143
+ source_platform="oracle",
144
+ target_platform="snowflake",
145
+ description="Oracle ROWNUM filtering does not map 1:1 to Snowflake QUALIFY/ROW_NUMBER.",
146
+ impact="Top-N queries may return different rows.",
147
+ detection_pattern=r"\bROWNUM\b",
148
+ recommendation="Rewrite as QUALIFY ROW_NUMBER() OVER (...) <= N.",
149
+ severity="high",
150
+ ),
151
+ BehaviorDifference(
152
+ name="redshift_encoding",
153
+ source_platform="redshift",
154
+ target_platform="snowflake",
155
+ description="Redshift column ENCODE / DISTKEY / SORTKEY have no Snowflake equivalent.",
156
+ impact="Physical design must be redesigned (clustering keys, search optimization).",
157
+ detection_pattern=r"\b(DISTKEY|SORTKEY|DISTSTYLE|ENCODE)\b",
158
+ recommendation="Drop distribution clauses; model clustering separately in Snowflake.",
159
+ severity="medium",
160
+ ),
161
+ BehaviorDifference(
162
+ name="redshift_listagg",
163
+ source_platform="redshift",
164
+ target_platform="bigquery",
165
+ description="LISTAGG / WITHIN GROUP semantics differ from BigQuery STRING_AGG.",
166
+ impact="Concatenated string order or NULL handling may differ.",
167
+ detection_pattern=r"\bLISTAGG\b",
168
+ recommendation="Validate STRING_AGG ORDER BY and NULL treatment.",
169
+ severity="medium",
170
+ ),
171
+ BehaviorDifference(
172
+ name="bigquery_struct",
173
+ source_platform="bigquery",
174
+ target_platform="snowflake",
175
+ description="BigQuery STRUCT/ARRAY types map to Snowflake VARIANT/ARRAY with different access paths.",
176
+ impact="Nested field projections may fail or need FLATTEN.",
177
+ detection_pattern=r"\b(STRUCT|ARRAY)\s*<|\bUNNEST\b",
178
+ recommendation="Model nested data as VARIANT and rewrite UNNEST to FLATTEN.",
179
+ severity="high",
180
+ ),
181
+ BehaviorDifference(
182
+ name="snowflake_variant",
183
+ source_platform="snowflake",
184
+ target_platform="bigquery",
185
+ description="Snowflake VARIANT/FLATTEN patterns need BigQuery JSON/UNNEST rewrites.",
186
+ impact="Semi-structured queries will not transpile cleanly.",
187
+ detection_pattern=r"\b(VARIANT|FLATTEN|OBJECT_INSERT)\b",
188
+ recommendation="Rewrite to JSON functions and UNNEST in BigQuery.",
189
+ severity="high",
190
+ ),
191
+ BehaviorDifference(
192
+ name="vertica_to_bigquery_types",
193
+ source_platform="vertica",
194
+ target_platform="bigquery",
195
+ description="Vertica numeric/date types map to BigQuery INT64/FLOAT64/DATE with different casting rules.",
196
+ impact="Implicit casts that worked in Vertica may fail in BigQuery.",
197
+ detection_pattern=r"\b(CAST|::|NUMERIC|FLOAT)\b",
198
+ recommendation="Use explicit CAST to BigQuery types (INT64, FLOAT64, NUMERIC).",
199
+ severity="medium",
200
+ ),
201
+ ]
202
+
203
+
204
+ def get_behavior_warnings(
205
+ sql: str,
206
+ source: str,
207
+ target: str,
208
+ ) -> list[BehaviorDifference]:
209
+ """Return applicable behavior warnings for a SQL object."""
210
+ import re
211
+
212
+ warnings: list[BehaviorDifference] = []
213
+ for diff in BEHAVIOR_DIFFERENCES:
214
+ if diff.source_platform != source:
215
+ continue
216
+ if diff.target_platform != target:
217
+ continue
218
+ if re.search(diff.detection_pattern, sql, re.IGNORECASE):
219
+ warnings.append(diff)
220
+ return warnings
221
+
222
+
223
+ def format_behavior_warning(diff: BehaviorDifference) -> str:
224
+ return (
225
+ f"[{diff.severity.upper()}] {diff.name}: {diff.description} "
226
+ f"Recommendation: {diff.recommendation}"
227
+ )
morphsql/lineage/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Lineage analysis package."""
2
+
3
+ from morphsql.lineage.builder import (
4
+ build_column_lineage_for_object,
5
+ build_lineage_graph,
6
+ build_table_lineage,
7
+ detect_circular_dependencies,
8
+ find_orphaned_tables,
9
+ format_lineage_tree,
10
+ get_lineage_chain,
11
+ )
12
+
13
+ __all__ = [
14
+ "build_column_lineage_for_object",
15
+ "build_lineage_graph",
16
+ "build_table_lineage",
17
+ "detect_circular_dependencies",
18
+ "find_orphaned_tables",
19
+ "format_lineage_tree",
20
+ "get_lineage_chain",
21
+ ]
morphsql/lineage/builder.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Table and column-level lineage construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections import defaultdict
7
+
8
+ import networkx as nx
9
+
10
+ from morphsql.models import ColumnLineage, Dialect, MigrationObject, TableLineage
11
+ from morphsql.parser.sql_parser import extract_columns, extract_tables, parse_sql_multi
12
+
13
+
14
+ def build_lineage_graph(
15
+ objects: list[MigrationObject],
16
+ dialect: Dialect,
17
+ ) -> nx.DiGraph:
18
+ """Build a directed dependency graph from migration objects."""
19
+ graph = nx.DiGraph()
20
+
21
+ for obj in objects:
22
+ graph.add_node(
23
+ obj.name,
24
+ object_type=obj.object_type.value,
25
+ source_path=obj.source_path,
26
+ )
27
+
28
+ tables_read = extract_tables(obj.source_sql, dialect)
29
+ tables_written = _extract_write_targets(obj.source_sql)
30
+
31
+ for table in tables_read:
32
+ graph.add_node(table, object_type="table")
33
+ graph.add_edge(table, obj.name, relation="reads")
34
+
35
+ for table in tables_written:
36
+ graph.add_node(table, object_type="table")
37
+ graph.add_edge(obj.name, table, relation="writes")
38
+
39
+ return graph
40
+
41
+
42
+ def build_table_lineage(
43
+ objects: list[MigrationObject],
44
+ dialect: Dialect,
45
+ ) -> list[TableLineage]:
46
+ """Build table-level lineage with upstream/downstream relationships."""
47
+ graph = build_lineage_graph(objects, dialect)
48
+ lineages: list[TableLineage] = []
49
+
50
+ table_nodes = [
51
+ n for n, d in graph.nodes(data=True)
52
+ if d.get("object_type") in ("table", "view", "temp_table")
53
+ ]
54
+
55
+ for table in sorted(set(table_nodes)):
56
+ upstream = sorted(
57
+ pred for pred in graph.predecessors(table)
58
+ if graph.nodes[pred].get("object_type") != "table"
59
+ )
60
+ downstream = sorted(
61
+ succ for succ in graph.successors(table)
62
+ if graph.nodes[succ].get("object_type") != "table"
63
+ )
64
+
65
+ columns = _build_column_lineage(table, objects, dialect)
66
+
67
+ lineages.append(
68
+ TableLineage(
69
+ table=table,
70
+ upstream=upstream,
71
+ downstream=downstream,
72
+ columns=columns,
73
+ )
74
+ )
75
+
76
+ return lineages
77
+
78
+
79
+ def build_column_lineage_for_object(
80
+ obj: MigrationObject,
81
+ dialect: Dialect,
82
+ ) -> list[ColumnLineage]:
83
+ """Extract column-level lineage for a single object."""
84
+ columns_map = extract_columns(obj.source_sql, dialect)
85
+ lineages: list[ColumnLineage] = []
86
+
87
+ for table, cols in columns_map.items():
88
+ for col in set(cols):
89
+ lineages.append(
90
+ ColumnLineage(
91
+ column=col,
92
+ table=table,
93
+ source_columns=[f"{table}.{col}"],
94
+ transformations=_detect_transformations(obj.source_sql, col),
95
+ downstream=[],
96
+ )
97
+ )
98
+
99
+ return lineages
100
+
101
+
102
+ def get_lineage_chain(
103
+ graph: nx.DiGraph,
104
+ node: str,
105
+ direction: str = "downstream",
106
+ max_depth: int = 10,
107
+ ) -> list[str]:
108
+ """Get lineage chain for a node."""
109
+ chain: list[str] = []
110
+ visited: set[str] = set()
111
+
112
+ def _walk(current: str, depth: int) -> None:
113
+ if depth > max_depth or current in visited:
114
+ return
115
+ visited.add(current)
116
+ neighbors = (
117
+ list(graph.successors(current))
118
+ if direction == "downstream"
119
+ else list(graph.predecessors(current))
120
+ )
121
+ for neighbor in neighbors:
122
+ chain.append(neighbor)
123
+ _walk(neighbor, depth + 1)
124
+
125
+ _walk(node, 0)
126
+ return chain
127
+
128
+
129
+ def format_lineage_tree(
130
+ graph: nx.DiGraph,
131
+ root: str,
132
+ ) -> str:
133
+ """Format lineage as an ASCII tree."""
134
+ lines: list[str] = [root]
135
+ visited: set[str] = {root}
136
+
137
+ def _render(node: str, prefix: str, depth: int) -> None:
138
+ if depth > 8:
139
+ return
140
+ successors = list(graph.successors(node))
141
+ for i, succ in enumerate(successors):
142
+ if succ in visited:
143
+ continue
144
+ visited.add(succ)
145
+ connector = "└── " if i == len(successors) - 1 else "├── "
146
+ lines.append(f"{prefix}{connector}{succ}")
147
+ extension = " " if i == len(successors) - 1 else "│ "
148
+ _render(succ, prefix + extension, depth + 1)
149
+
150
+ _render(root, "", 0)
151
+ return "\n".join(lines)
152
+
153
+
154
+ def detect_circular_dependencies(graph: nx.DiGraph) -> list[list[str]]:
155
+ """Detect circular dependencies in the lineage graph."""
156
+ try:
157
+ cycles = list(nx.simple_cycles(graph))
158
+ return cycles
159
+ except nx.NetworkXError:
160
+ return []
161
+
162
+
163
+ def find_orphaned_tables(
164
+ graph: nx.DiGraph,
165
+ objects: list[MigrationObject],
166
+ ) -> list[str]:
167
+ """Find tables with no downstream consumers."""
168
+ all_tables = set()
169
+ consumed = set()
170
+
171
+ for obj in objects:
172
+ for table in extract_tables(obj.source_sql, Dialect.VERTICA):
173
+ all_tables.add(table)
174
+ for table in _extract_write_targets(obj.source_sql):
175
+ all_tables.add(table)
176
+ if graph.out_degree(table) == 0:
177
+ consumed.add(table)
178
+
179
+ orphans = [t for t in all_tables if graph.out_degree(t) == 0 and t not in consumed]
180
+ return sorted(orphans)
181
+
182
+
183
+ def _extract_write_targets(sql: str) -> set[str]:
184
+ """Extract tables written to (INSERT/CREATE/MERGE targets)."""
185
+ targets: set[str] = set()
186
+ patterns = [
187
+ r"INSERT\s+INTO\s+([\w.]+)",
188
+ r"CREATE\s+(?:OR\s+REPLACE\s+)?(?:LOCAL\s+TEMP\s+|TEMP\s+|TEMPORARY\s+)?TABLE\s+([\w.]+)",
189
+ r"MERGE\s+INTO\s+([\w.]+)",
190
+ r"UPDATE\s+([\w.]+)\s+SET",
191
+ r"CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+([\w.]+)",
192
+ ]
193
+ for pattern in patterns:
194
+ for match in re.finditer(pattern, sql, re.IGNORECASE):
195
+ targets.add(match.group(1).upper())
196
+ return targets
197
+
198
+
199
+ def _build_column_lineage(
200
+ table: str,
201
+ objects: list[MigrationObject],
202
+ dialect: Dialect,
203
+ ) -> list[ColumnLineage]:
204
+ columns: dict[str, ColumnLineage] = {}
205
+
206
+ for obj in objects:
207
+ if table.upper() not in extract_tables(obj.source_sql, dialect):
208
+ if table.upper() not in _extract_write_targets(obj.source_sql):
209
+ continue
210
+
211
+ for col_lineage in build_column_lineage_for_object(obj, dialect):
212
+ key = f"{col_lineage.table}.{col_lineage.column}"
213
+ if key not in columns:
214
+ columns[key] = col_lineage
215
+ else:
216
+ columns[key].downstream.extend(col_lineage.source_columns)
217
+
218
+ return list(columns.values())
219
+
220
+
221
+ def _detect_transformations(sql: str, column: str) -> list[str]:
222
+ """Detect transformation patterns involving a column."""
223
+ transformations: list[str] = []
224
+ col_escaped = re.escape(column.upper())
225
+
226
+ patterns = {
227
+ "aggregation": rf"\b(SUM|COUNT|AVG|MIN|MAX)\s*\([^)]*{col_escaped}",
228
+ "case_logic": rf"\bCASE\b[\s\S]*?{col_escaped}",
229
+ "window_function": rf"{col_escaped}[^)]*\bOVER\s*\(",
230
+ "cast": rf"CAST\s*\(\s*{col_escaped}",
231
+ "coalesce": rf"\b(COALESCE|NVL|IFNULL|ZEROIFNULL)\s*\([^)]*{col_escaped}",
232
+ "date_trunc": rf"\b(DATE_TRUNC|TRUNC)\s*\([^)]*{col_escaped}",
233
+ }
234
+
235
+ sql_upper = sql.upper()
236
+ for name, pattern in patterns.items():
237
+ try:
238
+ if re.search(pattern, sql_upper, re.IGNORECASE):
239
+ transformations.append(name)
240
+ except re.error:
241
+ continue
242
+
243
+ return transformations
morphsql/models.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core data models for MorphSQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class Dialect(str, Enum):
12
+ VERTICA = "vertica"
13
+ ORACLE = "oracle"
14
+ SNOWFLAKE = "snowflake"
15
+ BIGQUERY = "bigquery"
16
+ REDSHIFT = "redshift"
17
+ DBT_SNOWFLAKE = "dbt-snowflake"
18
+ PANDAS = "pandas"
19
+ PYSPARK = "pyspark"
20
+
21
+
22
+ class ObjectType(str, Enum):
23
+ TABLE = "table"
24
+ VIEW = "view"
25
+ STORED_PROCEDURE = "stored_procedure"
26
+ FUNCTION = "function"
27
+ SQL_SCRIPT = "sql_script"
28
+ DBT_MODEL = "dbt_model"
29
+ AIRFLOW_DAG = "airflow_dag"
30
+ TEMP_TABLE = "temp_table"
31
+ UNKNOWN = "unknown"
32
+
33
+
34
+ class RiskLevel(str, Enum):
35
+ LOW = "low"
36
+ MEDIUM = "medium"
37
+ HIGH = "high"
38
+ CRITICAL = "critical"
39
+
40
+
41
+ class MigrationCategory(str, Enum):
42
+ AUTO_MIGRATE = "automatically_migratable"
43
+ AUTO_WITH_REVIEW = "automatically_migratable_with_review"
44
+ PARTIAL = "partially_migratable"
45
+ MANUAL_REDESIGN = "manual_redesign_required"
46
+ RETIRE = "retire_or_consolidate"
47
+
48
+
49
+ class ColumnLineage(BaseModel):
50
+ column: str
51
+ table: str
52
+ source_columns: list[str] = Field(default_factory=list)
53
+ transformations: list[str] = Field(default_factory=list)
54
+ downstream: list[str] = Field(default_factory=list)
55
+
56
+
57
+ class TableLineage(BaseModel):
58
+ table: str
59
+ upstream: list[str] = Field(default_factory=list)
60
+ downstream: list[str] = Field(default_factory=list)
61
+ columns: list[ColumnLineage] = Field(default_factory=list)
62
+
63
+
64
+ class RiskFactor(BaseModel):
65
+ name: str
66
+ score: int
67
+ description: str
68
+ category: str = "complexity"
69
+
70
+
71
+ class MigrationObject(BaseModel):
72
+ name: str
73
+ object_type: ObjectType
74
+ source_path: str = ""
75
+ source_sql: str = ""
76
+ target_sql: str = ""
77
+ complexity_score: int = 0
78
+ risk_level: RiskLevel = RiskLevel.LOW
79
+ risk_factors: list[RiskFactor] = Field(default_factory=list)
80
+ migration_category: MigrationCategory = MigrationCategory.AUTO_MIGRATE
81
+ conversion_confidence: float = 0.0
82
+ auto_converted: list[str] = Field(default_factory=list)
83
+ requires_review: list[str] = Field(default_factory=list)
84
+ unsupported_features: list[str] = Field(default_factory=list)
85
+ dependencies: list[str] = Field(default_factory=list)
86
+ downstream_count: int = 0
87
+ business_rules: list[str] = Field(default_factory=list)
88
+ dbt_models: list[str] = Field(default_factory=list)
89
+ tests_generated: list[str] = Field(default_factory=list)
90
+ metadata: dict[str, Any] = Field(default_factory=dict)
91
+
92
+
93
+ class DashboardMetrics(BaseModel):
94
+ total_objects: int = 0
95
+ auto_migratable: int = 0
96
+ requires_review: int = 0
97
+ requires_redesign: int = 0
98
+ recommended_retirement: int = 0
99
+ conversion_completed_pct: float = 0.0
100
+ validation_passed_pct: float = 0.0
101
+ lineage_coverage_pct: float = 0.0
102
+ test_coverage_pct: float = 0.0
103
+ migration_risk_score: float = 0.0
104
+ estimated_annual_savings_usd: tuple[float, float] = (0.0, 0.0)
105
+
106
+
107
+ class ValidationResult(BaseModel):
108
+ object_name: str
109
+ check_name: str
110
+ passed: bool
111
+ source_value: str | float | int | None = None
112
+ target_value: str | float | int | None = None
113
+ difference: str | float | int | None = None
114
+ root_cause: str = ""
115
+ recommendation: str = ""
116
+
117
+
118
+ class MigrationReport(BaseModel):
119
+ source_dialect: Dialect
120
+ target_dialect: Dialect
121
+ repository_path: str
122
+ objects: list[MigrationObject] = Field(default_factory=list)
123
+ lineage: list[TableLineage] = Field(default_factory=list)
124
+ dashboard: DashboardMetrics = Field(default_factory=DashboardMetrics)
125
+ validation_results: list[ValidationResult] = Field(default_factory=list)
126
+ behavior_warnings: list[str] = Field(default_factory=list)
127
+ retirement_candidates: list[str] = Field(default_factory=list)
128
+ consolidation_opportunities: list[str] = Field(default_factory=list)
morphsql/parser/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SQL parser package."""
2
+
3
+ from morphsql.parser.sql_parser import (
4
+ count_sql_complexity,
5
+ detect_unsupported_features,
6
+ extract_columns,
7
+ extract_tables,
8
+ parse_sql,
9
+ parse_sql_multi,
10
+ )
11
+
12
+ __all__ = [
13
+ "count_sql_complexity",
14
+ "detect_unsupported_features",
15
+ "extract_columns",
16
+ "extract_tables",
17
+ "parse_sql",
18
+ "parse_sql_multi",
19
+ ]