From 7901b073b4c138018579c3ae8e286f238e543c7d Mon Sep 17 00:00:00 2001 From: zzxcoding <33575021+zzxCoding@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:47:09 +0800 Subject: [PATCH] =?UTF-8?q?release:=20=E5=87=86=E5=A4=87=200.3.7=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=94=B9=E8=BF=9B=E4=B8=8E=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B1=95=E7=A4=BA=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 30 ++++++ README.en.md | 87 ++++++++++----- README.md | 88 +++++++++------ docs/assets/flydb-workbench.en.png | Bin 0 -> 63666 bytes docs/assets/flydb-workbench.png | Bin 0 -> 57190 bytes docs/design/12-web-workbench.md | 2 +- docs/getting-started/ci-integration.md | 4 +- docs/getting-started/mcp-adapter.md | 2 +- docs/getting-started/web.en.md | 28 ++++- docs/getting-started/web.md | 24 ++++- docs/reference/commands.md | 3 + docs/reference/compatibility.md | 4 +- docs/reference/json-output.md | 4 + docs/reference/web-api.md | 31 +++++- .../src/main/java/com/flydb/cli/FlydbCli.java | 2 +- .../test/java/com/flydb/cli/FlydbCliTest.java | 4 +- .../flydb/core/api/MigrationInfoService.java | 7 +- .../core/api/MigrationInfoServiceTest.java | 23 ++++ .../com/flydb/runtime/state/RunStore.java | 63 ++++++++++- .../com/flydb/runtime/state/RunStoreTest.java | 47 ++++++++ flydb-skills/plugin.json | 2 +- flydb-web/frontend/package-lock.json | 4 +- flydb-web/frontend/package.json | 2 +- flydb-web/frontend/src/App.vue | 96 +++++++++++------ flydb-web/frontend/src/agent-context.ts | 36 +++++++ flydb-web/frontend/src/agent-handoff.test.ts | 45 ++++++++ flydb-web/frontend/src/clean-dialog.test.ts | 54 ++++++++++ .../frontend/src/components/AgentHandoff.vue | 25 +++++ .../frontend/src/components/CleanDialog.vue | 36 +++++++ .../frontend/src/components/Pagination.vue | 14 +++ .../frontend/src/components/PlanPanel.vue | 24 ++++- .../src/components/ProfileSidebar.vue | 102 ++++++++++++++++++ .../frontend/src/components/RunPanel.vue | 32 ++++-- .../frontend/src/components/SqlPreview.vue | 31 ++++++ .../frontend/src/history-pagination.test.ts | 55 ++++++++++ flydb-web/frontend/src/i18n.ts | 48 +++++++-- flydb-web/frontend/src/many-versions.test.ts | 33 ++++++ .../frontend/src/migration-status.test.ts | 41 +++++++ .../frontend/src/plan-performance.test.ts | 35 ++++++ flydb-web/frontend/src/run-loading.test.ts | 42 ++++++++ flydb-web/frontend/src/run-report.test.ts | 48 +++++++++ flydb-web/frontend/src/scroll-lock.test.ts | 11 +- flydb-web/frontend/src/sidebar-groups.test.ts | 89 +++++++++++++++ flydb-web/frontend/src/style.css | 68 +++++++++++- flydb-web/frontend/src/types.ts | 6 +- .../java/com/flydb/web/OperationService.java | 49 ++++++++- .../main/java/com/flydb/web/ProfileStore.java | 59 +++++++++- .../main/java/com/flydb/web/WebServer.java | 8 +- .../java/com/flydb/web/ProfileStoreTest.java | 52 +++++++++ .../java/com/flydb/web/WebServerTest.java | 64 +++++++++++ pom.xml | 2 +- 51 files changed, 1519 insertions(+), 147 deletions(-) create mode 100644 docs/assets/flydb-workbench.en.png create mode 100644 docs/assets/flydb-workbench.png create mode 100644 flydb-web/frontend/src/agent-context.ts create mode 100644 flydb-web/frontend/src/agent-handoff.test.ts create mode 100644 flydb-web/frontend/src/clean-dialog.test.ts create mode 100644 flydb-web/frontend/src/components/AgentHandoff.vue create mode 100644 flydb-web/frontend/src/components/CleanDialog.vue create mode 100644 flydb-web/frontend/src/components/Pagination.vue create mode 100644 flydb-web/frontend/src/components/ProfileSidebar.vue create mode 100644 flydb-web/frontend/src/components/SqlPreview.vue create mode 100644 flydb-web/frontend/src/history-pagination.test.ts create mode 100644 flydb-web/frontend/src/many-versions.test.ts create mode 100644 flydb-web/frontend/src/migration-status.test.ts create mode 100644 flydb-web/frontend/src/plan-performance.test.ts create mode 100644 flydb-web/frontend/src/run-loading.test.ts create mode 100644 flydb-web/frontend/src/run-report.test.ts create mode 100644 flydb-web/frontend/src/sidebar-groups.test.ts create mode 100644 flydb-web/src/test/java/com/flydb/web/ProfileStoreTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d53594e..7d8daa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ Flydb 的重要变更记录在本文件中。版本遵循语义化版本;正 ## [Unreleased] +## [0.3.7] - 2026-09-08 + +本次重点完善本机工作台的日常使用:大变更预览更流畅,多配置更易整理,问题上下文可直接交给 Agent;修复大记录读取与迁移状态显示问题。 + +### 新增 + +- Web 配置名称旁新增“复制给 Agent”:整理脱敏配置、迁移状态与最近执行摘要,标明过期状态、未保存草稿和截断范围;剪贴板不可用时提供手动复制。 +- Web 侧边栏支持分组折叠、新建、重命名、删除、拖拽排序及跨组移动配置,提供无需拖拽的移动入口;分组目录与顺序持久化,删除组只移入未分组。 +- Web 迁移列表、当前项目执行记录和全部执行记录每页 10 条,顶部与底部均支持翻页、直接输入页码,后台刷新保留当前页。 +- Web 高级操作支持 clean:显示生效目标和当前 schema 删除范围,要求风险勾选、输入 `CLEAN` 与最终确认;后端以五分钟单次引用绑定配置和目标,单次解除禁用保护,复用 Core clean 和执行记录,不修改配置文件。 +- 随包提供独立 `flydb-analysis` Skill preview,支持离线材料的方言预检、结构漂移、依赖和引用分析;这是独立技能工作流,不代表迁移内核已交付完整影响分析引擎。 + +### 修复 + +- 修复分组新建、重命名和配置移动弹窗因内容根节点与焦点管理不兼容,导致输入框或选择框无法获得键盘焦点的问题。 +- clean 确认弹窗不再因周期刷新返回内容相同的配置对象而清空输入和风险勾选;仅在配置、目标或凭据实际变化、或主动重新确认时重置。 +- 大迁移预览改用只读虚拟滚动编辑器,支持全文查找、长行横向滚动和完整 SQL 下载;脚本列表分页,原始报告详情按需渲染。Web 轮询使用不含 SQL 正文的摘要,完整记录仅在打开时加载,避免反复传输和渲染大结果。 +- Web 遇到 `MISSING` / `FUTURE` 时直接解释历史与本地脚本不匹配,提供连接配置入口,并明确修复历史、基线及临时测试重新开始的边界。 +- 修复大迁移预览记录写入成功后因超过 8 MiB 而无法读取的问题;Run 采用文件流读取完整记录,配置登记文件继续保留大小限制。 +- 修复 GUI 将 `OUT_OF_ORDER` 误标为“乱序已执行”并计入已执行数量的问题,按状态区分待执行与成功历史;当前版本包含本地缺失的成功历史,不再因 `FUTURE` / `MISSING` 而遗漏。 +- 执行失败面板直接展示脱敏后的具体错误详情,无需展开原始 JSON 才能看到脚本与原因。 +- Web 正确显示无法读取的执行记录:缺失的核验结果显示“未知”,缺失或无效时间的耗时显示“—”,修复中英文界面及导出报告中的 `undefined`、`NaN:NaN`;明确区分本地记录读取失败与数据库执行失败。 + +### 使用与升级 + +- 停止旧的 `flydb web`,解压新版 ZIP,保留原配置和驱动位置后用新版启动。工作台登记兼容既有 profiles.json,新增 groups 保存空分组及顺序;执行记录不迁移或自动删除。 +- 执行记录列表覆盖最近 200 条,分页不代表全量历史查询;预览 SQL 完整保留,Agent 交接内容使用有界摘要。 +- 版本同步为 0.3.7,MCP Adapter 独立版本仍为 0.1.0,CLI JSON 协议不变。README 更新为 GUI、CLI 与 Agent 使用场景,并保留数据库验证矩阵。 +- 本轮 clean 的新增确认入口由隔离测试验证,未在用户数据库执行清理,也不新增厂商数据库认证声明。 + ## [0.3.6] - 2026-09-06 ### 安全修复 diff --git a/README.en.md b/README.en.md index ab78501..e63f3d5 100644 --- a/README.en.md +++ b/README.en.md @@ -1,56 +1,84 @@ English | [中文](./README.md)

