@@ -0,0 +1,24 @@ | |||||
# Logs | |||||
logs | |||||
*.log | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
pnpm-debug.log* | |||||
lerna-debug.log* | |||||
node_modules | |||||
dist | |||||
dist-ssr | |||||
*.local | |||||
# Editor directories and files | |||||
.vscode/* | |||||
!.vscode/extensions.json | |||||
.idea | |||||
.DS_Store | |||||
*.suo | |||||
*.ntvs* | |||||
*.njsproj | |||||
*.sln | |||||
*.sw? |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"recommendations": ["Vue.volar"] | |||||
} |
@@ -0,0 +1,16 @@ | |||||
# Vue 3 + TypeScript + Vite | |||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. | |||||
## Recommended IDE Setup | |||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) | |||||
## Type Support For `.vue` Imports in TS | |||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps: | |||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled. | |||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette. | |||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471). |
@@ -0,0 +1,13 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<meta charset="UTF-8" /> | |||||
<link rel="icon" href="/favicon.ico" /> | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||||
<title>SQL 生成器</title> | |||||
</head> | |||||
<body> | |||||
<div id="app"></div> | |||||
<script type="module" src="/src/main.ts"></script> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,22 @@ | |||||
{ | |||||
"name": "sql-generator", | |||||
"private": true, | |||||
"version": "0.0.0", | |||||
"scripts": { | |||||
"dev": "vite", | |||||
"build": "vue-tsc --noEmit && vite build", | |||||
"preview": "vite preview" | |||||
}, | |||||
"dependencies": { | |||||
"monaco-editor": "^0.33.0", | |||||
"sql-formatter": "^4.0.2", | |||||
"tdesign-vue-next": "^0.14.1", | |||||
"vue": "^3.2.25" | |||||
}, | |||||
"devDependencies": { | |||||
"@vitejs/plugin-vue": "^2.3.1", | |||||
"typescript": "^4.5.4", | |||||
"vite": "^2.9.7", | |||||
"vue-tsc": "^0.34.7" | |||||
} | |||||
} |
@@ -0,0 +1,104 @@ | |||||
<script setup lang="ts"> | |||||
import {doGenerateSQL} from "./generator"; | |||||
import {onMounted, ref, toRaw} from "vue"; | |||||
import * as monaco from "monaco-editor"; | |||||
import IStandaloneCodeEditor = monaco.editor.IStandaloneCodeEditor; | |||||
import {format} from "sql-formatter"; | |||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' | |||||
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' | |||||
self.MonacoEnvironment = { | |||||
getWorker(_, label) { | |||||
if (label === 'json') { | |||||
return new jsonWorker() | |||||
} | |||||
return new editorWorker() | |||||
} | |||||
} | |||||
const inputEditor = ref<IStandaloneCodeEditor>(); | |||||
const outputEditor = ref<IStandaloneCodeEditor>(); | |||||
const inputContainer = ref<HTMLElement>(); | |||||
const outputContainer = ref<HTMLElement>(); | |||||
const getSQL = () => { | |||||
if (inputEditor.value && outputEditor.value) { | |||||
const inputJSON = JSON.parse(toRaw(inputEditor.value).getValue()); | |||||
let result = format(doGenerateSQL(inputJSON)); | |||||
// 针对执行引擎,处理自动格式化的问题 | |||||
result = result.replaceAll("{ {", "{{") | |||||
result = result.replaceAll("} }", "}}") | |||||
toRaw(outputEditor.value).setValue(result); | |||||
} | |||||
} | |||||
const initJSONValue = "{\n" + | |||||
" \"main\": {\n" + | |||||
" \"sql\": \"select * from @union_all_layer(分区 = 2021) where 分区 = #{分区}\",\n" + | |||||
" \"params\": {\n" + | |||||
" \"分区\": 2022\n" + | |||||
" }\n" + | |||||
" },\n" + | |||||
" \"union_all_layer\": {\n" + | |||||
" \"sql\": \"select * from xx where 分区 = #{分区}\"\n" + | |||||
" }\n" + | |||||
"}"; | |||||
onMounted(() => { | |||||
if (inputContainer.value) { | |||||
inputEditor.value = monaco.editor.create(inputContainer.value, { | |||||
value: localStorage.getItem('draft') ?? initJSONValue, | |||||
language: 'json', | |||||
theme: 'vs-dark', | |||||
formatOnPaste: true, | |||||
fontSize: 16, | |||||
minimap: { | |||||
enabled: false, | |||||
}, | |||||
}); | |||||
setInterval(() => { | |||||
if (inputEditor.value) { | |||||
localStorage.setItem('draft', toRaw(inputEditor.value).getValue()); | |||||
} | |||||
}, 3000) | |||||
} | |||||
if (outputContainer.value) { | |||||
outputEditor.value = monaco.editor.create(outputContainer.value, { | |||||
value: "", | |||||
language: 'sql', | |||||
theme: 'vs-dark', | |||||
formatOnPaste: true, | |||||
fontSize: 16, | |||||
minimap: { | |||||
enabled: false, | |||||
}, | |||||
}); | |||||
getSQL(); | |||||
} | |||||
}); | |||||
</script> | |||||
<template> | |||||
<h1> | |||||
SQL 生成器 | |||||
<t-button size="large" theme="primary" @click="getSQL" style="float: right"> 生成 SQL</t-button> | |||||
</h1> | |||||
<t-row :gutter="24"> | |||||
<t-col :xs="12" :sm="6"> | |||||
<div id="inputContainer" ref="inputContainer" style="height: 80vh; max-width: 100%"/> | |||||
</t-col> | |||||
<t-col :xs="12" :sm="6"> | |||||
<div id="outputContainer" ref="outputContainer" style="height: 80vh; max-width: 100%"/> | |||||
</t-col> | |||||
</t-row> | |||||
<div>yupi:你能体会手写 1500 行 SQL、牵一发而动全身的恐惧么?</div> | |||||
</template> | |||||
<style> | |||||
#app { | |||||
padding: 0 25px; | |||||
} | |||||
</style> |
@@ -0,0 +1,8 @@ | |||||
/// <reference types="vite/client" /> | |||||
declare module '*.vue' { | |||||
import type { DefineComponent } from 'vue' | |||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types | |||||
const component: DefineComponent<{}, {}, any> | |||||
export default component | |||||
} |
@@ -0,0 +1,91 @@ | |||||
/** | |||||
* 生成 SQL | |||||
* @param json | |||||
*/ | |||||
export function doGenerateSQL(json: InputJSON): string { | |||||
if (!json?.main) { | |||||
return ""; | |||||
} | |||||
const context = json; | |||||
const result = replaceParams(context.main, context); | |||||
return replaceSubSql(result, context); | |||||
} | |||||
/** | |||||
* 参数替换(params) | |||||
* @param currentNode | |||||
* @param context | |||||
* @param params 动态参数 | |||||
*/ | |||||
function replaceParams(currentNode: InputJSONValue, context: InputJSON, params?: Record<string, string>): string { | |||||
if (currentNode == null) { | |||||
return ""; | |||||
} | |||||
const sql = currentNode.sql ?? currentNode; | |||||
if (!sql) { | |||||
return ""; | |||||
} | |||||
// 动态、静态参数结合,且优先用静态参数 | |||||
params = {...(params ?? {}), ...currentNode.params}; | |||||
// 无需替换 | |||||
if (!params || Object.keys(params).length < 1) { | |||||
return sql; | |||||
} | |||||
let result = sql; | |||||
for (const paramsKey in params) { | |||||
const replacedKey = `#{${paramsKey}}`; | |||||
// 递归解析 | |||||
const replacement = replaceSubSql(params[paramsKey], context); | |||||
// find and replace | |||||
result = result.replaceAll(replacedKey, replacement); | |||||
} | |||||
return result; | |||||
} | |||||
/** | |||||
* 替换子 SQL(@xxx) | |||||
* @param sql | |||||
* @param context | |||||
*/ | |||||
function replaceSubSql(sql: string, context: InputJSON): string { | |||||
if (!sql) { | |||||
return ""; | |||||
} | |||||
const regExp = /@([\u4e00-\u9fa5_a-zA-Z0-9]+)\((.*?)\)/; | |||||
let result = sql; | |||||
result = String(result); | |||||
let regExpMatchArray = result.match(regExp); | |||||
// 依次替换 | |||||
while (regExpMatchArray && regExpMatchArray.length > 2) { | |||||
// 找到结果 | |||||
const subKey = regExpMatchArray[1]; | |||||
// 可用来替换的节点 | |||||
const replacementNode = context[subKey]; | |||||
// 没有可替换的节点 | |||||
if (!replacementNode) { | |||||
throw new Error(`${subKey} 不存在`); | |||||
} | |||||
// 获取要传递的动态参数 | |||||
// e.g. "a = b, c = d" | |||||
let paramsStr = regExpMatchArray[2]; | |||||
if (paramsStr) { | |||||
paramsStr = paramsStr.trim(); | |||||
} | |||||
// e.g. ["a = b", "c = d"] | |||||
const singleParamsStrArray = paramsStr.split(','); | |||||
// string => object | |||||
const params: Record<string, string> = {}; | |||||
for (const singleParamsStr of singleParamsStrArray) { | |||||
const keyValueArray = singleParamsStr.split('='); | |||||
if (keyValueArray.length < 2) { | |||||
continue; | |||||
} | |||||
const key = keyValueArray[0].trim(); | |||||
params[key] = keyValueArray[1].trim(); | |||||
} | |||||
const replacement = replaceParams(replacementNode, context, params); | |||||
result = result.replaceAll(regExpMatchArray[0], replacement); | |||||
regExpMatchArray = result.match(regExp); | |||||
} | |||||
return result; | |||||
} |
@@ -0,0 +1,17 @@ | |||||
/** | |||||
* 输入 JSON | |||||
*/ | |||||
interface InputJSON extends Record<string, InputJSONValue> { | |||||
// 入口文件 | |||||
main: InputJSONValue; | |||||
} | |||||
/** | |||||
* 输入 JSON Value | |||||
*/ | |||||
interface InputJSONValue { | |||||
// sql 语句 | |||||
sql: string; | |||||
// 静态参数 | |||||
params?: Record<string, string>; | |||||
} |
@@ -0,0 +1,9 @@ | |||||
import { createApp } from 'vue'; | |||||
import TDesign from 'tdesign-vue-next'; | |||||
import App from './app.vue'; | |||||
// 引入组件库全局样式资源 | |||||
import 'tdesign-vue-next/es/style/index.css'; | |||||
const app = createApp(App); | |||||
app.use(TDesign); | |||||
app.mount('#app'); | |||||
//# sourceMappingURL=main.js.map |
@@ -0,0 +1 @@ | |||||
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,GAAG,MAAM,WAAW,CAAC;AAE5B,cAAc;AACd,OAAO,qCAAqC,CAAC;AAE7C,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;AAC3B,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACjB,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC"} |
@@ -0,0 +1,10 @@ | |||||
import { createApp } from 'vue'; | |||||
import TDesign from 'tdesign-vue-next'; | |||||
import App from './app.vue'; | |||||
// 引入组件库全局样式资源 | |||||
import 'tdesign-vue-next/es/style/index.css'; | |||||
const app = createApp(App); | |||||
app.use(TDesign); | |||||
app.mount('#app'); |
@@ -0,0 +1,18 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"target": "esnext", | |||||
"useDefineForClassFields": true, | |||||
"module": "esnext", | |||||
"moduleResolution": "node", | |||||
"strict": true, | |||||
"jsx": "preserve", | |||||
"sourceMap": true, | |||||
"resolveJsonModule": true, | |||||
"isolatedModules": true, | |||||
"esModuleInterop": true, | |||||
"lib": ["esnext", "dom"], | |||||
"skipLibCheck": true | |||||
}, | |||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], | |||||
"references": [{ "path": "./tsconfig.node.json" }] | |||||
} |
@@ -0,0 +1,8 @@ | |||||
{ | |||||
"compilerOptions": { | |||||
"composite": true, | |||||
"module": "esnext", | |||||
"moduleResolution": "node" | |||||
}, | |||||
"include": ["vite.config.ts"] | |||||
} |
@@ -0,0 +1,7 @@ | |||||
import { defineConfig } from 'vite' | |||||
import vue from '@vitejs/plugin-vue' | |||||
// https://vitejs.dev/config/ | |||||
export default defineConfig({ | |||||
plugins: [vue()] | |||||
}) |