v26.10.1 正式版

修复部分问题,优化性能,进一步推进类型系统
This commit is contained in:
LIm1tZz7 committed 2026-10-05 22:22:23 +08:00
1 parent 48eec484b7
commit 8ce4239452
89 files changed
+5808 -884

No files matched your search

+61 -16
View File
@@ -2,6 +2,9 @@
// 请求都带 X-ChuSQL-Database 头;401 时广播登录事件。
export const AUTH_EVENT = 'chusql-auth-required';
let editing = null;
let consoleTransaction = null;
const pageOwner = [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, '0')).join('');
// 是否是普通对象。
function isObject(value) {
@@ -12,6 +15,9 @@ function isObject(value) {
async function request(url, init, database) {
const headers = new Headers(init && init.headers ? init.headers : undefined);
if (database) headers.set('X-ChuSQL-Database', database);
if (editing && editing.database === database && url.startsWith('/api/tables') && (!init || init.method === 'GET')) {
headers.set('X-ChuSQL-Edit', editing.id);
}
let response;
try {
response = await fetch(url, { credentials: 'same-origin', ...(init ?? {}), headers });
@@ -31,7 +37,10 @@ async function request(url, init, database) {
const message = isObject(body) && typeof body.message === 'string'
? body.message
: `请求失败(HTTP ${response.status})。`;
throw new Error(message);
const error = new Error(message);
error.result = body;
error.code = body?.error;
throw error;
}
return body;
}
@@ -215,33 +224,68 @@ export async function deleteRow(database, table, id) {
await request(`/api/tables/${encodeURIComponent(table)}/rows/${encodeURIComponent(id)}`, jsonInit('DELETE'), database);
}
// 逐条提交,遇到第一条失败就停,返回已经成功了几条。
export async function commitChanges(database, changes) {
let applied = 0;
for (const [index, change] of changes.entries()) {
try {
if (change.op === 'insert') await insertRow(database, change.table, change.values);
else if (change.op === 'delete') await deleteRow(database, change.table, change.pk);
else await updateRow(database, change.table, change.pk, change.set);
applied += 1;
} catch (error) {
return { ok: false, applied, errors: [{ index, message: error instanceof Error ? error.message : String(error) }] };
}
// 为页面开启固定数据库的编辑快照。
export async function beginEdit(id, database) {
await request('/api/edit/begin', jsonInit('POST', { id, database }));
editing = { id, database };
}
// 在服务端事务中原子提交编辑集。
export async function commitChanges(id, database, changes) {
try {
const result = await request('/api/edit/commit', jsonInit('POST', { id, database, changes }));
if (!isObject(result) || result.ok !== true || result.state !== 'committed') throw new Error('服务器未返回可确认的提交结果。');
editing = null;
return result;
} catch (error) {
if (isObject(error.result) && typeof error.result.state === 'string') return error.result;
return { ok: false, applied: 0, state: error.result ? 'lost' : 'unknown',
reloadRequired: Boolean(error.result), message: error.message };
}
}
// 回滚编辑快照并清除本地事务标识。
export async function rollbackEdit(id, database) {
const result = await request('/api/edit/rollback', jsonInit('POST', { id, database }));
editing = null;
return result;
}
// 忘记已失效的登录会话编辑标识。
export function forgetEdit() {
editing = null;
consoleTransaction = null;
}
// 标签关闭时回滚它持有的控制台事务。
export async function rollbackConsole(tabId) {
if (!consoleTransaction || consoleTransaction.console !== `${pageOwner}:${tabId ?? 'default'}`) return;
await request('/api/query/rollback', jsonInit('POST', { console: consoleTransaction.console }));
consoleTransaction = null;
}
// 页面离开时请求清理控制台事务。
export function closeConsoleOnPagehide() {
if (consoleTransaction) {
navigator.sendBeacon('/api/query/rollback', new Blob([JSON.stringify({ console: consoleTransaction.console })], { type: 'application/json' }));
}
return { ok: true, applied };
}
// ---------------------------------------------------------------- 查询
// 执行 SQL 并规整结果。
export async function query(sql, database) {
export async function query(sql, database, tabId) {
const started = performance.now();
try {
const body = await request('/api/query', jsonInit('POST', { sql }), database);
const console = `${pageOwner}:${tabId ?? 'default'}`;
const init = jsonInit('POST', { sql });
init.headers['X-ChuSQL-Console'] = console;
const body = await request('/api/query', init, database);
if (!isObject(body) || !Array.isArray(body.columns) || !Array.isArray(body.rows)) {
throw new Error('服务器返回的查询结果无法识别。');
}
const durationMs = performance.now() - started;
consoleTransaction = body.transaction === true ? { console, database } : null;
return {
type: body.columns.length ? 'rows' : 'affected',
columns: body.columns,
@@ -249,6 +293,7 @@ export async function query(sql, database) {
affected: typeof body.rowCount === 'number' ? body.rowCount : 0,
truncated: body.truncated === true,
database: typeof body.database === 'string' ? body.database : undefined,
transactionActive: body.transaction === true,
durationMs,
};
} catch (error) {
+185 -40
View File
@@ -46,6 +46,12 @@ const state = {
tabs: [],
activeId: undefined,
changes: createEmptyChangeSet(),
edit: null,
editStarting: null,
editAttempt: null,
editBusy: false,
editNeedsReload: false,
editUncertain: null,
insertSeq: 0,
messages: [],
tabSeq: 0,
@@ -301,8 +307,8 @@ function renderSidebar() {
<section class="side-section">
<div class="side-title">变更</div>
<div class="side-row">
<button data-action="commit"${count ? '' : ' disabled'}>提交 ${count}</button>
<button data-action="rollback"${count ? '' : ' disabled'}>撤销</button>
<button data-action="commit"${count && !state.editBusy && !state.editNeedsReload ? '' : ' disabled'}>提交 ${count}</button>
<button data-action="rollback"${(state.edit || state.editAttempt) && !state.editBusy ? '' : ' disabled'}>撤销</button>
</div>
</section>`;
}
@@ -364,7 +370,9 @@ function renderTableTab(el, tab) {
}
const columns = data.columns;
const keyColumn = columns.find((column) => column.name === 'id');
const editable = Boolean(keyColumn);
const editable = Boolean(keyColumn) && tab.table !== accountsTable && Boolean(state.edit)
&& data.editId === state.edit.id && !data.loading
&& !state.editBusy && !state.editNeedsReload && !state.editUncertain;
const filters = data.filters ?? {};
const count = changeCount(state.changes);
@@ -413,8 +421,8 @@ function renderTableTab(el, tab) {
<button data-action="insert-row"${editable ? '' : ' disabled'}>+ 新增行</button>
<button data-action="delete-selected"${editable && (tab.selected?.size ?? 0) ? '' : ' disabled'}>删除选中</button>
<span class="sep"></span>
<button class="primary" data-action="commit"${count ? '' : ' disabled'}>提交 (${count})</button>
<button data-action="rollback"${count ? '' : ' disabled'}>撤销</button>
<button class="primary" data-action="commit"${count && !state.editBusy && !state.editNeedsReload ? '' : ' disabled'}>提交 (${count})</button>
<button data-action="rollback"${(state.edit || state.editAttempt) && !state.editBusy ? '' : ' disabled'}>撤销</button>
<span class="sep"></span>
<button data-action="reload-table">刷新</button>
<span class="flex-spacer"></span>
@@ -438,7 +446,7 @@ function renderTableTab(el, tab) {
${data.rows.length || stagedInserts.length ? '' : '<p class="empty">这张表没有数据。</p>'}
</div>
${data.loading ? '<div class="grid-loading">加载中…</div>' : ''}
${editable ? '' : '<p class="notice">这张表没有 id 列,只能浏览。</p>'}
${editable ? '' : '<p class="notice">当前表或事务状态仅允许浏览;系统身份请通过 SQL 管理。</p>'}
</div>`;
}
@@ -650,8 +658,8 @@ function renderBottom() {
<button class="${state.bottomTab === 'changes' ? 'active' : ''}" data-action="bottom-tab" data-tab="changes">待提交变更 (${count})</button>
<button class="${state.bottomTab === 'messages' ? 'active' : ''}" data-action="bottom-tab" data-tab="messages">消息 (${state.messages.length})</button>
<span class="flex-spacer"></span>
<button data-action="commit"${count ? '' : ' disabled'}>提交全部</button>
<button data-action="rollback"${count ? '' : ' disabled'}>撤销全部</button>
<button data-action="commit"${count && !state.editBusy && !state.editNeedsReload ? '' : ' disabled'}>提交全部</button>
<button data-action="rollback"${(state.edit || state.editAttempt) && !state.editBusy ? '' : ' disabled'}>撤销全部</button>
<button data-action="copy-sql"${count ? '' : ' disabled'}>复制 SQL</button>
</div>
<div class="bottom-body">${state.bottomTab === 'changes' ? changeList : messages}</div>`;
@@ -694,19 +702,36 @@ async function loadWorkspace() {
// 切换当前数据库。
async function switchDatabase(name) {
if (state.editBusy || state.editStarting) return;
if (name === state.database) return;
if (changeCount(state.changes) > 0) {
log('warn', '请先提交或撤销当前库的修改,再切换数据库。');
renderSidebar();
return;
}
const previousEdit = state.edit ?? state.editAttempt;
state.editBusy = true;
afterChange();
try {
if (previousEdit) await api.rollbackEdit(previousEdit.id, previousEdit.database);
} catch (error) {
state.editBusy = false;
fail(error);
afterChange();
return;
}
state.edit = null;
state.editAttempt = null;
const result = await api.query(`USE ${name}`, state.database);
state.editBusy = false;
if (result.type === 'error') {
fail(new Error(result.message));
renderSidebar();
return;
}
state.database = result.database ?? name;
state.tabs = state.tabs.filter((entry) => entry.kind !== 'table');
state.activeId = state.tabs[0]?.id;
log('info', `已切换到数据库 ${state.database}`);
await loadWorkspace();
renderEditor();
@@ -732,10 +757,12 @@ async function openTable(database, name) {
// 加载表标签的一页数据。
async function loadTableData(tab) {
if (state.editBusy || state.editNeedsReload || state.editUncertain) return;
const data = tab.data;
data.loading = true;
renderEditor();
try {
if (tab.table !== accountsTable) await ensureEdit(tab.database);
const meta = await api.getTable(tab.database, tab.table);
const filters = Object.entries(data.filters ?? {})
.filter(([, expression]) => String(expression).trim().length > 0)
@@ -756,6 +783,7 @@ async function loadTableData(tab) {
data.offset = offset;
data.pageSize = pageSize;
data.error = null;
data.editId = state.edit?.id;
// 服务端做等值过滤,界面上的 like / 比较表达式再在本地收敛一次
data.rows = filters.length
? page.rows.filter((row) => filters.every((filter) => matchesFilter(row[filter.column], filter.expression)))
@@ -770,6 +798,28 @@ async function loadTableData(tab) {
}
}
// 在首个数据表加载时固定编辑快照。
async function ensureEdit(database) {
if (database !== state.database) throw new Error('请切换到表所属数据库后重新打开。');
if (state.edit) {
if (state.edit.database !== database) throw new Error('请先撤销当前数据库的编辑事务。');
return;
}
if (!state.editStarting) {
const edit = state.editAttempt ?? { id: [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, '0')).join(''), database };
if (edit.database !== database) throw new Error('请先撤销之前的编辑快照,再切换数据库。');
state.editAttempt = edit;
const session = state.session;
state.editStarting = api.beginEdit(edit.id, database).then(() => {
if (state.session !== session) throw new Error('登录会话已更换,请重新加载。');
state.edit = edit;
state.editAttempt = null;
})
.finally(() => { state.editStarting = null; });
}
await state.editStarting;
}
// 重新加载所有表标签。
async function reloadOpenTables() {
for (const tab of state.tabs) {
@@ -790,6 +840,7 @@ function unstageRow(table, pk) {
// 暂存单元格修改。
function stageUpdate(table, database, pk, column, oldValue, newValue) {
if (!canEdit(database)) return;
if (oldValue === newValue) state.changes.updates.delete(changeKey(table, pk, column));
else state.changes.updates.set(changeKey(table, pk, column), { table, database, pk, column, oldValue, newValue });
afterChange();
@@ -797,6 +848,7 @@ function stageUpdate(table, database, pk, column, oldValue, newValue) {
// 暂存新增行。
function stageInsert(table, database, values) {
if (!canEdit(database)) return;
state.insertSeq += 1;
const tempId = `new-${state.insertSeq}`;
state.changes.inserts.set(insertKey(table, tempId), { table, database, tempId, values });
@@ -805,6 +857,7 @@ function stageInsert(table, database, values) {
// 暂存删除行。
function stageDelete(table, database, pk, row) {
if (!canEdit(database)) return;
unstageRow(table, pk);
state.changes.deletes.set(deleteKey(table, pk), { table, database, pk, row });
afterChange();
@@ -819,6 +872,7 @@ function afterChange() {
// 撤下一条暂存变更。
function removeStaged(key) {
if (!canEdit(state.database)) return;
if (state.changes.updates.has(key)) state.changes.updates.delete(key);
else if (state.changes.inserts.has(key)) state.changes.inserts.delete(key);
else if (state.changes.deletes.has(key)) state.changes.deletes.delete(key);
@@ -826,48 +880,70 @@ function removeStaged(key) {
}
// 撤销全部暂存变更。
function rollbackAll() {
state.changes = createEmptyChangeSet();
for (const tab of state.tabs) if (tab.selected) tab.selected.clear();
log('info', '已撤销全部待提交变更。');
async function rollbackAll() {
if (state.editBusy || state.editStarting) return;
state.editBusy = true;
afterChange();
try {
const edit = state.edit ?? state.editAttempt;
const result = edit ? await api.rollbackEdit(edit.id, edit.database) : null;
state.edit = null;
state.editAttempt = null;
state.editNeedsReload = false;
state.editUncertain = null;
state.changes = createEmptyChangeSet();
for (const tab of state.tabs) if (tab.selected) tab.selected.clear();
log('info', result?.state === 'committed' ? '先前提交已成功,现重新加载数据。' : '已回滚编辑事务,重新加载数据。');
state.editBusy = false;
await loadWorkspace();
await reloadOpenTables();
} catch (error) {
state.editNeedsReload = true;
fail(error);
} finally {
state.editBusy = false;
afterChange();
}
}
// 检查编辑事务与页面操作状态。
function canEdit(database) {
return Boolean(state.edit) && state.edit.database === database && !state.editBusy
&& !state.editNeedsReload && !state.editUncertain;
}
// 提交全部暂存变更。
async function commitAll() {
const payload = toCommitPayload(state.changes);
if (state.editBusy || state.editStarting) return;
if (state.editNeedsReload || !state.edit) {
fail(new Error('编辑快照已失效,请撤销并重新加载后编辑。'));
return;
}
const payload = state.editUncertain ?? toCommitPayload(state.changes);
if (!payload.length) {
log('info', '没有待提交的变更。');
return;
}
const database = state.database;
const result = await api.commitChanges(database, payload);
state.editBusy = true;
afterChange();
const edit = state.edit;
const result = await api.commitChanges(edit.id, edit.database, payload);
if (state.edit !== edit) { state.editBusy = false; return; }
if (result.ok) {
state.edit = null;
state.editUncertain = null;
state.changes = createEmptyChangeSet();
for (const tab of state.tabs) if (tab.selected) tab.selected.clear();
log('info', `已提交 ${result.applied} 条变更。`);
setStatus(`已提交 ${result.applied} 条变更`);
} else {
const failed = result.errors?.[0];
// 成功的部分不再保留,失败的那条留在面板上等修正
state.changes = createEmptyChangeSet();
payload.slice(result.applied).forEach((change) => {
if (change.op === 'insert') {
state.insertSeq += 1;
const tempId = `new-${state.insertSeq}`;
state.changes.inserts.set(insertKey(change.table, tempId), { table: change.table, database, tempId, values: change.values });
} else if (change.op === 'delete') {
state.changes.deletes.set(deleteKey(change.table, change.pk), { table: change.table, database, pk: change.pk, row: {} });
} else {
for (const [column, value] of Object.entries(change.set)) {
state.changes.updates.set(changeKey(change.table, change.pk, column), {
table: change.table, database, pk: change.pk, column, oldValue: undefined, newValue: value,
});
}
}
});
log('error', `提交在第 ${(failed?.index ?? 0) + 1} 条失败:${failed?.message ?? '未知错误'}(已成功 ${result.applied} 条)`);
state.editNeedsReload = result.reloadRequired === true;
state.editUncertain = result.state === 'unknown' ? payload : null;
log('error', `${result.message ?? '提交失败'}(事务状态:${result.state})。${state.editNeedsReload
? result.state === 'lost' ? '连接已失效,请重新登录,确认数据后重新编辑。' : '请撤销并重新加载,禁止覆盖其他会话修改。' : state.editUncertain
? '结果尚未确认,编辑已冻结;再次提交仅查询同一提交结果。' : '全部修改已回退,编辑内容保留,可修正后重试。'}`);
}
state.editBusy = false;
await loadWorkspace();
await reloadOpenTables();
afterChange();
@@ -877,10 +953,31 @@ async function commitAll() {
// 运行 SQL 并展示结果。
async function runSql(tab) {
if (state.editBusy || state.editStarting || state.editUncertain || state.editNeedsReload) return;
if (changeCount(state.changes)) {
log('warn', '请先提交或撤销表格修改,再运行 SQL。');
return;
}
const sql = String(tab.sql ?? '').trim();
if (!sql) return;
state.editBusy = true;
afterChange();
const previousEdit = state.edit ?? state.editAttempt;
if (previousEdit) {
try {
await api.rollbackEdit(previousEdit.id, previousEdit.database);
state.edit = null;
state.editAttempt = null;
} catch (error) {
state.editBusy = false;
fail(error);
afterChange();
return;
}
}
setStatus('执行中…');
const result = await api.query(sql, tab.database);
const result = await api.query(sql, tab.database, tab.id);
state.editBusy = false;
tab.result = result;
if (result.type === 'error') {
log('error', result.message ?? '执行失败');
@@ -888,6 +985,7 @@ async function runSql(tab) {
} else {
if (result.database && result.database !== state.database) {
state.database = result.database;
state.tabs = state.tabs.filter((entry) => entry.kind !== 'table');
tab.database = result.database;
await loadWorkspace();
}
@@ -897,8 +995,10 @@ async function runSql(tab) {
} else {
log('info', `语句影响 ${result.affected ?? 0} 行`);
setStatus(`影响 ${result.affected ?? 0} 行`);
await loadWorkspace();
await reloadOpenTables();
if (!result.transactionActive) {
await loadWorkspace();
await reloadOpenTables();
}
}
}
renderEditor();
@@ -912,15 +1012,22 @@ function activateTab(id) {
const tab = activeTab();
if (tab?.kind === 'sql') tab.database = state.database;
if (tab?.kind === 'settings' && !state.serverSettings) void loadServerSettings();
if (tab?.kind === 'table' && !state.edit && tab.table !== accountsTable) void loadTableData(tab);
renderTabs();
}
// 关闭指定标签。
function closeTabById(id) {
if (state.tabs.find((tab) => tab.id === id)?.kind === 'sql') {
void api.rollbackConsole(id).catch(fail);
}
const closed = closeTab(state.tabs, id, state.activeId);
state.tabs = closed.tabs;
state.activeId = closed.activeId;
renderTabs();
if (!state.tabs.some((tab) => tab.kind === 'table') && !changeCount(state.changes) && (state.edit || state.editAttempt)) {
void rollbackAll();
}
}
// 新开一个查询标签。
@@ -1066,6 +1173,10 @@ function deleteSelected(tab) {
// 就地编辑单元格:提交或放弃。
function beginCellEdit(cell, options) {
if (/^runtime\(\[1,\[[567],/.test(String(options.type))) {
setStatus('复合值请通过 SQL 控制台修改');
return;
}
if (cell.querySelector('input')) return;
const input = document.createElement('input');
input.className = 'cell-input';
@@ -1292,6 +1403,7 @@ function onClick(event) {
const target = event.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
if (state.editBusy || state.editStarting) return;
const tab = activeTab();
// 没有选库时,需要当前库的动作先拦下来(服务端也会拒,这里给人话)
@@ -1305,13 +1417,19 @@ function onClick(event) {
void (async () => {
try {
await api.logout();
} catch {
// 退出失败也要回到登录页
} catch (error) {
fail(error);
return;
}
state.session = null;
state.tabs = [];
state.activeId = undefined;
state.changes = createEmptyChangeSet();
state.edit = null;
state.editAttempt = null;
state.editNeedsReload = false;
state.editUncertain = null;
api.forgetEdit();
renderLogin('已退出登录。', 'info');
})();
return;
@@ -1450,7 +1568,7 @@ function onClick(event) {
void commitAll();
return;
case 'rollback':
rollbackAll();
void rollbackAll();
return;
case 'copy-sql': {
const text = changesSql(state.changes).join('\n');
@@ -1493,10 +1611,12 @@ function onClick(event) {
if (!meta || !row) return;
const staged = state.changes.updates.get(changeKey(tab.table, pk, column));
const oldValue = staged ? staged.oldValue : row[column.name];
const editId = state.edit?.id;
beginCellEdit(target, {
type: meta.type,
currentValue: staged ? staged.newValue : row[column.name],
commit: (value) => {
if (state.edit?.id !== editId) return;
if (value === undefined) renderEditor();
else stageUpdate(tab.table, tab.database, pk, column, oldValue, value);
},
@@ -1513,6 +1633,7 @@ function onClick(event) {
type: meta ? meta.type : 'str',
currentValue: insert.values[column],
commit: (value) => {
if (!canEdit(tab.database)) return;
if (value === undefined) renderEditor();
else {
insert.values[column] = value;
@@ -1617,9 +1738,33 @@ window.addEventListener(AUTH_EVENT, () => {
state.session = null;
state.tabs = [];
state.activeId = undefined;
state.changes = createEmptyChangeSet();
state.edit = null;
state.editAttempt = null;
state.editStarting = null;
state.editUncertain = null;
state.editNeedsReload = false;
api.forgetEdit();
renderLogin('会话已过期,请重新登录。', 'warn');
});
// 离开页面时请求回滚仍持有的编辑快照。
window.addEventListener('pagehide', () => {
api.closeConsoleOnPagehide();
const edit = state.edit ?? state.editAttempt;
if (edit && !state.editBusy) {
navigator.sendBeacon('/api/edit/rollback', new Blob([JSON.stringify(edit)], { type: 'application/json' }));
}
});
// 未提交或结果未知时提示页面关闭。
window.addEventListener('beforeunload', (event) => {
if (changeCount(state.changes) || state.editBusy) {
event.preventDefault();
event.returnValue = '';
}
});
root.addEventListener('click', onClick);
root.addEventListener('change', onChange);
root.addEventListener('submit', onSubmit);
+60
View File
@@ -23,6 +23,8 @@ export const accountsTable = '__system_users';
// 值转 SQL 字面量(字符串加引号转义)。
export function sqlLiteral(value) {
if (value === null || value === undefined) return 'NULL';
const runtime = runtimeEnvelope(value);
if (runtime) return runtimeValueText(runtime[1], runtime[2], true);
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
return `'${String(value).replaceAll("'", "''")}'`;
}
@@ -235,6 +237,11 @@ export function isBooleanType(type) {
// 类型的显示名。
export function typeLabel(type) {
if (String(type).startsWith('runtime(') && String(type).endsWith(')')) {
const envelope = JSON.parse(String(type).slice(8, -1));
if (!Array.isArray(envelope) || envelope.length !== 2 || envelope[0] !== 1) throw new Error('Unsupported runtime type encoding');
return runtimeTypeText(envelope[1]);
}
const kind = columnKind(type);
const label = typeLabels[kind];
const args = String(type).includes('(') ? String(type).slice(String(type).indexOf('(')) : '';
@@ -247,6 +254,8 @@ export function typeLabel(type) {
// 单元格显示文本(空值用占位文本)。
export function cellText(value, nullText) {
if (value === null || value === undefined) return nullText;
const runtime = runtimeEnvelope(value);
if (runtime) return runtimeValueText(runtime[1], runtime[2], false);
if (typeof value === 'boolean') return value ? 'true' : 'false';
return String(value);
}
@@ -254,9 +263,60 @@ export function cellText(value, nullText) {
// 编辑框里回填的文本(null 给空串)。
export function editText(value) {
if (value === null || value === undefined) return '';
const runtime = runtimeEnvelope(value);
if (runtime && runtime[1][0] <= 4) return String(runtime[2]);
if (runtime) return runtimeValueText(runtime[1], runtime[2], false);
return String(value);
}
// 读取版本化运行时值封装。
function runtimeEnvelope(value) {
if (!value || typeof value !== 'object' || !Object.hasOwn(value, '__chusql_runtime_v1')) return null;
const envelope = value.__chusql_runtime_v1;
if (!Array.isArray(envelope) || envelope.length !== 3 || envelope[0] !== 1) throw new Error('Unsupported runtime value encoding');
return envelope;
}
// 渲染运行时类型的 SQL 语法。
function runtimeTypeText(type) {
if (!Array.isArray(type) || type.length !== 2 || !Array.isArray(type[1])) throw new Error('Invalid runtime type identity');
const [id, args] = type;
if (!Number.isInteger(id)) throw new Error('Invalid runtime constructor identity');
if (id >= 1 && id <= 4 && args.length === 0) return ['Int', 'Double', 'Bool', 'String'][id - 1];
if (id === 5 && args.length === 1) return `[${runtimeTypeText(args[0])}]`;
if (id === 6 && args.length === 1) return `Maybe ${runtimeTypeText(args[0])}`;
if (id === 7) return `(${args.map(runtimeTypeText).join(', ')})`;
throw new Error('Unknown runtime type identity');
}
// 渲染复合值或对应的 SQL 构造器。
function runtimeValueText(type, value, sql) {
runtimeTypeText(type);
const [id, args] = type;
if (id === 1 || id === 2) {
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('Invalid runtime numeric value');
if (id === 1 && !Number.isInteger(value)) throw new Error('Invalid runtime integer value');
const text = String(value);
return sql && id === 2 && !/[.eE]/.test(text) ? `${text}.0` : text;
}
if (id === 3 && typeof value === 'boolean') return sql ? (value ? 'TRUE' : 'FALSE') : String(value);
if (id === 4 && typeof value === 'string') return sql ? sqlLiteral(value) : JSON.stringify(value);
if (!Array.isArray(value)) throw new Error('Invalid runtime compound value');
if (id === 5) {
const items = value.map((item) => runtimeValueText(args[0], item, sql)).join(', ');
return sql ? `LIST<${runtimeTypeText(args[0])}>(${items})` : `[${items}]`;
}
if (id === 6 && value.length <= 1) {
const item = value.length ? runtimeValueText(args[0], value[0], sql) : '';
return sql ? `MAYBE<${runtimeTypeText(args[0])}>(${item})` : (value.length ? `Just (${item})` : 'Nothing');
}
if (id === 7 && value.length === args.length) {
const items = value.map((item, index) => runtimeValueText(args[index], item, sql)).join(', ');
return sql ? `TUPLE<${args.map(runtimeTypeText).join(', ')}>(${items})` : `(${items})`;
}
throw new Error('Runtime value type mismatch');
}
// 编辑框文本转提交值(空串当 null)。
export function parseEdited(raw, type) {
const text = String(raw ?? '').trim();