- Flydb Data Courier mascot + Flydb Data Courier mascot

# Flydb +**Database migrations you can see. Context your Agent can use.** + [![CI](https://github.com/zzxCoding/Flydb/actions/workflows/ci.yml/badge.svg)](https://github.com/zzxCoding/Flydb/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/zzxCoding/Flydb)](https://github.com/zzxCoding/Flydb/releases/latest) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](./LICENSE) ![Java 8+](https://img.shields.io/badge/Java-8%2B-blue) -[![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003.svg?logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOWE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do) -Flydb is a versioned schema migration tool for databases with JDBC drivers: built-in dialects for mainstream databases, first-class support for Chinese Xinchuang databases, and extension to niche JDBC databases through the `DatabaseType` SPI. +[Website & demo](https://flydb.zzxcoding.dev) · [Download](https://github.com/zzxCoding/Flydb/releases/latest) · [GUI guide](./docs/getting-started/web.en.md) · [Agent setup](./flydb-skills/README.md) · [Documentation](./docs/getting-started/README.md) + +Flydb helps developers and operators manage versioned database changes. Use the local GUI to organize configurations, review SQL and watch execution; use the CLI in scripts and CI; copy context to an Agent when you want help. All three entry points share the same configuration and migration engine, while the GUI and local CLI share execution records. -**Today**, Flydb 0.2 is a reliable migration runtime: commands such as `migrate`, `info`, `validate`, `baseline`, `repair`, `undo`, and `clean`, backed by concurrency locks, transaction semantics, checksum validation, and failure blocking with recovery; eight built-in dialects; and Spring Boot 2/3 starters. **The long-term direction** is a database change capability shared safely by humans and AI agents: agents decide *what* changes; Flydb guarantees *how* it changes safely. See the [roadmap](./ROADMAP.md) (in Chinese) for stage goals and current progress. +MySQL, PostgreSQL, Oracle and Chinese databases including OceanBase, TiDB, DM8, KingbaseES and openGauss. **Run with Java 8+; the GUI needs no Node.js, network access or language model.** -> **Scope boundary:** Flydb manages migration versions, execution safety, and database dialect behavior. It does not translate arbitrary vendor SQL into every database syntax. Keep separate migration directories for database families when their dialects differ; see the [multi-environment guide](./docs/getting-started/multi-environment.md#4-脚本仓库按数据库家族分目录). +![Flydb local workbench with environment groups, migration state and Agent handoff](./docs/assets/flydb-workbench.en.png) +*Actual workbench UI using fictional demonstration configurations and migration records.* -## Why Flydb +## Choose how you work -- **Xinchuang databases as first-class citizens**: DM8 (Dameng), KingbaseES, openGauss, OceanBase, and TiDB ship as built-in dialects alongside MySQL, PostgreSQL, and Oracle. The CLI never bundles vendor drivers; it resolves them from `drivers/`, the runtime classpath, or a Maven repository, which fits drivers that cannot be distributed publicly. -- **Zero-dependency Java 8 core**: `flydb-core` has no third-party runtime dependencies (enforced by Maven Enforcer) and drops into any legacy Java 8 system; Boot 3 / Java 17 environments use a separate starter. -- **Friendly to humans and agents alike**: stable exit and error codes, `--dry-run` previews, non-interactive operation; the distribution ships an Agent Skill and docs matched to the CLI version. -- **Safe defaults**: `clean` is disabled by default and needs a double opt-in; failed migrations block subsequent runs; passwords come from environment variables or password files, never commands, logs, or SQL. +| Your workflow | What Flydb provides | +|---|---| +| Manage development, staging and production configurations | Collapsible groups, drag-and-drop organization, forms and an advanced editor for the original files | +| Review an upgrade before applying it | SQL previews, version search, pagination, full-text search and complete SQL downloads | +| Follow execution and diagnose failures | Script progress, transaction outcomes and post-run verification; unknown outcomes stay explicit | +| Bring in an Agent | “Copy for Agent” packages redacted settings, migration state and recent errors; Skills, JSON and MCP offer further integration | +| Automate migrations in an application or pipeline | CLI for CI, a Java API and Spring Boot 2/3 starters sharing the same engine | -## Quick start +## Open the GUI with one command -Prerequisites: Java 8 or newer, an existing target database, and a Java 8-compatible JDBC driver. +Download the ZIP from [Releases](https://github.com/zzxCoding/Flydb/releases/latest), extract it and run: ```bash -curl -LO https://github.com/zzxCoding/Flydb/releases/download/v0.3.6/flydb-cli-0.3.6.zip -unzip flydb-cli-0.3.6.zip -cd flydb-cli-0.3.6 +cd flydb-cli-0.3.7 +bin/flydb web +``` -# Example: place mysql-connector-j.jar into drivers/ -cp /path/to/mysql-connector-j.jar drivers/ +On Windows, use `bin\flydb.bat web`. Import an existing `flydb.conf` or create a configuration in the browser, then provide the JDBC driver for your database. No account setup; Chinese / English and light / dark themes are included. Starting the workbench does not run migrations. + +Open an existing configuration directly: + +```bash +bin/flydb --config /path/to/project/flydb.conf web +``` + +See the [GUI guide](./docs/getting-started/web.en.md). Vendor JDBC drivers are supplied by the user under their respective licenses, not bundled in the ZIP. + +
+Prefer a terminal? Start with the CLI -bin/flydb init \ - --url 'jdbc:mysql://127.0.0.1:3306/demo' \ - --user flydb_user \ - --database-type mysql \ - --yes +For an existing MySQL database: +```bash +cp /path/to/mysql-connector-j.jar drivers/ +bin/flydb init --url 'jdbc:mysql://127.0.0.1:3306/demo' --user flydb_user --database-type mysql --yes export FLYDB_PASSWORD='replace-me' +bin/flydb validate bin/flydb --dry-run migrate +# Review the SQL and target before applying bin/flydb migrate bin/flydb info -bin/flydb validate ``` -`init` generates `flydb.conf`, `db/migration/V1__init.sql`, and `drivers/README.md`, and never overwrites existing files. Passwords can also be supplied through `flydb.password=${env:DB_PASSWORD}` or `flydb.password.file=/run/secrets/db_password`; a plaintext `flydb.password` is only recommended for local throwaway testing. +`init` generates `flydb.conf`, `db/migration/V1__init.sql` and `drivers/README.md`, refusing to overwrite existing files. The V1 sample contains `SELECT 1;`; replace it with your actual changes. Environment variables and password files are also supported. + +
+ +## One migration engine across every entry point + +- **Review before execution**: checksum validation, concurrency locks, transaction handling and failure blocking. Changes to configuration or scripts after a preview are rechecked. +- **Fit existing environments**: a Java 8 core with zero third-party runtime dependencies, a standalone CLI ZIP, and Spring Boot 2/3 starters. +- **Work across database families**: built-in dialects and driver loading, extensible through `DatabaseType` SPI. Verification levels are documented below. +- **Share facts with your Agent**: structured JSON, Plan Artifacts and execution records. Unknown or interrupted migrations are never automatically replayed. GUI clean requires risk acknowledgment and typing `CLEAN`. + +Flydb manages migration workflow and database dialect behavior. It does not translate arbitrary vendor SQL into other database syntaxes; keep separate migration directories where dialects differ. ## Database support @@ -72,7 +100,8 @@ See the [database getting-started guides](./docs/getting-started/README.md) for - [x] **Reliable migration runtime**: engine, 8 built-in dialects, CLI, Spring Boot starters, Agent Skill, the `v0.2.0` GitHub Release, and the `v0.2.1` Maven Central publishing - [x] **DX and machine contract**: `--json` machine-readable output, protocolVersion contract versioning, CI integration docs, an Agent Plugins 1.0 package (`v0.3.0`; package managers and a Docker image on demand) -- [x] **Agent distribution**: an MCP adapter (TypeScript, nine domain tools, writes unregistered by default) plus the Plan Artifact v1 plan digest; the npm package ships with the next release +- [x] **Agent distribution**: an MCP adapter (TypeScript, nine domain tools, writes unregistered by default) plus the Plan Artifact v1 plan digest; a built adapter ships in the CLI ZIP +- [x] **Local workbench**: multiple configurations, groups, migration previews, execution records and Agent handoff - [ ] **Brownfield change intelligence**: impact analysis, application reference scanning, coverage with explicit unknowns - [ ] **Agent-safe change runtime**: a Plan → Validate → Risk → Approval → Apply → Verify protocol @@ -114,7 +143,7 @@ Plain Java applications depend on `flydb-core`: io.github.zzxcoding flydb-core - 0.3.6 + 0.3.7 ``` @@ -125,13 +154,13 @@ Spring Boot applications pick the matching starter; it runs `migrate` during con io.github.zzxcoding flydb-spring-boot-3-starter - 0.3.6 + 0.3.7 io.github.zzxcoding flydb-spring-boot-2-starter - 0.3.6 + 0.3.7 ``` @@ -175,7 +204,7 @@ The full reactor, including the Boot 3 modules, is built with Java 17; the Boot ./mvnw verify ``` -The CLI distribution is generated at `flydb-cli/target/flydb-cli-0.3.6.zip`. The core module enforces an 80% JaCoCo line-coverage gate and zero non-test runtime dependencies via Maven Enforcer. +The CLI distribution is generated at `flydb-cli/target/flydb-cli-0.3.7.zip`. The core module enforces an 80% JaCoCo line-coverage gate and zero non-test runtime dependencies via Maven Enforcer. Database integration contracts are skipped by default. Set `-Pmysql`/`-Ppostgresql` and `-Dflydb.integration.database=` explicitly to start temporary databases and run the selected tests. The full matrix runs in `.github/workflows/ci.yml`. diff --git a/README.md b/README.md index 6402d3c..5fdcb3c 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,84 @@ [English](./README.en.md) | 中文

