This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
||||
buildSetupSql,
|
||||
defaultSqlTableId,
|
||||
sqlTables,
|
||||
type SqlColumn,
|
||||
} from "../data/sqlTables"
|
||||
|
||||
export const selectedTableId = ref(defaultSqlTableId)
|
||||
@@ -12,32 +11,49 @@ export function resetSqlTableSelection() {
|
||||
selectedTableId.value = defaultSqlTableId
|
||||
}
|
||||
|
||||
// SELECT / WITH 属于查询,直接展示查询结果的列;其余(增删改)回显整张表
|
||||
function isQuery(sql: string): boolean {
|
||||
return /^\s*(SELECT|WITH)\b/i.test(sql)
|
||||
}
|
||||
|
||||
export function buildSqlScript(studentSql: string) {
|
||||
const table =
|
||||
sqlTables.find((item) => item.id === selectedTableId.value) ??
|
||||
sqlTables[0]
|
||||
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
|
||||
if (isQuery(studentSql.trim())) {
|
||||
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
|
||||
}
|
||||
return [
|
||||
buildSetupSql(table),
|
||||
".output /dev/null",
|
||||
normalizedSql,
|
||||
".output stdout",
|
||||
".headers on",
|
||||
`SELECT * FROM ${table.tableName};`,
|
||||
].join("\n\n")
|
||||
}
|
||||
|
||||
export function parseResultRows(
|
||||
output: string,
|
||||
columns: SqlColumn[],
|
||||
): Record<string, string | number>[] {
|
||||
return output
|
||||
export interface SqlResult {
|
||||
columns: string[]
|
||||
rows: Record<string, string | number>[]
|
||||
}
|
||||
|
||||
// 输出为 sqlite CLI 的 list 模式(| 分隔),开启 .headers on 后首行是列名
|
||||
export function parseResult(output: string): SqlResult {
|
||||
const lines = output
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
if (lines.length === 0) return { columns: [], rows: [] }
|
||||
const columns = lines[0].split("|")
|
||||
const rows = lines.slice(1).map((line, index) => {
|
||||
const cells = line.split("|")
|
||||
return Object.fromEntries(
|
||||
columns.map((column, index) => [column.name, cells[index] ?? ""]),
|
||||
)
|
||||
const record: Record<string, string | number> = { __key: index }
|
||||
columns.forEach((column, i) => {
|
||||
record[column] = cells[i] ?? ""
|
||||
})
|
||||
return record
|
||||
})
|
||||
return { columns, rows }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DataTableColumns } from "naive-ui"
|
||||
import { computed, watch } from "vue"
|
||||
import { computed, h, watch } from "vue"
|
||||
import { output, status } from "../composables/code"
|
||||
import { parseResultRows, selectedTableId } from "../composables/sqlTable"
|
||||
import { parseResult, selectedTableId } from "../composables/sqlTable"
|
||||
import { sqlTables } from "../data/sqlTables"
|
||||
import { Status } from "../types"
|
||||
import OutputSection from "./OutputSection.vue"
|
||||
@@ -13,9 +13,37 @@ const selectedTable = computed(
|
||||
sqlTables[0],
|
||||
)
|
||||
|
||||
// 列头展示:列名 + 暗色小字的数据类型(仅取基础类型,去掉约束如 NOT NULL)
|
||||
function baseType(type: string): string {
|
||||
return type.trim().split(/\s+/)[0]
|
||||
}
|
||||
|
||||
function renderColumnTitle(name: string, type?: string) {
|
||||
return () =>
|
||||
h("span", null, [
|
||||
name,
|
||||
type
|
||||
? h(
|
||||
"span",
|
||||
{
|
||||
style:
|
||||
"font-size:12px;opacity:0.55;margin-left:4px;font-weight:normal;",
|
||||
},
|
||||
baseType(type),
|
||||
)
|
||||
: null,
|
||||
])
|
||||
}
|
||||
|
||||
const columnTypeMap = computed<Record<string, string>>(() =>
|
||||
Object.fromEntries(
|
||||
selectedTable.value.columns.map((column) => [column.name, column.type]),
|
||||
),
|
||||
)
|
||||
|
||||
const tableColumns = computed<DataTableColumns>(() =>
|
||||
selectedTable.value.columns.map((column) => ({
|
||||
title: column.name,
|
||||
title: renderColumnTitle(column.name, column.type),
|
||||
key: column.name,
|
||||
})),
|
||||
)
|
||||
@@ -31,10 +59,17 @@ const initialRows = computed(() =>
|
||||
),
|
||||
)
|
||||
|
||||
const resultRows = computed(() =>
|
||||
parseResultRows(output.value, selectedTable.value.columns),
|
||||
const result = computed(() => parseResult(output.value))
|
||||
|
||||
const resultColumns = computed<DataTableColumns>(() =>
|
||||
result.value.columns.map((name) => ({
|
||||
title: renderColumnTitle(name, columnTypeMap.value[name]),
|
||||
key: name,
|
||||
})),
|
||||
)
|
||||
|
||||
const resultRows = computed(() => result.value.rows)
|
||||
|
||||
watch(selectedTableId, () => {
|
||||
output.value = ""
|
||||
status.value = Status.NotStarted
|
||||
@@ -50,7 +85,9 @@ watch(selectedTableId, () => {
|
||||
>
|
||||
<template #1>
|
||||
<div class="table-panel">
|
||||
<div class="panel-title">原始数据({{ selectedTable.label }})</div>
|
||||
<div class="panel-title">
|
||||
原始数据({{ selectedTable.label }} · {{ selectedTable.tableName }})
|
||||
</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
@@ -62,13 +99,13 @@ watch(selectedTableId, () => {
|
||||
</template>
|
||||
<template #2>
|
||||
<div class="table-panel" v-if="status === Status.Accepted">
|
||||
<div class="panel-title">运行后数据</div>
|
||||
<div class="panel-title">运行结果</div>
|
||||
<n-data-table
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:columns="tableColumns"
|
||||
:columns="resultColumns"
|
||||
:data="resultRows"
|
||||
:row-key="(row: any) => row.id"
|
||||
:row-key="(row: any) => row.__key"
|
||||
/>
|
||||
</div>
|
||||
<OutputSection v-else />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { cpp } from "./cpp"
|
||||
import { c } from "./c"
|
||||
import { python } from "./python"
|
||||
import { turtle } from "./turtle"
|
||||
import { sql } from "./sql"
|
||||
|
||||
type ChineseCompletion = Pick<
|
||||
Completion,
|
||||
@@ -22,6 +23,7 @@ const chineseAnnotations: Record<string, ChineseCompletion[]> = {
|
||||
turtle,
|
||||
c,
|
||||
cpp,
|
||||
sql,
|
||||
}
|
||||
|
||||
export function enhanceCompletion(language: LANGUAGE): CompletionSource {
|
||||
|
||||
404
src/extensions/sql.ts
Normal file
404
src/extensions/sql.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
export const sql = [
|
||||
{
|
||||
label: "SELECT",
|
||||
detail: "查询数据",
|
||||
type: "keyword",
|
||||
info: "从表中查询数据,后面接要查询的列名,用 * 表示所有列。",
|
||||
boost: 100,
|
||||
apply: "SELECT ",
|
||||
},
|
||||
{
|
||||
label: "FROM",
|
||||
detail: "指定表",
|
||||
type: "keyword",
|
||||
info: "指定要查询的表,和 SELECT 搭配使用,如 SELECT * FROM 表名。",
|
||||
boost: 98,
|
||||
apply: "FROM ",
|
||||
},
|
||||
{
|
||||
label: "WHERE",
|
||||
detail: "筛选条件",
|
||||
type: "keyword",
|
||||
info: "按条件筛选行,只保留满足条件的数据,如 WHERE age > 18。",
|
||||
boost: 96,
|
||||
apply: "WHERE ",
|
||||
},
|
||||
{
|
||||
label: "ORDER BY",
|
||||
detail: "排序",
|
||||
type: "keyword",
|
||||
info: "按指定列排序,默认从小到大(ASC),加 DESC 表示从大到小。",
|
||||
boost: 90,
|
||||
apply: "ORDER BY ",
|
||||
},
|
||||
{
|
||||
label: "GROUP BY",
|
||||
detail: "分组",
|
||||
type: "keyword",
|
||||
info: "按指定列分组,常配合 COUNT、SUM 等聚合函数统计每组数据。",
|
||||
boost: 88,
|
||||
apply: "GROUP BY ",
|
||||
},
|
||||
{
|
||||
label: "HAVING",
|
||||
detail: "分组后筛选",
|
||||
type: "keyword",
|
||||
info: "对分组后的结果再筛选,WHERE 筛选行,HAVING 筛选组。",
|
||||
boost: 86,
|
||||
apply: "HAVING ",
|
||||
},
|
||||
{
|
||||
label: "LIMIT",
|
||||
detail: "限制条数",
|
||||
type: "keyword",
|
||||
info: "限制返回的行数,如 LIMIT 5 只取前 5 条,常和 ORDER BY 搭配。",
|
||||
boost: 84,
|
||||
apply: "LIMIT ",
|
||||
},
|
||||
{
|
||||
label: "DISTINCT",
|
||||
detail: "去重",
|
||||
type: "keyword",
|
||||
info: "去掉查询结果中的重复值,如 SELECT DISTINCT city FROM users。",
|
||||
boost: 82,
|
||||
apply: "DISTINCT ",
|
||||
},
|
||||
{
|
||||
label: "AS",
|
||||
detail: "起别名",
|
||||
type: "keyword",
|
||||
info: "给列或表起别名,让结果更易读,如 SELECT name AS 姓名。",
|
||||
boost: 80,
|
||||
apply: "AS ",
|
||||
},
|
||||
{
|
||||
label: "JOIN",
|
||||
detail: "连接表",
|
||||
type: "keyword",
|
||||
info: "把两张表按条件连接起来查询,需要用 ON 指定连接条件。",
|
||||
boost: 78,
|
||||
apply: "JOIN ",
|
||||
},
|
||||
{
|
||||
label: "LEFT JOIN",
|
||||
detail: "左连接",
|
||||
type: "keyword",
|
||||
info: "以左表为主连接右表,左表的行都保留,右表没匹配的补 NULL。",
|
||||
boost: 76,
|
||||
apply: "LEFT JOIN ",
|
||||
},
|
||||
{
|
||||
label: "ON",
|
||||
detail: "连接条件",
|
||||
type: "keyword",
|
||||
info: "指定两张表的连接条件,如 ON a.id = b.user_id,和 JOIN 搭配。",
|
||||
boost: 74,
|
||||
apply: "ON ",
|
||||
},
|
||||
{
|
||||
label: "INSERT INTO",
|
||||
detail: "插入数据",
|
||||
type: "keyword",
|
||||
info: "向表中插入新行,如 INSERT INTO 表名 (列1, 列2) VALUES (值1, 值2)。",
|
||||
boost: 72,
|
||||
apply: "INSERT INTO ",
|
||||
},
|
||||
{
|
||||
label: "VALUES",
|
||||
detail: "插入的值",
|
||||
type: "keyword",
|
||||
info: "和 INSERT INTO 搭配,写具体要插入的值,顺序要和列名对应。",
|
||||
boost: 70,
|
||||
apply: "VALUES ",
|
||||
},
|
||||
{
|
||||
label: "UPDATE",
|
||||
detail: "更新数据",
|
||||
type: "keyword",
|
||||
info: "修改表中已有的数据,配合 SET 设置新值,别忘了加 WHERE 限定范围。",
|
||||
boost: 68,
|
||||
apply: "UPDATE ",
|
||||
},
|
||||
{
|
||||
label: "SET",
|
||||
detail: "设置新值",
|
||||
type: "keyword",
|
||||
info: "和 UPDATE 搭配,指定要修改的列和新值,如 SET score = 90。",
|
||||
boost: 66,
|
||||
apply: "SET ",
|
||||
},
|
||||
{
|
||||
label: "DELETE FROM",
|
||||
detail: "删除数据",
|
||||
type: "keyword",
|
||||
info: "删除表中的行,一定要配合 WHERE 使用,否则会删掉整张表的数据。",
|
||||
boost: 64,
|
||||
apply: "DELETE FROM ",
|
||||
},
|
||||
{
|
||||
label: "CREATE TABLE",
|
||||
detail: "创建表",
|
||||
type: "keyword",
|
||||
info: "新建一张表,需要定义列名和类型,如 CREATE TABLE users (id INTEGER, name TEXT)。",
|
||||
boost: 62,
|
||||
apply: "CREATE TABLE ",
|
||||
},
|
||||
{
|
||||
label: "DROP TABLE",
|
||||
detail: "删除表",
|
||||
type: "keyword",
|
||||
info: "删除整张表(包括结构和数据),操作不可恢复,要谨慎使用。",
|
||||
boost: 60,
|
||||
apply: "DROP TABLE ",
|
||||
},
|
||||
{
|
||||
label: "ALTER TABLE",
|
||||
detail: "修改表结构",
|
||||
type: "keyword",
|
||||
info: "修改已有表的结构,比如添加列:ALTER TABLE 表名 ADD COLUMN 列名 类型。",
|
||||
boost: 58,
|
||||
apply: "ALTER TABLE ",
|
||||
},
|
||||
{
|
||||
label: "AND",
|
||||
detail: "并且",
|
||||
type: "keyword",
|
||||
info: "连接多个条件,全部成立才算满足,如 WHERE age > 18 AND city = '上海'。",
|
||||
boost: 56,
|
||||
apply: "AND ",
|
||||
},
|
||||
{
|
||||
label: "OR",
|
||||
detail: "或者",
|
||||
type: "keyword",
|
||||
info: "连接多个条件,任意一个成立就算满足。",
|
||||
boost: 54,
|
||||
apply: "OR ",
|
||||
},
|
||||
{
|
||||
label: "NOT",
|
||||
detail: "取反",
|
||||
type: "keyword",
|
||||
info: "对条件取反,如 NOT IN、NOT LIKE、IS NOT NULL。",
|
||||
boost: 52,
|
||||
apply: "NOT ",
|
||||
},
|
||||
{
|
||||
label: "IN",
|
||||
detail: "在列表中",
|
||||
type: "keyword",
|
||||
info: "判断值是否在给定列表中,如 WHERE city IN ('北京', '上海')。",
|
||||
boost: 50,
|
||||
apply: "IN ",
|
||||
},
|
||||
{
|
||||
label: "BETWEEN",
|
||||
detail: "在区间内",
|
||||
type: "keyword",
|
||||
info: "判断值是否在某个范围内(包含两端),如 BETWEEN 60 AND 100。",
|
||||
boost: 48,
|
||||
apply: "BETWEEN ",
|
||||
},
|
||||
{
|
||||
label: "LIKE",
|
||||
detail: "模糊匹配",
|
||||
type: "keyword",
|
||||
info: "模糊查询,% 匹配任意多个字符,_ 匹配单个字符,如 LIKE '张%'。",
|
||||
boost: 46,
|
||||
apply: "LIKE ",
|
||||
},
|
||||
{
|
||||
label: "IS NULL",
|
||||
detail: "是否为空",
|
||||
type: "keyword",
|
||||
info: "判断值是否为 NULL(空值),不能写 = NULL,要用 IS NULL。",
|
||||
boost: 44,
|
||||
},
|
||||
{
|
||||
label: "IS NOT NULL",
|
||||
detail: "是否非空",
|
||||
type: "keyword",
|
||||
info: "判断值不为 NULL,常用于过滤掉缺失数据的行。",
|
||||
boost: 42,
|
||||
},
|
||||
{
|
||||
label: "ASC",
|
||||
detail: "升序",
|
||||
type: "keyword",
|
||||
info: "排序时从小到大排列,是 ORDER BY 的默认方式,可以省略。",
|
||||
boost: 40,
|
||||
},
|
||||
{
|
||||
label: "DESC",
|
||||
detail: "降序",
|
||||
type: "keyword",
|
||||
info: "排序时从大到小排列,如 ORDER BY score DESC 按分数从高到低。",
|
||||
boost: 41,
|
||||
},
|
||||
{
|
||||
label: "UNION",
|
||||
detail: "合并结果",
|
||||
type: "keyword",
|
||||
info: "合并两个查询的结果并去重,两个查询的列数和类型要一致。",
|
||||
boost: 38,
|
||||
apply: "UNION ",
|
||||
},
|
||||
{
|
||||
label: "CASE",
|
||||
detail: "条件表达式",
|
||||
type: "keyword",
|
||||
info: "类似 if/else 的条件判断,语法 CASE WHEN 条件 THEN 值 ELSE 值 END。",
|
||||
boost: 36,
|
||||
apply: "CASE ",
|
||||
},
|
||||
{
|
||||
label: "WHEN",
|
||||
detail: "当条件成立",
|
||||
type: "keyword",
|
||||
info: "和 CASE 搭配,写判断条件,成立时返回 THEN 后面的值。",
|
||||
boost: 34,
|
||||
apply: "WHEN ",
|
||||
},
|
||||
{
|
||||
label: "THEN",
|
||||
detail: "返回值",
|
||||
type: "keyword",
|
||||
info: "和 WHEN 搭配,条件成立时返回的结果。",
|
||||
boost: 32,
|
||||
apply: "THEN ",
|
||||
},
|
||||
{
|
||||
label: "ELSE",
|
||||
detail: "否则",
|
||||
type: "keyword",
|
||||
info: "CASE 中所有 WHEN 都不成立时返回的默认值。",
|
||||
boost: 30,
|
||||
apply: "ELSE ",
|
||||
},
|
||||
{
|
||||
label: "END",
|
||||
detail: "结束 CASE",
|
||||
type: "keyword",
|
||||
info: "标记 CASE 表达式的结束,写 CASE 时不要漏掉。",
|
||||
boost: 28,
|
||||
},
|
||||
{
|
||||
label: "NULL",
|
||||
detail: "空值",
|
||||
type: "keyword",
|
||||
info: "表示没有值(缺失),判断时要用 IS NULL / IS NOT NULL。",
|
||||
boost: 26,
|
||||
},
|
||||
{
|
||||
label: "COUNT",
|
||||
detail: "统计行数",
|
||||
type: "function",
|
||||
info: "统计行数,COUNT(*) 统计所有行,COUNT(列名) 不统计 NULL。",
|
||||
boost: 92,
|
||||
apply: "COUNT()",
|
||||
},
|
||||
{
|
||||
label: "SUM",
|
||||
detail: "求和",
|
||||
type: "function",
|
||||
info: "对某一列求和,只能用于数字列,如 SUM(score)。",
|
||||
boost: 87,
|
||||
apply: "SUM()",
|
||||
},
|
||||
{
|
||||
label: "AVG",
|
||||
detail: "平均值",
|
||||
type: "function",
|
||||
info: "计算某一列的平均值,会自动忽略 NULL,如 AVG(score)。",
|
||||
boost: 85,
|
||||
apply: "AVG()",
|
||||
},
|
||||
{
|
||||
label: "MAX",
|
||||
detail: "最大值",
|
||||
type: "function",
|
||||
info: "求某一列的最大值,如 MAX(score) 找最高分。",
|
||||
boost: 83,
|
||||
apply: "MAX()",
|
||||
},
|
||||
{
|
||||
label: "MIN",
|
||||
detail: "最小值",
|
||||
type: "function",
|
||||
info: "求某一列的最小值,如 MIN(score) 找最低分。",
|
||||
boost: 81,
|
||||
apply: "MIN()",
|
||||
},
|
||||
{
|
||||
label: "LENGTH",
|
||||
detail: "字符串长度",
|
||||
type: "function",
|
||||
info: "返回字符串的字符个数,如 LENGTH(name)。",
|
||||
boost: 79,
|
||||
apply: "LENGTH()",
|
||||
},
|
||||
{
|
||||
label: "UPPER",
|
||||
detail: "转大写",
|
||||
type: "function",
|
||||
info: "把字符串中的字母转成大写,如 UPPER('abc') 得到 'ABC'。",
|
||||
boost: 77,
|
||||
apply: "UPPER()",
|
||||
},
|
||||
{
|
||||
label: "LOWER",
|
||||
detail: "转小写",
|
||||
type: "function",
|
||||
info: "把字符串中的字母转成小写,如 LOWER('ABC') 得到 'abc'。",
|
||||
boost: 75,
|
||||
apply: "LOWER()",
|
||||
},
|
||||
{
|
||||
label: "SUBSTR",
|
||||
detail: "截取子串",
|
||||
type: "function",
|
||||
info: "截取字符串的一部分,语法 SUBSTR(字符串, 起始位置, 长度),位置从 1 开始。",
|
||||
boost: 73,
|
||||
apply: "SUBSTR()",
|
||||
},
|
||||
{
|
||||
label: "REPLACE",
|
||||
detail: "替换字符串",
|
||||
type: "function",
|
||||
info: "把字符串中的内容替换成新内容,语法 REPLACE(字符串, 旧内容, 新内容)。",
|
||||
boost: 71,
|
||||
apply: "REPLACE()",
|
||||
},
|
||||
{
|
||||
label: "ROUND",
|
||||
detail: "四舍五入",
|
||||
type: "function",
|
||||
info: "按指定小数位四舍五入,如 ROUND(3.456, 2) 得到 3.46。",
|
||||
boost: 69,
|
||||
apply: "ROUND()",
|
||||
},
|
||||
{
|
||||
label: "ABS",
|
||||
detail: "绝对值",
|
||||
type: "function",
|
||||
info: "返回数字的绝对值,把负数变成正数,如 ABS(-5) 得到 5。",
|
||||
boost: 67,
|
||||
apply: "ABS()",
|
||||
},
|
||||
{
|
||||
label: "IFNULL",
|
||||
detail: "空值替代",
|
||||
type: "function",
|
||||
info: "如果第一个值是 NULL 就返回第二个值,如 IFNULL(score, 0) 把空分数当 0。",
|
||||
boost: 65,
|
||||
apply: "IFNULL()",
|
||||
},
|
||||
{
|
||||
label: "TRIM",
|
||||
detail: "去首尾空格",
|
||||
type: "function",
|
||||
info: "去掉字符串首尾的空格,常用于清理输入数据。",
|
||||
boost: 63,
|
||||
apply: "TRIM()",
|
||||
},
|
||||
]
|
||||
@@ -17,7 +17,7 @@ for i in range(4):
|
||||
|
||||
turtle.done()`
|
||||
|
||||
const sqlSource = "-- 在这里编写你的 SQL 语句\n"
|
||||
const sqlSource = ""
|
||||
|
||||
export const languageToId: { [key in string]: number } = {
|
||||
c: 50,
|
||||
|
||||
Reference in New Issue
Block a user