update
Some checks failed
Deploy / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-07-05 01:12:06 -06:00
parent 22f7c34f76
commit aaf8719943
5 changed files with 480 additions and 21 deletions

View File

@@ -3,7 +3,6 @@ import {
buildSetupSql, buildSetupSql,
defaultSqlTableId, defaultSqlTableId,
sqlTables, sqlTables,
type SqlColumn,
} from "../data/sqlTables" } from "../data/sqlTables"
export const selectedTableId = ref(defaultSqlTableId) export const selectedTableId = ref(defaultSqlTableId)
@@ -12,32 +11,49 @@ export function resetSqlTableSelection() {
selectedTableId.value = defaultSqlTableId selectedTableId.value = defaultSqlTableId
} }
// SELECT / WITH 属于查询,直接展示查询结果的列;其余(增删改)回显整张表
function isQuery(sql: string): boolean {
return /^\s*(SELECT|WITH)\b/i.test(sql)
}
export function buildSqlScript(studentSql: string) { export function buildSqlScript(studentSql: string) {
const table = const table =
sqlTables.find((item) => item.id === selectedTableId.value) ?? sqlTables.find((item) => item.id === selectedTableId.value) ??
sqlTables[0] sqlTables[0]
const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";") const normalizedSql = studentSql.trim().replace(/;?\s*$/, ";")
if (isQuery(studentSql.trim())) {
return [buildSetupSql(table), ".headers on", normalizedSql].join("\n\n")
}
return [ return [
buildSetupSql(table), buildSetupSql(table),
".output /dev/null", ".output /dev/null",
normalizedSql, normalizedSql,
".output stdout", ".output stdout",
".headers on",
`SELECT * FROM ${table.tableName};`, `SELECT * FROM ${table.tableName};`,
].join("\n\n") ].join("\n\n")
} }
export function parseResultRows( export interface SqlResult {
output: string, columns: string[]
columns: SqlColumn[], rows: Record<string, string | number>[]
): Record<string, string | number>[] { }
return output
// 输出为 sqlite CLI 的 list 模式(| 分隔),开启 .headers on 后首行是列名
export function parseResult(output: string): SqlResult {
const lines = output
.split("\n") .split("\n")
.map((line) => line.trim()) .map((line) => line.trim())
.filter(Boolean) .filter(Boolean)
.map((line) => { if (lines.length === 0) return { columns: [], rows: [] }
const cells = line.split("|") const columns = lines[0].split("|")
return Object.fromEntries( const rows = lines.slice(1).map((line, index) => {
columns.map((column, index) => [column.name, cells[index] ?? ""]), const cells = line.split("|")
) const record: Record<string, string | number> = { __key: index }
columns.forEach((column, i) => {
record[column] = cells[i] ?? ""
}) })
return record
})
return { columns, rows }
} }

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { DataTableColumns } from "naive-ui" import type { DataTableColumns } from "naive-ui"
import { computed, watch } from "vue" import { computed, h, watch } from "vue"
import { output, status } from "../composables/code" import { output, status } from "../composables/code"
import { parseResultRows, selectedTableId } from "../composables/sqlTable" import { parseResult, selectedTableId } from "../composables/sqlTable"
import { sqlTables } from "../data/sqlTables" import { sqlTables } from "../data/sqlTables"
import { Status } from "../types" import { Status } from "../types"
import OutputSection from "./OutputSection.vue" import OutputSection from "./OutputSection.vue"
@@ -13,9 +13,37 @@ const selectedTable = computed(
sqlTables[0], 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>(() => const tableColumns = computed<DataTableColumns>(() =>
selectedTable.value.columns.map((column) => ({ selectedTable.value.columns.map((column) => ({
title: column.name, title: renderColumnTitle(column.name, column.type),
key: column.name, key: column.name,
})), })),
) )
@@ -31,10 +59,17 @@ const initialRows = computed(() =>
), ),
) )
const resultRows = computed(() => const result = computed(() => parseResult(output.value))
parseResultRows(output.value, selectedTable.value.columns),
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, () => { watch(selectedTableId, () => {
output.value = "" output.value = ""
status.value = Status.NotStarted status.value = Status.NotStarted
@@ -50,7 +85,9 @@ watch(selectedTableId, () => {
> >
<template #1> <template #1>
<div class="table-panel"> <div class="table-panel">
<div class="panel-title">原始数据{{ selectedTable.label }}</div> <div class="panel-title">
原始数据{{ selectedTable.label }} · {{ selectedTable.tableName }}
</div>
<n-data-table <n-data-table
size="small" size="small"
:bordered="false" :bordered="false"
@@ -62,13 +99,13 @@ watch(selectedTableId, () => {
</template> </template>
<template #2> <template #2>
<div class="table-panel" v-if="status === Status.Accepted"> <div class="table-panel" v-if="status === Status.Accepted">
<div class="panel-title">运行后数据</div> <div class="panel-title">运行结果</div>
<n-data-table <n-data-table
size="small" size="small"
:bordered="false" :bordered="false"
:columns="tableColumns" :columns="resultColumns"
:data="resultRows" :data="resultRows"
:row-key="(row: any) => row.id" :row-key="(row: any) => row.__key"
/> />
</div> </div>
<OutputSection v-else /> <OutputSection v-else />

View File

@@ -10,6 +10,7 @@ import { cpp } from "./cpp"
import { c } from "./c" import { c } from "./c"
import { python } from "./python" import { python } from "./python"
import { turtle } from "./turtle" import { turtle } from "./turtle"
import { sql } from "./sql"
type ChineseCompletion = Pick< type ChineseCompletion = Pick<
Completion, Completion,
@@ -22,6 +23,7 @@ const chineseAnnotations: Record<string, ChineseCompletion[]> = {
turtle, turtle,
c, c,
cpp, cpp,
sql,
} }
export function enhanceCompletion(language: LANGUAGE): CompletionSource { export function enhanceCompletion(language: LANGUAGE): CompletionSource {

404
src/extensions/sql.ts Normal file
View 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()",
},
]

View File

@@ -17,7 +17,7 @@ for i in range(4):
turtle.done()` turtle.done()`
const sqlSource = "-- 在这里编写你的 SQL 语句\n" const sqlSource = ""
export const languageToId: { [key in string]: number } = { export const languageToId: { [key in string]: number } = {
c: 50, c: 50,