Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +6 -35
- .gitignore +19 -0
- .hfignore +17 -0
- LICENSE +190 -0
- MODEL_CARD.md +47 -0
- PROJECT.md +115 -0
- README.md +62 -9
- README_HF_SPACE.md +68 -0
- app.py +534 -0
- datasets/.gitignore +2 -0
- datasets/leaderboard.json +12 -0
- datasets/vertica_snowflake_pairs.jsonl +256 -0
- demo/__init__.py +0 -0
- demo/handlers.py +1376 -0
- demo/theme.py +265 -0
- examples/ml_features/churn_feature_sql.sql +40 -0
- examples/vertica_legacy/procedures/SP_BUILD_CUSTOMER_DAILY.sql +61 -0
- examples/vertica_legacy/queries/customer_lifetime_value.sql +60 -0
- examples/vertica_legacy/tables/customer_tables.sql +37 -0
- examples/vertica_legacy/tables/refunds.sql +12 -0
- examples/vertica_legacy/views/V_CUSTOMER_METRICS.sql +25 -0
- model/README.md +59 -0
- model/config.json +34 -0
- model/rewrite_vocabulary.json +12 -0
- model/risk_classifier.joblib +3 -0
- morphsql/__init__.py +22 -0
- morphsql/ai/__init__.py +14 -0
- morphsql/ai/agent.py +251 -0
- morphsql/ai/pipeline.py +82 -0
- morphsql/ai/risk_model.py +195 -0
- morphsql/assistant/__init__.py +5 -0
- morphsql/assistant/copilot.py +370 -0
- morphsql/cli.py +275 -0
- morphsql/dbt_generator/__init__.py +5 -0
- morphsql/dbt_generator/decomposer.py +520 -0
- morphsql/eval/__init__.py +14 -0
- morphsql/eval/leaderboard.py +68 -0
- morphsql/eval/metrics.py +170 -0
- morphsql/eval/pairs.py +274 -0
- morphsql/intelligence/__init__.py +12 -0
- morphsql/intelligence/lineage_viz.py +119 -0
- morphsql/intelligence/rag.py +168 -0
- morphsql/intelligence/rationalization.py +65 -0
- morphsql/intelligence/runbook.py +113 -0
- morphsql/knowledge/__init__.py +15 -0
- morphsql/knowledge/behavior.py +227 -0
- morphsql/lineage/__init__.py +21 -0
- morphsql/lineage/builder.py +243 -0
- morphsql/models.py +128 -0
- morphsql/parser/__init__.py +19 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
*
|
| 5 |
-
*.
|
| 6 |
-
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned:
|
| 11 |
license: apache-2.0
|
| 12 |
-
short_description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
]
|