- Flydb 数据飞行兽吉祥物 + Flydb 数据飞行兽

# Flydb +**数据库迁移,让人看得清,也让 Agent 接得上。** + [![CI](https://github.com/zzxCoding/Flydb/actions/workflows/ci.yml/badge.svg)](https://github.com/zzxCoding/Flydb/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/zzxCoding/Flydb)](https://github.com/zzxCoding/Flydb/releases/latest) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](./LICENSE) ![Java 8+](https://img.shields.io/badge/Java-8%2B-blue) -[![LINUX DO](https://img.shields.io/badge/LINUX-DO-FFB003.svg?logo=data:image/svg%2bxml;base64,DQo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiPjxwYXRoIGQ9Ik00Ni44Mi0uMDU1aDYuMjVxMjMuOTY5IDIuMDYyIDM4IDIxLjQyNmM1LjI1OCA3LjY3NiA4LjIxNSAxNi4xNTYgOC44NzUgMjUuNDV2Ni4yNXEtMi4wNjQgMjMuOTY4LTIxLjQzIDM4LTExLjUxMiA3Ljg4NS0yNS40NDUgOC44NzRoLTYuMjVxLTIzLjk3LTIuMDY0LTM4LjAwNC0yMS40M1EuOTcxIDY3LjA1Ni0uMDU0IDUzLjE4di02LjQ3M0MxLjM2MiAzMC43ODEgOC41MDMgMTguMTQ4IDIxLjM3IDguODE3IDI5LjA0NyAzLjU2MiAzNy41MjcuNjA0IDQ2LjgyMS0uMDU2IiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZWNlY2VjO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTQ3LjI2NiAyLjk1N3EyMi41My0uNjUgMzcuNzc3IDE1LjczOGE0OS43IDQ5LjcgMCAwIDEgNi44NjcgMTAuMTU3cS00MS45NjQuMjIyLTgzLjkzIDAgOS43NS0xOC42MTYgMzAuMDI0LTI0LjM4N2E2MSA2MSAwIDAgMSA5LjI2Mi0xLjUwOCIgc3R5bGU9InN0cm9rZTpub25lO2ZpbGwtcnVsZTpldmVub2RkO2ZpbGw6IzE5MTkxOTtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik03Ljk4IDcwLjkyNmMyNy45NzctLjAzNSA1NS45NTQgMCA4My45My4xMTNRODMuNDI2IDg3LjQ3MyA2Ni4xMyA5NC4wODZxLTE4LjgxIDYuNTQ0LTM2LjgzMi0xLjg5OC0xNC4yMDMtNy4wOS0yMS4zMTctMjEuMjYyIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOmV2ZW5vZGQ7ZmlsbDojZjlhZjAwO2ZpbGwtb3BhY2l0eToxIi8+PC9zdmc+)](https://linux.do) -Flydb 是面向任意支持 JDBC 驱动的数据库的 Schema 版本化迁移工具:内置主流数据库方言,以国产信创数据库支持为特色,并通过 `DatabaseType` SPI 扩展小众 JDBC 数据库。 +[官网与演示](https://flydb.zzxcoding.dev) · [下载](https://github.com/zzxCoding/Flydb/releases/latest) · [GUI 上手](./docs/getting-started/web.md) · [Agent 接入](./flydb-skills/README.md) · [文档](./docs/getting-started/README.md) + +Flydb 是给开发、运维和实施人员使用的数据库版本迁移工具。用本机 GUI 管理多套配置、核对 SQL 和查看执行进度;用 CLI 接入脚本与 CI;有 Agent 时,一键复制上下文继续处理。三种入口使用同一份配置与迁移引擎,GUI 和同机 CLI 共用执行记录。 -**现在**,Flydb 0.2 是一个可靠的迁移运行时:`migrate`、`info`、`validate`、`baseline`、`repair`、`undo`、`clean` 等命令,配合并发锁、事务语义、checksum 校验、失败阻断与恢复,内置主流与信创共 8 个数据库方言,并提供 Spring Boot 2/3 starter。**长期方向**,是让人类与 AI Agent 共用同一套安全的数据库变更能力:Agent 决定“改什么”,Flydb 保证“怎么改是安全的”。各阶段目标与当前进度见[路线图](./ROADMAP.md)。 +支持 MySQL、PostgreSQL、Oracle 及多种国产数据库;**Java 8+ 即可运行,GUI 无需 Node.js、外网或大模型**。 -> **能力边界:** Flydb 负责迁移版本、执行安全与数据库方言适配,不会把任意一套厂商 SQL 自动转换成所有数据库语法。存在方言差异时,请按数据库家族维护迁移目录,具体组织方式见[多环境自动化指南](./docs/getting-started/multi-environment.md#4-脚本仓库按数据库家族分目录)。 +![Flydb 本机工作台:多环境分组、迁移状态与 Agent 交接](./docs/assets/flydb-workbench.png) +*实际工作台界面,使用虚构的演示配置与迁移记录。* -## 为什么选 Flydb +## 选择适合你的用法 -- **信创数据库一等公民**:达梦 DM8、人大金仓 KingbaseES、openGauss、OceanBase、TiDB 与 MySQL、PostgreSQL、Oracle 同为内置方言;CLI 不捆绑厂商驱动,从 `drivers/`、运行时 classpath 或 Maven 私服外置解析加载,适配不能公开分发的驱动。 -- **零依赖的 Java 8 内核**:`flydb-core` 无任何第三方运行时依赖(由 Maven Enforcer 强制),可直接进入任何存量 Java 8 系统;Boot 3 / Java 17 环境使用独立 starter。 -- **对人和 Agent 同样友好**:稳定退出码与错误码、`--dry-run` 预览、非交互可用;发行包随附与 CLI 版本匹配的 Agent Skill 和文档。 -- **安全默认**:`clean` 默认禁用且需双重开关;失败迁移阻断后续执行;密码支持环境变量与密码文件,不落命令行、日志和 SQL。 +| 使用场景 | Flydb 怎么帮你 | +|---|---| +| 管理多套开发、测试和生产配置 | 按分组折叠、拖拽整理;表单与高级文件编辑直接维护原配置 | +| 实施升级前核对变更 | 先预览 SQL;大量版本可搜索、分页,大 SQL 可全文查找与完整下载 | +| 跟踪执行、排查失败 | 查看脚本进度、事务结果与执行后核验;保留记录,不把未知结果当成成功 | +| 让 Agent 接着处理 | “复制给 Agent”整理脱敏配置、迁移状态和最近错误;也可通过 Skill、JSON 与 MCP 接入 | +| 在应用或流水线里自动迁移 | CLI 适配 CI,Java API 与 Spring Boot 2/3 starter 复用同一迁移引擎 | -## 快速上手 +## 一条命令打开 GUI -前置条件:Java 8 或更高版本、一个已创建的目标数据库,以及与 Java 8 兼容的 JDBC 驱动。 +从 [Releases](https://github.com/zzxCoding/Flydb/releases/latest) 下载 ZIP,解压后运行: ```bash -curl -LO https://github.com/zzxCoding/Flydb/releases/download/v0.3.6/flydb-cli-0.3.6.zip -unzip flydb-cli-0.3.6.zip -cd flydb-cli-0.3.6 +cd flydb-cli-0.3.7 +bin/flydb web +``` -# 示例:把 mysql-connector-j.jar 放入 drivers/ -cp /path/to/mysql-connector-j.jar drivers/ +Windows 使用 `bin\flydb.bat web`。浏览器中导入已有 `flydb.conf`,或新建配置;添加对应 JDBC 驱动后即可连接数据库。无需注册账号,支持中文 / English、明暗主题。启动工作台不会执行迁移。 + +已有配置也可以直接打开: + +```bash +bin/flydb --config /path/to/project/flydb.conf web +``` + +详见 [GUI 指南](./docs/getting-started/web.md)。驱动由使用者按厂商许可提供,不随 ZIP 捆绑。 -bin/flydb init \ - --url 'jdbc:mysql://127.0.0.1:3306/demo' \ - --user flydb_user \ - --database-type mysql \ - --yes +
+更习惯终端?从 CLI 开始 + +以下以已创建的 MySQL 数据库为例: +```bash +cp /path/to/mysql-connector-j.jar drivers/ +bin/flydb init --url 'jdbc:mysql://127.0.0.1:3306/demo' --user flydb_user --database-type mysql --yes export FLYDB_PASSWORD='replace-me' +bin/flydb validate bin/flydb --dry-run migrate +# 核对 SQL 与目标后执行 bin/flydb migrate bin/flydb info -bin/flydb validate ``` -`init` 会生成 `flydb.conf`、`db/migration/V1__init.sql` 和 `drivers/README.md`,并拒绝覆盖已有文件。密码也可通过 `flydb.password=${env:DB_PASSWORD}` 或 `flydb.password.file=/run/secrets/db_password` 提供;明文写入 `flydb.password` 仅建议本地临时测试。 +`init` 生成 `flydb.conf`、`db/migration/V1__init.sql` 和 `drivers/README.md`,拒绝覆盖已有文件。V1 示例为 `SELECT 1;`,请按实际变更替换;配置也支持环境变量与密码文件。 + +
+ +## 迁移能力,贯穿三种入口 -## 本机 GUI +- **先核对,再执行**:checksum 校验、并发锁、事务处理与失败阻断;预览后配置或脚本发生变化时重新核验。 +- **适应现有部署环境**:Java 8 零第三方运行时依赖内核,独立 CLI 发行包,以及 Spring Boot 2/3 starter。 +- **覆盖主流与国产数据库**:内置方言与驱动加载机制,支持 `DatabaseType` SPI 扩展;各数据库的验证层级见下表。 +- **人和 Agent 共用事实**:结构化 JSON、Plan Artifact 与执行记录;未知或中断的迁移不会自动重放。GUI 高级操作中的 clean 需风险确认和输入 `CLEAN`。 -0.3.5 起提供 `bin/flydb web`,支持配置导入与编辑、迁移预览、真实执行进度、CLI 记录共用、 -中英文和明暗主题。只做本机 GUI,不设账户、角色或审批页面。使用方法见 [GUI 指南](docs/getting-started/web.md)。 +Flydb 管理迁移流程与数据库方言行为,不会把任意厂商 SQL 自动翻译成其他数据库语法。存在语法差异时,请按数据库家族维护迁移目录。 ## 数据库支持 @@ -77,7 +100,8 @@ bin/flydb validate - [x] **可靠的迁移运行时**:迁移引擎、8 个内置方言、CLI、Spring Boot starter、Agent Skill、`v0.2.0` GitHub Release、`v0.2.1` Maven Central 发布 - [x] **开发体验与机器契约**:`--json` 机器输出、protocolVersion 契约版本化、CI 接入文档、Agent Plugins 1.0 插件包(`v0.3.0`;包管理器与 Docker 镜像按需启动) -- [x] **Agent 分发**:MCP 适配(TypeScript Adapter + 九个领域工具,写入默认不注册)与 Plan Artifact v1 计划摘要;npm 包随下一个 Release 发布 +- [x] **Agent 分发**:MCP 适配(TypeScript Adapter + 九个领域工具,写入默认不注册)与 Plan Artifact v1 计划摘要;CLI ZIP 内附已构建 Adapter +- [x] **本机图形工作台**:多配置、分组、迁移预览、执行记录与 Agent 上下文交接 - [ ] **存量变更智能**:影响分析、应用引用扫描、覆盖率与未知项标注 - [ ] **Agent 安全变更运行时**:Plan → Validate → Risk → Approval → Apply → Verify 协议 @@ -119,7 +143,7 @@ flydb.migrate(); io.github.zzxcoding flydb-core - 0.3.6 + 0.3.7 ``` @@ -130,13 +154,13 @@ Spring Boot 应用选择对应 starter,容器初始化期间执行 `migrate` io.github.zzxcoding flydb-spring-boot-3-starter - 0.3.6 + 0.3.7 io.github.zzxcoding flydb-spring-boot-2-starter - 0.3.6 + 0.3.7 ``` @@ -180,7 +204,7 @@ bin/flydb clean --clean-disabled=false --force # clean 默认禁用;非交 ./mvnw verify ``` -CLI 构建产物位于 `flydb-cli/target/flydb-cli-0.3.6.zip`。core 的 JaCoCo 行覆盖率门禁为 80%,并由 Maven Enforcer 保证零非测试运行时依赖。 +CLI 构建产物位于 `flydb-cli/target/flydb-cli-0.3.7.zip`。core 的 JaCoCo 行覆盖率门禁为 80%,并由 Maven Enforcer 保证零非测试运行时依赖。 数据库集成契约默认跳过;显式设置 `-Pmysql`/`-Ppostgresql` 与 `-Dflydb.integration.database=` 后,才会启动临时数据库执行对应测试。完整矩阵由 `.github/workflows/ci.yml` 执行。 diff --git a/docs/assets/flydb-workbench.en.png b/docs/assets/flydb-workbench.en.png new file mode 100644 index 0000000000000000000000000000000000000000..c929ad02b6759ec0f3977882e210cb9a0d96b24f GIT binary patch literal 63666 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

