Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/analysis-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@ of other databases, `INSTALL`/`LOAD` of extensions, and queries against S3 views
credentials are already configured. Treat an app's JavaScript as code you are choosing to
run, exactly as you would a shell script.

So the choice is asked for. The first time a folder opens as an app — from the command line,
a drop, the picker, or a tab restored at launch — its tab says what the app's SQL can do and
waits: **View source** shows the entry file without running it, **Trust and run** runs it.
The answer is remembered per folder, so later launches and every reload after a save run
without asking again. `ducklocal export --html` runs the app you name on the command line
and does not ask.

What an app does **not** get is anything else the process could do:

- **No filesystem, network, process, or environment module.** `fs`, `net`, `process` and
Expand All @@ -247,7 +254,7 @@ Because it is the loading state, a statement an app only runs on a click is not

A `.dash` file is a dashboard declared as data — query and plot blocks, no JavaScript — for the common case of standard plots over saved queries. The file format and `ducklocal check` validation are in [the CLI guide](cli.md#check-a-dashboard-spec).

Open one like an app: name it on the command line (`ducklocal dashboard.dash`) or drag the file onto the window, and it opens as a dashboard tab beside your queries and apps. The tab runs the spec's queries on the window's own connection — so a dashboard sees connection-local state such as `TEMP` tables, and queues with the editor's queries — and renders each plot as one panel of a vertical stack whose dividers drag to resize. A plot whose query fails shows the reason in its own panel; the rest of the dashboard still draws. The toolbar's reload re-reads the file and re-runs everything, and a reload that fails validation never replaces a working dashboard — the reason appears above it instead. Open dashboards are remembered between launches, exactly like apps.
Open one like an app: name it on the command line (`ducklocal dashboard.dash`) or drag the file onto the window, and it opens as a dashboard tab beside your queries and apps. The tab runs the spec's queries on the window's own connection — so a dashboard sees connection-local state such as `TEMP` tables, and queues with the editor's queries — and renders each plot as one panel of a vertical stack whose dividers drag to resize. Only read-only statements run: a query that could write (DDL, DML, `COPY`, `ATTACH`, …) is refused before it reaches the connection, so opening a `.dash` file someone sent you cannot change your data. A plot whose query fails shows the reason in its own panel; the rest of the dashboard still draws. The toolbar's reload re-reads the file and re-runs everything, and a reload that fails validation never replaces a working dashboard — the reason appears above it instead. Open dashboards are remembered between launches, exactly like apps.

The tab is also an editor: its source view edits the file with completion and diagnostics, and writes back with the save button or ⌘S — a save whose spec no longer validates keeps the last working dashboard up and says why. Outside the GUI, `ducklocal lsp` serves the same completion, diagnostics, hover and go-to-definition to any LSP-capable editor (see [the CLI guide](cli.md#edit-a-dashboard-spec-with-lsp)).

Expand Down
2 changes: 1 addition & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ ducklocal check dashboard.dash
ducklocal check dashboard.dash --database warehouse.duckdb
```

A `query` block holds one `sql` attribute — one statement, as a heredoc or a string. A `plot` block holds `type` (one of `line`, `bar`, `area`, `scatter`, `table`), `query` (a reference like `query.latency` to a query block in the same file), `x` and `y` (result columns, as bare identifiers or quoted strings; `y` is optional for `table`), plus optional `series` and `title`. `#` and `//` comment to end of line. That is the whole language: no functions, no conditionals, no interpolation.
A `query` block holds one `sql` attribute — one read-only statement, as a heredoc or a string. `SELECT`, `WITH`, `FROM`-first, `VALUES`, `SHOW`, `DESCRIBE`, `SUMMARIZE` and `PIVOT` are accepted; anything that could write — DDL, DML, `COPY`, `ATTACH`, `INSTALL` — is a diagnostic, and the GUI refuses to run it, because a dashboard's queries run whenever the file is opened. A `plot` block holds `type` (one of `line`, `bar`, `area`, `scatter`, `table`), `query` (a reference like `query.latency` to a query block in the same file), `x` and `y` (result columns, as bare identifiers or quoted strings; `y` is optional for `table`), plus optional `series` and `title`. `#` and `//` comment to end of line. That is the whole language: no functions, no conditionals, no interpolation.

Without `--database` the check is fully static — no table needs to exist and nothing executes. Each query's SQL is validated by the real DuckDB parser on a throwaway connection, the way `query` validates before running. With `--database PATH` (existing file, read-only) every query is additionally described — planned, not run — and each plot's `x`/`y`/`series` is checked against the columns the query actually returns; a `y` that is not numeric is an error for every type but `table`.

Expand Down
7 changes: 6 additions & 1 deletion docs/zh/analysis-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ npx --yes -p typescript tsc -p <应用目录>/jsconfig.json --noImplicitAny fals
包括 `COPY` 写文件、`ATTACH` 其他数据库、`INSTALL`/`LOAD` 扩展,以及查询已配置好的 S3 视图。请把应用
里的 JavaScript 当作你主动选择运行的代码,就像对待一个 shell 脚本那样。

因此这个选择会明确征求你的同意。一个文件夹第一次作为应用打开时——无论来自命令行、拖放、选择器,还是启动时恢复的
标签页——标签页会先说明应用的 SQL 能做什么并等待:**先看源码**只显示入口文件、不运行,**信任并运行**才会运行。
同意按文件夹记住,之后的启动以及保存后的每次重新加载都不再询问。`ducklocal export --html` 运行的是你在命令行
上点名的应用,不会询问。

应用拿不到的,是进程的其他能力:

- **没有文件系统、网络、进程与环境变量模块。** 除非宿主授予,否则脚本无法使用 `fs`、`net`、
Expand All @@ -219,7 +224,7 @@ npx --yes -p typescript tsc -p <应用目录>/jsconfig.json --noImplicitAny fals

`.dash` 文件把 dashboard 声明为数据——query 与 plot block,没有 JavaScript——适合「已保存查询 + 标准图表」的常见场景。文件格式与 `ducklocal check` 校验见 [CLI 指南](cli.md#校验-dashboard-规格文件)。

打开方式与应用相同:在命令行指定(`ducklocal dashboard.dash`),或把文件拖到窗口上,就会以 dashboard 标签页的形式打开,与查询、应用并列。标签页在窗口自己的连接上执行规格里的查询——因此能看到 `TEMP` 表等连接级状态,也会与编辑器的查询排队——并把每个 plot 画成竖直堆叠中的一格,分隔条可拖动调整高度。某个 plot 的查询失败时,原因显示在它自己的格子里,其余部分照常绘制。工具栏的重新加载会重读文件并重跑所有查询;重载后的规格若不再通过校验,不会替换掉仍在工作的 dashboard,而是在上方显示原因。已打开的 dashboard 会像应用一样被记住,下次启动自动恢复。
打开方式与应用相同:在命令行指定(`ducklocal dashboard.dash`),或把文件拖到窗口上,就会以 dashboard 标签页的形式打开,与查询、应用并列。标签页在窗口自己的连接上执行规格里的查询——因此能看到 `TEMP` 表等连接级状态,也会与编辑器的查询排队——并把每个 plot 画成竖直堆叠中的一格,分隔条可拖动调整高度。只有只读语句会被执行:可能写入的查询(DDL、DML、`COPY`、`ATTACH` 等)在到达连接之前就被拒绝,因此打开别人发来的 `.dash` 文件不会改动你的数据。某个 plot 的查询失败时,原因显示在它自己的格子里,其余部分照常绘制。工具栏的重新加载会重读文件并重跑所有查询;重载后的规格若不再通过校验,不会替换掉仍在工作的 dashboard,而是在上方显示原因。已打开的 dashboard 会像应用一样被记住,下次启动自动恢复。

标签页同时也是编辑器:源码视图可以直接修改文件,带补全和诊断,用保存按钮或 ⌘S 写回——保存后的规格若不再通过校验,仍在工作的 dashboard 不会被替换,并会显示原因。在 GUI 之外,`ducklocal lsp` 把同样的补全、诊断、悬停和跳转定义提供给任何支持 LSP 的编辑器(见 [CLI 指南](cli.md#用-lsp-编辑-dashboard-规格文件))。

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ ducklocal check dashboard.dash
ducklocal check dashboard.dash --database warehouse.duckdb
```

`query` block 只含一个 `sql` 属性——一条语句,heredoc 或字符串。`plot` block 含 `type`(`line`、`bar`、`area`、`scatter`、`table` 之一)、`query`(指向同文件某个 query block 的引用,如 `query.latency`)、`x` 和 `y`(结果列名,可写裸标识符或带引号的字符串;`table` 类型不需要 `y`),以及可选的 `series`、`title`。`#` 和 `//` 注释到行尾。这就是全部语法:没有函数、没有条件、没有插值。
`query` block 只含一个 `sql` 属性——一条只读语句,heredoc 或字符串。接受 `SELECT`、`WITH`、`FROM` 开头、`VALUES`、`SHOW`、`DESCRIBE`、`SUMMARIZE` 与 `PIVOT`;任何可能写入的语句——DDL、DML、`COPY`、`ATTACH`、`INSTALL`——都会报诊断,GUI 也拒绝执行,因为 dashboard 的查询在文件一打开时就会运行。`plot` block 含 `type`(`line`、`bar`、`area`、`scatter`、`table` 之一)、`query`(指向同文件某个 query block 的引用,如 `query.latency`)、`x` 和 `y`(结果列名,可写裸标识符或带引号的字符串;`table` 类型不需要 `y`),以及可选的 `series`、`title`。`#` 和 `//` 注释到行尾。这就是全部语法:没有函数、没有条件、没有插值。

不带 `--database` 时校验完全静态——表不需要存在,什么都不会执行。每条查询的 SQL 由真正的 DuckDB parser 在一次性连接上校验,与 `query` 执行前的校验相同。带 `--database PATH`(已存在的文件,只读打开)时,每条查询还会被 describe——只规划、不执行——并把每个 plot 的 `x`/`y`/`series` 与查询实际返回的列逐一核对;对 `table` 以外的类型,`y` 不是数值列也是错误。

Expand Down
4 changes: 2 additions & 2 deletions skills/ducklocal/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ plot "revenue" {
}
```

A `query` block holds one `sql` attribute (one statement, heredoc or string). A `plot` block holds `type` (`line`, `bar`, `area`, `scatter`, `table`), `query` (a `query.name` reference), `x` and `y` (result columns, bare identifiers or quoted strings; `y` optional for `table`), optional `series` and `title`. No functions, conditionals, or interpolation exist. There is a working example at `examples/analysis_app/dashboard.dash`.
A `query` block holds one `sql` attribute (one read-only statement — SELECT, WITH, FROM, VALUES, SHOW, DESCRIBE, SUMMARIZE or PIVOT — heredoc or string; DDL, DML, COPY, ATTACH and INSTALL are rejected). A `plot` block holds `type` (`line`, `bar`, `area`, `scatter`, `table`), `query` (a `query.name` reference), `x` and `y` (result columns, bare identifiers or quoted strings; `y` optional for `table`), optional `series` and `title`. No functions, conditionals, or interpolation exist. There is a working example at `examples/analysis_app/dashboard.dash`.

Always validate before handing a spec over: `ducklocal check dashboard.dash`, or `ducklocal check dashboard.dash --database warehouse.duckdb` to also verify every `x`/`y`/`series` against the columns the queries actually return (a non-numeric `y` is an error outside `table`). A spec mistake is exit 2 with kind `spec`, one `file:line: message` per diagnostic — fix all of them, not just the first. To see it rendered, open the file in the GUI (`ducklocal dashboard.dash` or drag it onto the window): it becomes a dashboard tab, a resizable vertical stack of the plots with per-plot inline errors.

## Authoring an app

An analysis app is a folder holding `main.js`: a default-exported `View` subclass. `init(props, cx)` runs at load; `render()` returns the UI tree. The smallest working app:
An analysis app is a folder holding `main.js`: a default-exported `View` subclass. `init(props, cx)` runs at load; `render()` returns the UI tree. The first time a folder opens in the GUI its tab asks the user to **Trust and run** before any of its code or SQL runs — tell the user to expect that when you hand an app over. The smallest working app:

```js
import { View, div } from "gpui-kit";
Expand Down
2 changes: 1 addition & 1 deletion skills/ducklocal/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ ducklocal check dashboard.dash
ducklocal check dashboard.dash --database warehouse.duckdb
```

Validates a `.dash` file — `query "name" { sql = <<SQL … SQL }` blocks and `plot "name" { type/query/x/y/series/title }` blocks joined by `query.name` references — without opening a window. Static checks (syntax, duplicate names, dangling references, SQL through the real parser) need no database; `--database` (existing file, read-only) additionally describes each query and checks plot columns against what it returns, including a numeric-type check on `y`. Success prints one JSON object with the spec's queries and plots. A spec mistake is exit 2, kind `spec`, one `file:line: message` per diagnostic, all diagnostics at once; database/I/O failures are exit 1.
Validates a `.dash` file — `query "name" { sql = <<SQL … SQL }` blocks and `plot "name" { type/query/x/y/series/title }` blocks joined by `query.name` references — without opening a window. Static checks (syntax, duplicate names, dangling references, SQL through the real parser — one read-only statement per query) need no database; `--database` (existing file, read-only) additionally describes each query and checks plot columns against what it returns, including a numeric-type check on `y`. Success prints one JSON object with the spec's queries and plots. A spec mistake is exit 2, kind `spec`, one `file:line: message` per diagnostic, all diagnostics at once; database/I/O failures are exit 1.

## Discover, aggregate, convert

Expand Down
48 changes: 48 additions & 0 deletions src/analysis/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,58 @@ pub fn restore() -> Restored {
}
}

/// The `settings` key prefix under which a trusted app folder is recorded.
const TRUST_PREFIX: &str = "app_trusted:";

/// An app's SQL runs with the user's full database privileges — `COPY` to
/// files, `ATTACH`, `read_text` of anything readable — and apps open without a
/// click: from the command line, from a drop, and again at every launch. So
/// the first run of a folder waits for the user to say yes, once per folder.
/// The folder rather than its content is what is trusted: editing an app and
/// saving it is the authoring loop, and asking again on every save would train
/// the user to click through.
fn trust_key(directory: &Path) -> String {
let directory = directory
.canonicalize()
.unwrap_or_else(|_| directory.to_path_buf());
format!("{TRUST_PREFIX}{}", directory.to_string_lossy())
}

/// Whether the user has said this folder's app may run.
pub fn is_trusted(directory: &Path) -> bool {
crate::history::get_setting(&trust_key(directory))
.ok()
.flatten()
.is_some()
}

/// Record that the user trusts this folder's app.
pub fn trust(directory: &Path) -> anyhow::Result<()> {
crate::history::set_setting(&trust_key(directory), &crate::history::now_timestamp())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn trust_is_remembered_per_folder() {
let _guard = crate::db::connection_guard();
crate::history::with_test_history(|| {
let trusted = TempDir::new("trust_yes");
trusted.app();
let other = TempDir::new("trust_no");
other.app();

assert!(!is_trusted(trusted.path()));
trust(trusted.path()).unwrap();
assert!(is_trusted(trusted.path()));
// The same folder by another spelling is the same folder.
assert!(is_trusted(&trusted.path().join(".")));
assert!(!is_trusted(other.path()));
});
}

/// A directory that removes itself, named for the test that made it.
struct TempDir(PathBuf);

Expand Down
Loading
Loading