_34Yz-(WvE!65Ut-njp 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 literal 0 HcmV?d00001 diff --git a/docs/assets/flydb-workbench.png b/docs/assets/flydb-workbench.png new file mode 100644 index 0000000000000000000000000000000000000000..6445a7c76893a50ad4a781d73b90c1d9c8f4b088 GIT binary patch literal 57190 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 literal 0 HcmV?d00001 diff --git a/docs/design/12-web-workbench.md b/docs/design/12-web-workbench.md index 64ebc6d..3627612 100644 --- a/docs/design/12-web-workbench.md +++ b/docs/design/12-web-workbench.md @@ -59,7 +59,7 @@ Web 复用相同迁移运行时。通用 SQL IDE、数据库对象编辑器、 | G11 | 普通 CLI / Agent 调用在同一台机器的同一状态目录登记执行;GUI 可以在执行期间及结束后查看;CLI 不要求先启动 Web | | G12 | 结束后核验 info / validate,分别展示执行和核验结果;失败给出错误码、原始诊断、确认/回滚/未知状态及后续建议;不自动重放或 repair | | G13 | 本地执行历史、事件恢复、详情、脱敏报告导出和诊断复制;重启时没有可信终态的任务显示结果待核验 | -| G14 | 存量库 baseline 有解释、版本输入与独立确认;repair、undo 提供独立的高级处置流程和明确影响提示;clean 无 Web 写入入口 | +| G14 | 存量库 baseline 有解释、版本输入与独立确认;repair、undo 提供独立的高级处置流程和明确影响提示;clean 作为危险高级操作,必须展示目标与当前 schema 删除范围、风险勾选、输入 CLEAN,并使用绑定生效配置的短期单次确认引用(2026-09-07 按用户确认扩展原先无 Web clean 入口的边界) | | G15 | 识别和选择已有 JDBC 驱动,显示实际驱动来源与失败建议,支持离线目录/企业私服;不捆绑或主动下载厂商驱动 | | G16 | 中英文完整切换,记住偏好;所有界面、表单错误、应用提示、状态说明及导出报告可切换;SQL、路径、原始厂商错误保留原文 | | G17 | 明暗主题、键盘操作、焦点、对比度、减少动态效果、单配置和多配置、窄屏布局;美观和内容密度有实际截图证据 | diff --git a/docs/getting-started/ci-integration.md b/docs/getting-started/ci-integration.md index 03994d6..da59657 100644 --- a/docs/getting-started/ci-integration.md +++ b/docs/getting-started/ci-integration.md @@ -22,7 +22,7 @@ jobs: migrate: runs-on: ubuntu-latest env: - FLYDB_HOME: ${{ runner.temp }}/flydb-cli-0.3.6 # ZIP 内的版本化根目录 + FLYDB_HOME: ${{ runner.temp }}/flydb-cli-0.3.7 # ZIP 内的版本化根目录 CONF: deploy/flydb.mysql.uat.conf steps: - uses: actions/checkout@v4 @@ -30,7 +30,7 @@ jobs: - name: 安装 Flydb CLI(版本随仓库锁定) run: | curl -fsSL -o flydb.zip \ - "https://github.com/zzxCoding/Flydb/releases/download/v0.3.6/flydb-cli-0.3.6.zip" + "https://github.com/zzxCoding/Flydb/releases/download/v0.3.7/flydb-cli-0.3.7.zip" unzip -q flydb.zip -d "$RUNNER_TEMP" "$FLYDB_HOME/bin/flydb" version diff --git a/docs/getting-started/mcp-adapter.md b/docs/getting-started/mcp-adapter.md index 81d36dd..8e21276 100644 --- a/docs/getting-started/mcp-adapter.md +++ b/docs/getting-started/mcp-adapter.md @@ -52,7 +52,7 @@ npx flydb-mcp # 一次性启动(发布后可用;registry 名称以发 "command": "npx", "args": ["flydb-mcp"], "env": { - "FLYDB_CLI": "/opt/flydb-cli-0.3.6/bin/flydb", + "FLYDB_CLI": "/opt/flydb-cli-0.3.7/bin/flydb", "FLYDB_PASSWORD": "由宿主 secret 注入" } } diff --git a/docs/getting-started/web.en.md b/docs/getting-started/web.en.md index 78e3c25..4aa50cd 100644 --- a/docs/getting-started/web.en.md +++ b/docs/getting-started/web.en.md @@ -97,7 +97,13 @@ inspect the database and history before deciding what to do. No write is automat Advanced actions separately explain and confirm baseline, undo and repair. Undo previews the matched `U__` script. Baseline records an existing version; repair changes history and does not reverse SQL. -There is no Web `clean` action. +“Clean database” removes tables, views, sequences, and Flydb history/lock tables in the connection’s current +schema according to its dialect, including objects not managed by Flydb. Check the displayed target, account +and backups, acknowledge the warning, type `CLEAN`, then select “Confirm clean”. Confirmation is bound to +effective settings, expires after five minutes, and is single-use. The confirmation page does not connect or +enumerate objects; the connection determines the actual schema. Local configuration and SQL files are retained. +Clean is enabled for this invocation only; `flydb.conf` is not changed. Failure may leave partial deletions; +inspect the database before deciding what to do next. ## CLI and Agent interoperability @@ -120,15 +126,35 @@ diagnostics. Passwords, secret URL parameters and configuration values named wit credential are redacted. Unmarked sensitive business literals cannot be exhaustively inferred; review reports according to your project's data policy before sharing. The list shows the most recent 200 records; older files remain on disk without automatic deletion. +Project and all-run histories show 10 entries per page, with navigation and direct page entry above and below the list. Background refresh preserves the page; switching projects resets project history to page one. ## Preferences and troubleshooting +“Copy for Agent” appears beside the configuration name across all three tabs, or below it on narrow screens. +It copies file and working-directory paths, redacted connection settings, the latest inspection statistics and up to +50 scripts, plus 10 recent execution summaries and errors. Stale state, excluded unsaved drafts and truncation are explicit. +It does not read raw configuration files or include temporary passwords or SQL bodies. Nothing is automatically sent to an Agent. +Add your specific request when pasting; the Agent needs access to the local paths and must verify current state first. +Copying context does not authorize database writes. A manual-copy dialog is available when clipboard access fails. + +Create empty groups in the sidebar; click a group name to collapse it. Its menu supports rename, +move up/down and delete. Drag a group before another group to reorder, or onto Ungrouped to move it last. +Drag configurations into groups, or use the folder button beside a configuration to choose its group. +Deleting a group moves its configurations to Ungrouped and preserves files and database data. +Groups and order persist in the local workbench across restarts. Collapse preferences stay in the browser; +search temporarily expands matching groups. + Use the bottom-left controls for Chinese/English and dark/light themes. Preferences persist in the browser. Narrow screens use the top-left configuration menu; SQL scrolls within its preview. +Large previews use a read-only editor that renders the visible region. Use `Ctrl/⌘ F` to search the full +SQL and scroll horizontally for long lines. “Download full SQL” exports every preview statement in the +selected script. Migration lists show 10 entries per page with controls above and below the list; preview directories show 50, with full-set version/script +search and direct page-number entry. Filtering and pagination never change totals or the execution set. - Driver failure: check the JAR, Java version, class, coordinate and offline setting. - Connection failure: check the effective URL, credentials and network, then inspect again. - Validation or SQL failure: read the original details and [error reference](../reference/errors.md). +- Unreadable execution record (`UNREADABLE_RECORD`): the local record could not be read; this does not mean the database is empty or the migration failed. Preserve `runs//summary.json` and `events.jsonl` in the state directory, check file size, integrity and access permissions, and verify database state before deciding what to do next. - Unavailable page: keep the local process running, check the port, then refresh or select Retry. - Configuration conflict: merge external changes before saving; the original file was not overwritten. diff --git a/docs/getting-started/web.md b/docs/getting-started/web.md index e98626d..5ca58eb 100644 --- a/docs/getting-started/web.md +++ b/docs/getting-started/web.md @@ -101,7 +101,13 @@ Windows 使用 `bin\flydb.bat web`。只需要 Java 8+ 和浏览器;发行包 先检查目标库和迁移历史,再决定处置。没有自动重放、自动 repair 或模拟进度。 “高级操作”中的基线、撤销、修复分别说明影响并确认目标。撤销先预览匹配的 `U__` 脚本; -基线只登记存量版本;修复历史不撤销已执行 SQL。Web 不提供 `clean`。 +基线只登记存量版本;修复历史不撤销已执行 SQL。 + +“清空数据库(clean)”会按方言删除当前连接 schema 的表、视图、序列及 Flydb 历史/锁表, +不限于 Flydb 管理的对象,不是只删除迁移记录。先核对弹窗中的目标连接、账号和备份, +勾选风险确认并输入 `CLEAN`,再点击“确认清空”。此确认五分钟内单次有效;配置变化后必须重新确认。 +确认页面不连接数据库或枚举对象,实际 schema 由连接决定。本地配置和 SQL 文件会保留。 +只对本次操作解除 clean 禁用保护,不改写 `flydb.conf`;失败也可能已删除部分对象,核对现场后再决定后续操作。 ## 与 CLI / Agent 共用 @@ -121,15 +127,31 @@ CLI 的 `--json` stdout 仍为既有机器契约的一行 JSON。 导出和复制采用当前界面语言,保留 SQL、路径和厂商错误原文。密码、URL 秘密参数及名称含 password/secret/token/credential 的配置值会脱敏;业务 SQL 中未标识的敏感常量不可能自动穷尽识别, 分享报告前按项目数据要求检查。记录保留在本地;当前列表显示最近 200 条,旧记录文件不会自动删除。 +当前项目和全部执行记录每页 10 条,顶部和底部都可翻页或直接输入页码。后台刷新保留当前页,切换项目后项目记录回到第一页。 ## 语言、主题与问题定位 +配置名称右侧的“复制给 Agent”在三个页签均可使用,窄屏位于名称下方。复制内容包含原配置路径、 +工作目录、脱敏连接信息、最近一次检查的迁移统计与最多 50 条脚本、最近 10 条执行摘要及错误。 +内容标明状态过期、未保存草稿未包含、记录或错误截断等情况,不读取原文件正文、不包含临时密码或 SQL 正文。 +复制只写入本机剪贴板,不会自动发送给 Agent;粘贴时可补充具体诉求。Agent 需能访问这些本地路径, +并先核验最新现场,复制上下文本身不授权数据库写入。浏览器禁止自动复制时可在弹窗中手动复制。 + +侧边栏可新建空分组,点击组名折叠或展开,组旁菜单可重命名、上移、下移或删除。 +拖动组名到另一组前可排序,拖到“未分组”可排在末尾;拖动配置到目标组可移动配置, +也可用配置旁的文件夹按钮选择分组。删除分组仅将配置移到“未分组”,保留配置文件及数据库数据。 +分组与顺序随工作台保存在本机,重启后保留;折叠状态在当前浏览器记忆。搜索会临时展开匹配组。 + 左下角可随时切换中文/English 和明暗主题,偏好保存在浏览器。切换语言不会清空当前草稿。 窄屏从左上角菜单选择配置,长 SQL 在预览区域内滚动。 +大预览使用只读编辑器,仅渲染可见区域;`Ctrl/⌘ F` 查找完整 SQL,长行可横向滚动。 +“下载完整 SQL”导出当前脚本的全部预览语句;迁移列表每页 10 项,预览目录每页 50 项,支持搜索全部版本、脚本和直接输入页码。迁移列表顶部和底部均有分页入口。 +筛选与分页只改变显示范围,总数统计与确认执行仍使用完整集合。 - 驱动不可用:核对现有 JAR、Java 版本、驱动类名、坐标和离线配置。 - 连接失败:核对实际 JDBC URL、数据库凭据与网络;保留输入后修正再检查。 - 校验或迁移失败:打开原始详情,按[错误码参考](../reference/errors.md)定位,不自动修复历史。 +- 执行记录无法读取(`UNREADABLE_RECORD`):表示本地记录读取失败,不代表数据库为空或迁移失败。保留状态目录中的 `runs/<报告 id>/summary.json` 与 `events.jsonl`,排查文件大小、完整性和访问权限;核对数据库状态后再决定后续操作。 - 页面未连接:保持终端进程运行,确认端口后刷新或点击“重试”;换端口时使用新的本机地址。 - 配置冲突:先合并文件变化再保存;原文件未被覆盖。 diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 7d6471f..d3ab48b 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -158,6 +158,9 @@ MySQL/Oracle 家族虽然不支持 DDL 事务,但一份脚本若解析后的 `clean` 默认禁用。非交互环境必须同时设置 `--clean-disabled=false` 和 `--force`: +本机 Web 工作台在“高级操作 → 清空数据库”提供独立警示和确认流程,单次授权不会改写此默认配置, +见 [GUI 指南](../getting-started/web.md)。 + ```bash bin/flydb clean --clean-disabled=false --force ``` diff --git a/docs/reference/compatibility.md b/docs/reference/compatibility.md index 04aeb66..3726ada 100644 --- a/docs/reference/compatibility.md +++ b/docs/reference/compatibility.md @@ -9,8 +9,8 @@ Maven 坐标 groupId 统一为 `io.github.zzxcoding`(Central Portal 经 GitHub | 模块 | 坐标 | Java 运行时 | 依赖框架 | 发布到 Central | |---|---|---:|---|---| | Core(Java API) | `io.github.zzxcoding:flydb-core` | 8+ | 无(零第三方运行时依赖) | 是 | -| Runtime(待发布) | `io.github.zzxcoding:flydb-runtime` | 8+ | Core、Jackson 2 | 用户验收后随下一版发布 | -| Web(待发布) | `io.github.zzxcoding:flydb-web` | 8+ | Runtime、JDK HttpServer、内嵌静态界面 | 用户验收后随下一版发布 | +| Runtime | `io.github.zzxcoding:flydb-runtime` | 8+ | Core、Jackson 2 | 是(0.3.5 起) | +| Web | `io.github.zzxcoding:flydb-web` | 8+ | Runtime、JDK HttpServer、内嵌静态界面 | 是(0.3.5 起) | | CLI | `io.github.zzxcoding:flydb-cli` | 8+ | picocli | 是(jar;发行 ZIP 经 GitHub Release 分发) | | Spring Boot 2 Starter | `io.github.zzxcoding:flydb-spring-boot-2-starter` | 8+ | Spring Boot 2.7.x(以 2.7.18 验证) | 是 | | Spring Boot 3 Starter | `io.github.zzxcoding:flydb-spring-boot-3-starter` | 17+ | Spring Boot 3.x(以 3.5.16 验证) | 是 | diff --git a/docs/reference/json-output.md b/docs/reference/json-output.md index 315e4bd..5de9125 100644 --- a/docs/reference/json-output.md +++ b/docs/reference/json-output.md @@ -55,6 +55,10 @@ $ bin/flydb --json migrate 取值约定:状态 token 为 `PENDING`、`OUT_OF_ORDER`、`SUCCESS`、`FAILED`、`MISSING`、`OUTDATED`、`FUTURE`、`BASELINE`、`UNDONE`;类型 token 为 `SQL`、`JDBC`、`BASELINE`、`UNDO_SQL`;`installedOn` 为 ISO-8601 本地时间;可重复迁移的 `version` 为 `null`;未知或不适用的数值为 `null`。 +`info.current` 为仍有效的成功历史记录中的最高版本,包含本地脚本缺失时的 `MISSING` / `FUTURE`, +不计失败或已撤销的记录。`OUT_OF_ORDER` 表示本地存在、数据库尚未应用的低版本脚本,属于待执行, +不能将它显示为“乱序已执行”。校验是否允许执行仍由 Core 决定,不由统计数量决定。 + ## 稳定性承诺 同一 `protocolVersion` 内只新增字段、不改名、不删除、不改类型或语义;**消费者必须忽略未知字段**。破坏性变更会递增 `protocolVersion` 并在 CHANGELOG 说明。字段顺序固定但消费者不得依赖。 diff --git a/docs/reference/web-api.md b/docs/reference/web-api.md index 9da277a..234925f 100644 --- a/docs/reference/web-api.md +++ b/docs/reference/web-api.md @@ -22,6 +22,7 @@ POST 操作提交成功返回 HTTP 200 和 Run 快照,不代表数据库执行 | `POST /api/profiles` | mode 为 import/create/discover/duplicate;返回登记数组 | | `PUT /api/profiles/:id` | name/group/environment/workingDirectory,可选 driversDirectory | | `DELETE /api/profiles/:id` | 只移除登记 | +| `POST /api/groups` | 分组组织操作:create/rename/delete/move/moveProfile,见下文;只修改本机登记 | | `GET /api/profiles/:id/config` | revision/values/effective/sources/secretKeys,可含配置 error | | `PUT /api/profiles/:id/config` | `{revision, values:{key:string或null}}` 或 `{revision, content}`;整文件保存强制校验,返回脱敏快照 | | `POST /api/profiles/:id/actions` | 提交预定义操作,返回 Run | @@ -32,6 +33,14 @@ POST 操作提交成功返回 HTTP 200 和 Run 快照,不代表数据库执行 discover 使用 `paths` 数组;duplicate 使用 `sourceId/configPath`。显示元数据是可选的, 同一真实文件路径去重。配置内容保存到原文件,登记只保存路径和显示信息。 +bootstrap 的 `groups` 为有序分组名称数组,包括空分组;旧登记的 profile.group 自动纳入列表。 +`POST /groups` 输入 `action`:create 使用 `newName`;rename 使用 `name/newName`;delete 使用 `name`; +move 使用 `name/before`(空 before 表示末尾);moveProfile 使用 `profileId/name`(空 name 表示未分组)。 +新增名称去除首尾空白,最多 80 字符,不允许空白或重名。重名返回 GROUP_EXISTS,分组不存在返回 GROUP_NOT_FOUND。 +删除分组将其配置移到未分组;重命名同步修改组内配置显示元数据。所有操作在 profiles.lock 下原子写入 +profiles.json 的 profiles/groups,不修改原配置、SQL 或数据库。未分组是空字符串代表的内置归属,不可删除或重命名。 +折叠状态按状态目录存于当前浏览器,搜索临时展开匹配分组,不覆盖折叠偏好。 + 文档转换的 `validate:false` 仅用于模式切换时保留待修正的字段,不绕过创建/保存时的校验。 新建草稿与 CLI 共用 `InitScaffolder.configurationDraft`;确认创建时完整原文交给同一个 `InitScaffolder.create`,使用 CREATE_NEW 生成 `flydb.conf`、`db/migration/V1__init.sql` 和缺失的 @@ -43,10 +52,19 @@ HTTP 409 / FLYDB-4004,已有驱动说明保留。默认迁移路径为绝对 错误包含所在逻辑行的起始行号。整文件保存仍校验 revision、使用文件锁及原子替换。 操作正文公共字段为 `command`、`revision`、`requestId`(调用方生成的 UUID),可选临时 `password`。 -command 只允许 inspect、validate、plan、migrate、undo-plan、undo、baseline、repair。 +command 只允许 inspect、validate、plan、migrate、undo-plan、undo、baseline、repair、clean。 baseline 增加 `baselineVersion` 和 `confirmed:true`;repair 增加 `confirmed:true`。 migrate/undo 必须使用对应 plan/undo-plan 结果中的 `planId`。 +`clean` 先 POST `/api/profiles//clean-confirmation`,携带当前 `revision` 和可选临时 `password`。 +该步骤只读取生效配置,不连接数据库或枚举对象;返回脱敏 `target`、`user`、`scope=CURRENT_SCHEMA`、 +`token` 与 `expiresInSeconds=300`。界面必须说明将按方言清理当前 schema 的表、视图、序列及历史/锁表, +不限于 Flydb 创建的对象,且失败可能留下部分删除结果。用户勾选风险并输入 `CLEAN` 后, +通过 actions 提交 `command=clean`、`cleanToken`、`confirmed:true`、`confirmationText:"CLEAN"`。 +后端校验确认引用与配置、目标绑定,引用单次有效,重启或五分钟后失效;旧迁移预览也会失效。 +仅对本次调用设置 `flydb.clean-disabled=false`,不改写配置文件;底层仍复用 Core clean、锁与执行记录。 +同一 requestId 重试返回原记录,不重复执行。失败或结果未知时必须先核对现场,不能自动重放。 + planId 是本服务内存中的一次确认引用,30 分钟后或重启后失效,不是 Plan Artifact 的内容摘要。 同一请求标识会返回已有记录,不重放。最近 1,000 条持久化记录用于重启后的去重查找, 浏览器每次新意图使用新 UUID;过期请求不能当作永久幂等 API。 @@ -72,8 +90,16 @@ SQL/Java 回调不在迁移预览清单内,且沿用原生命周期。 ## Run 与事件 +Web 使用 `GET /api/bootstrap?compact=true` 轮询:省略记录中的 SQL `statements` 数组, +并以 `detailsOmitted=true` 标记。不带参数的 bootstrap 保持完整响应。 +打开预览或报告后通过 `GET /api/runs/` 获取完整记录;详情未加载成功前不允许确认执行或导出不完整报告。 +终态摘要按文件身份、大小及修改时间缓存,运行中记录继续检查文件锁。摘要不改变原始记录、计划 ID 或执行集合。 + 状态目录布局:`profiles.json`、`profiles.lock`、`runs/<时间戳-UUID>/`。 每次 Run 包含 `summary.json`、`events.jsonl`、`run.lock`。 +`summary.json` 包含完整预览结果,读取时不采用配置登记文件的 8 MiB 限制; +采用文件流解析并保留 JSON 结构约束,不截断 SQL,也不修改旧记录。结果仍需在内存中解析, +该修复不表示无限大小的预览;配置登记文件继续保留 8 MiB 限制。 进程持有 OS 文件锁表示执行仍在运行,心跳超时不能推断它失败。 快照字段包括 schemaVersion=1、id、source=CLI/WEB、command、configPath、configRevision、 @@ -82,6 +108,9 @@ Web 还记录 profileId/profileName/requestId;执行中按实际事件更新 p 状态为 RUNNING、SUCCEEDED、FAILED、UNKNOWN;进程消失且没有终态时,读取返回 UNKNOWN 和 recovery=NO_TERMINAL_RESULT,不改写原证据。记录无法读取为 UNREADABLE_RECORD。 +列表中的不可读记录仅保证包含 id、status=UNKNOWN 和 recovery=UNREADABLE_RECORD; +调用方不得假定命令、目标、时间或核验结果存在。Web 对缺失核验结果显示“未知”, +对无法计算的耗时显示“—”,不会将缺失值解释为成功、失败或未执行核验。 INTERRUPTED 是前端可识别的保留状态,当前无法确认数据库结果的进程中断采用 UNKNOWN。 verification 独立为 NOT_RUN、PASSED、FAILED;CLI 不隐式增加执行后数据库命令,因此默认为 NOT_RUN。 diff --git a/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java b/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java index 5454b4c..af82927 100644 --- a/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java +++ b/flydb-cli/src/main/java/com/flydb/cli/FlydbCli.java @@ -750,7 +750,7 @@ private static void put(Map values, String key, Object value) { private static String version() { Package pkg = FlydbCli.class.getPackage(); String implementation = pkg == null ? null : pkg.getImplementationVersion(); - return implementation != null ? implementation : "0.3.6"; + return implementation != null ? implementation : "0.3.7"; } private static Path detectInstallDirectory() { diff --git a/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java b/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java index 3acbd9f..afa9487 100644 --- a/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java +++ b/flydb-cli/src/test/java/com/flydb/cli/FlydbCliTest.java @@ -44,7 +44,7 @@ void versionCommandReturnsSuccess() { int exitCode = cli.execute("version"); assertThat(exitCode).isZero(); - assertThat(standardOutput.toString()).contains("flydb 0.3.6"); + assertThat(standardOutput.toString()).contains("flydb 0.3.7"); assertThat(errorOutput.toString()).isEmpty(); } @@ -63,7 +63,7 @@ void jsonVersionEmitsSingleLineEnvelope() { assertThat(exitCode).isZero(); assertThat(standardOutput.toString()) .isEqualTo("{\"protocolVersion\":1,\"command\":\"version\"," - + "\"status\":\"success\",\"exitCode\":0,\"version\":\"0.3.6\"}\n"); + + "\"status\":\"success\",\"exitCode\":0,\"version\":\"0.3.7\"}\n"); assertThat(errorOutput.toString()).isEmpty(); } diff --git a/flydb-core/src/main/java/com/flydb/core/api/MigrationInfoService.java b/flydb-core/src/main/java/com/flydb/core/api/MigrationInfoService.java index 10b4eaa..00f1441 100644 --- a/flydb-core/src/main/java/com/flydb/core/api/MigrationInfoService.java +++ b/flydb-core/src/main/java/com/flydb/core/api/MigrationInfoService.java @@ -8,6 +8,7 @@ import com.flydb.core.migration.AppliedMigration; import com.flydb.core.migration.MigrationInfo; import com.flydb.core.migration.MigrationState; +import com.flydb.core.migration.MigrationType; import com.flydb.core.migration.MigrationVersion; import com.flydb.core.migration.ResolvedMigration; @@ -53,11 +54,11 @@ public List applied() { public MigrationVersion current() { MigrationVersion current = null; for (MigrationInfo info : all) { - if (info.state() != MigrationState.SUCCESS && info.state() != MigrationState.BASELINE) { + AppliedMigration applied = info.applied(); + if (applied == null || !applied.success() || applied.type() == MigrationType.UNDO_SQL) { continue; } - AppliedMigration applied = info.applied(); - MigrationVersion version = applied == null ? null : applied.version(); + MigrationVersion version = applied.version(); if (version != null && (current == null || version.compareTo(current) > 0)) { current = version; } diff --git a/flydb-core/src/test/java/com/flydb/core/api/MigrationInfoServiceTest.java b/flydb-core/src/test/java/com/flydb/core/api/MigrationInfoServiceTest.java index bfa4e4c..fc8a31c 100644 --- a/flydb-core/src/test/java/com/flydb/core/api/MigrationInfoServiceTest.java +++ b/flydb-core/src/test/java/com/flydb/core/api/MigrationInfoServiceTest.java @@ -17,6 +17,29 @@ @DisplayName("MigrationInfoService") class MigrationInfoServiceTest { + @Test void currentIncludesSuccessfulHistoryMissingFromTheLocalCheckout() { + MigrationInfo future = MigrationInfo.derive(null, applied("20260901.3"), version("20260901.3"), version("20260227.3")); + MigrationInfo older = MigrationInfo.derive(resolved("20260101.1"), null, version("20260901.3"), version("20260227.3")); + MigrationInfoService service = new MigrationInfoService(Arrays.asList(older, future)); + assertThat(service.current()).isEqualTo(version("20260901.3")); + assertThat(service.pending()).containsExactly(older); + MigrationInfo missing = MigrationInfo.derive(null, applied("2"), version("2"), version("3")); + assertThat(new MigrationInfoService(Arrays.asList(missing)).current()).isEqualTo(version("2")); + } + + @Test void currentExcludesFailedAndUndoneHistory() { + AppliedMigration failed = AppliedMigration.of(2, version("4"), "failed", MigrationType.SQL, + "V4__failed.sql", 1, "u", new Timestamp(0), 1, false); + AppliedMigration undone = AppliedMigration.of(3, version("5"), "undone", MigrationType.UNDO_SQL, + "U5__undone.sql", 1, "u", new Timestamp(0), 1, true); + MigrationInfoService service = new MigrationInfoService(Arrays.asList( + MigrationInfo.derive(null, applied("2"), version("2"), version("1")), + MigrationInfo.derive(null, failed, version("2"), version("1")), + MigrationInfo.derive(null, undone, version("2"), version("1")))); + assertThat(service.current()).isEqualTo(version("2")); + } + + @Test @DisplayName("提供 pending/applied/current 只读视图") void exposesFilteredViewsAndCurrentVersion() { diff --git a/flydb-runtime/src/main/java/com/flydb/runtime/state/RunStore.java b/flydb-runtime/src/main/java/com/flydb/runtime/state/RunStore.java index 06c5719..a237427 100644 --- a/flydb-runtime/src/main/java/com/flydb/runtime/state/RunStore.java +++ b/flydb-runtime/src/main/java/com/flydb/runtime/state/RunStore.java @@ -5,6 +5,10 @@ import java.nio.channels.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.filter.FilteringParserDelegate; +import com.fasterxml.jackson.core.filter.TokenFilter; import java.time.Instant; import java.util.*; import com.fasterxml.jackson.databind.JsonNode; @@ -15,6 +19,9 @@ /** Cross-process local execution journal. A held OS lock, not a heartbeat, proves liveness. */ public final class RunStore { private final Path directory; + private final Map summaries = new LinkedHashMap(16, .75f, true) { + @Override protected boolean removeEldestEntry(Map.Entry entry) { return size() > 200; } + }; public RunStore(Path stateDirectory) throws IOException { this.directory = stateDirectory.resolve("runs"); Files.createDirectories(directory); @@ -31,7 +38,15 @@ public Writer start(ObjectNode metadata, String... secrets) throws IOException { } public ObjectNode read(String id) throws IOException { Path run = path(id); - ObjectNode summary = StateJson.read(run.resolve("summary.json")); + // Run results contain full migration previews, so they cannot share the + // small configuration-file size limit. Stream the file to avoid a second + // full byte-array copy; keep the mapper's JSON structure constraints. + ObjectNode summary; + try (InputStream input = Files.newInputStream(run.resolve("summary.json"))) { + JsonNode node = StateJson.MAPPER.readTree(input); + if (node == null || !node.isObject()) throw new IOException("Expected JSON object"); + summary = (ObjectNode) node; + } if ("RUNNING".equals(summary.path("status").asText()) && !isLive(run)) { summary.put("status", "UNKNOWN"); summary.put("recovery", "NO_TERMINAL_RESULT"); @@ -39,6 +54,13 @@ public ObjectNode read(String id) throws IOException { return summary; } public List list(int limit) throws IOException { + return list(limit, false); + } + /** SQL-free polling view. Full records remain available through read(id). */ + public List listSummaries(int limit) throws IOException { + return list(limit, true); + } + private List list(int limit, boolean compact) throws IOException { List paths = new ArrayList(); try (DirectoryStream stream = Files.newDirectoryStream(directory)) { for (Path path : stream) if (Files.isDirectory(path) && Files.exists(path.resolve("summary.json"))) paths.add(path); @@ -47,7 +69,7 @@ public List list(int limit) throws IOException { List result = new ArrayList(); for (Path path : paths) { if (result.size() >= limit) break; - try { result.add(read(path.getFileName().toString())); } + try { result.add(compact ? readSummary(path.getFileName().toString()) : read(path.getFileName().toString())); } catch (IOException e) { ObjectNode damaged = StateJson.object().put("id", path.getFileName().toString()) .put("status", "UNKNOWN").put("recovery", "UNREADABLE_RECORD"); @@ -56,6 +78,43 @@ public List list(int limit) throws IOException { } return result; } + private synchronized ObjectNode readSummary(String id) throws IOException { + Path run = path(id), file = run.resolve("summary.json"); + BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class); + SummaryCache cached = summaries.get(id); + if (cached != null && cached.matches(attributes)) return cached.value.deepCopy(); + final boolean[] omitted = { false }; + TokenFilter filter = new TokenFilter() { + @Override public TokenFilter includeProperty(String name) { + if ("statements".equals(name)) { omitted[0] = true; return null; } + return this; + } + }; + ObjectNode summary; + try (InputStream input = Files.newInputStream(file); + JsonParser parser = new FilteringParserDelegate(StateJson.MAPPER.getFactory().createParser(input), filter, + TokenFilter.Inclusion.INCLUDE_ALL_AND_PATH, true)) { + JsonNode node = StateJson.MAPPER.readTree(parser); + if (node == null || !node.isObject()) throw new IOException("Expected JSON object"); + summary = (ObjectNode) node; + } + if (omitted[0]) summary.put("detailsOmitted", true); + if ("RUNNING".equals(summary.path("status").asText())) { + if (!isLive(run)) summary.put("status", "UNKNOWN").put("recovery", "NO_TERMINAL_RESULT"); + } else if (SummaryCache.same(attributes, Files.readAttributes(file, BasicFileAttributes.class))) { + summaries.put(id, new SummaryCache(attributes, summary.deepCopy())); + } + return summary; + } + private static final class SummaryCache { + final BasicFileAttributes attributes; + final ObjectNode value; + SummaryCache(BasicFileAttributes attributes, ObjectNode value) { this.attributes = attributes; this.value = value; } + boolean matches(BasicFileAttributes other) { return same(attributes, other); } + static boolean same(BasicFileAttributes a, BasicFileAttributes b) { + return a.size() == b.size() && a.lastModifiedTime().equals(b.lastModifiedTime()) && Objects.equals(a.fileKey(), b.fileKey()); + } + } public List events(String id, long after, int limit) throws IOException { List result = new ArrayList(); Path events = path(id).resolve("events.jsonl"); diff --git a/flydb-runtime/src/test/java/com/flydb/runtime/state/RunStoreTest.java b/flydb-runtime/src/test/java/com/flydb/runtime/state/RunStoreTest.java index 7bf1bef..25be904 100644 --- a/flydb-runtime/src/test/java/com/flydb/runtime/state/RunStoreTest.java +++ b/flydb-runtime/src/test/java/com/flydb/runtime/state/RunStoreTest.java @@ -9,6 +9,53 @@ class RunStoreTest { @TempDir Path state; + @Test void summaryCacheTracksReplacementAndDoesNotCacheLiveness() throws Exception { + RunStore store = new RunStore(state); + String id; + try (RunStore.Writer writer = store.start(StateJson.object().put("command", "plan"))) { + id = writer.id(); + assertThat(store.listSummaries(1).get(0).path("status").asText()).isEqualTo("RUNNING"); + writer.finish("SUCCEEDED", StateJson.object().put("planId", "first"), "NOT_RUN"); + } + assertThat(store.listSummaries(1).get(0).path("status").asText()).isEqualTo("SUCCEEDED"); + ObjectNode changed = store.read(id).put("status", "FAILED"); + StateJson.write(state.resolve("runs").resolve(id).resolve("summary.json"), changed); + assertThat(store.listSummaries(1).get(0).path("status").asText()).isEqualTo("FAILED"); + changed.put("status", "RUNNING"); + StateJson.write(state.resolve("runs").resolve(id).resolve("summary.json"), changed); + assertThat(store.listSummaries(1).get(0).path("recovery").asText()).isEqualTo("NO_TERMINAL_RESULT"); + } + @Test void malformedRecordsStillHaveUnknownOutcomes() throws Exception { + RunStore store = new RunStore(state); + String id; + try (RunStore.Writer writer = store.start(StateJson.object())) { id = writer.id(); } + Path file = state.resolve("runs").resolve(id).resolve("summary.json"); + for (String invalid : new String[] { "{", "[]", "{\"status\":\"FAILED\",\"status\":\"SUCCEEDED\"}" }) { + Files.write(file, invalid.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + ObjectNode record = store.list(1).get(0); + assertThat(record.path("status").asText()).isEqualTo("UNKNOWN"); + assertThat(record.path("recovery").asText()).isEqualTo("UNREADABLE_RECORD"); + } + } + @Test void readsLargePreviewWrittenByTheSameStoreWithoutLosingOutcome() throws Exception { + RunStore store = new RunStore(state); + char[] chars = new char[9 * 1024 * 1024]; + java.util.Arrays.fill(chars, 'x'); + String sql = new String(chars); + String id; + try (RunStore.Writer writer = store.start(StateJson.object().put("command", "plan"))) { + id = writer.id(); + writer.finish("SUCCEEDED", StateJson.object().put("sql", sql), "NOT_RUN"); + } + assertThat(Files.size(state.resolve("runs").resolve(id).resolve("summary.json"))).isGreaterThan(8 * 1024 * 1024); + ObjectNode record = new RunStore(state).list(1).get(0); + assertThat(record.path("status").asText()).isEqualTo("SUCCEEDED"); + assertThat(record.path("result").path("sql").asText()).isEqualTo(sql); + assertThat(store.read(id)).isEqualTo(record); + assertThatThrownBy(() -> StateJson.read(state.resolve("runs").resolve(id).resolve("summary.json"))) + .isInstanceOf(java.io.IOException.class).hasMessageContaining("8 MiB"); + } + @Test void persistsOrderedRedactedEventsAndDetectsCrossInstanceLiveness() throws Exception { RunStore store = new RunStore(state); String id; diff --git a/flydb-skills/plugin.json b/flydb-skills/plugin.json index 9f182c8..1eaf4eb 100644 --- a/flydb-skills/plugin.json +++ b/flydb-skills/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "flydb", - "version": "0.3.6", + "version": "0.3.7", "description": "面向 JDBC 数据库的 Schema 版本化迁移工具:flydb-cli 指导 Agent 使用本机 GUI 并安全执行迁移;flydb-analysis 提供方言预检、结构漂移、对象依赖、应用引用与变更影响分析。", "homepage": "https://github.com/zzxCoding/Flydb", "repository": "https://github.com/zzxCoding/Flydb", diff --git a/flydb-web/frontend/package-lock.json b/flydb-web/frontend/package-lock.json index cd4da22..a0b8bff 100644 --- a/flydb-web/frontend/package-lock.json +++ b/flydb-web/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@flydb/workbench", - "version": "0.3.6", + "version": "0.3.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@flydb/workbench", - "version": "0.3.6", + "version": "0.3.7", "dependencies": { "@codemirror/autocomplete": "6.20.3", "@codemirror/commands": "6.11.0", diff --git a/flydb-web/frontend/package.json b/flydb-web/frontend/package.json index b1f001b..3153cd9 100644 --- a/flydb-web/frontend/package.json +++ b/flydb-web/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@flydb/workbench", "private": true, - "version": "0.3.6", + "version": "0.3.7", "type": "module", "engines": { "node": ">=22.12.0" diff --git a/flydb-web/frontend/src/App.vue b/flydb-web/frontend/src/App.vue index 1a75ece..fbc5163 100644 --- a/flydb-web/frontend/src/App.vue +++ b/flydb-web/frontend/src/App.vue @@ -1,5 +1,5 @@ +