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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **Saved-query Library JSON now uses the version 2 canonical model** (#211):
every entry is `{id, sql, specVersion, spec}`, with the complete Spec carried
unchanged through local storage, tabs, panel edits, sharing, import/export,
and merge. Version 1 files and share links are upgraded on read; exports,
official examples, and generated Libraries now emit only version 2. Unknown
Spec and panel fields are preserved for forward compatibility.
- **Builds now use a committed `package-lock.json` and `npm ci`** (#157), making
local, pull-request, and tagged-release artifacts resolve the same complete
dependency graph instead of silently picking up newly published transitive
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,18 +364,21 @@ an unsaved-changes dot, managed from the header **File ▾** menu:
- **New Library** — clears to an empty, default-named library (confirms first
when non-empty). Open editor tabs are unaffected.
- **Save JSON** (`.json`) — downloads the whole Library in the versioned
`altinity-sql-browser/saved-queries` envelope (lossless: keeps id, name,
description, sql, favorite, chart, view). The filename derives from the Library
name; saving clears the unsaved-changes dot.
`altinity-sql-browser/saved-queries` envelope. Version 2 stores each query as
`{id, sql, specVersion, spec}`: `spec` is the complete, lossless query
definition (`name`, `description`, `favorite`, `view`, `panel`, `dashboard`,
and future extension fields). The filename derives from the Library name;
saving clears the unsaved-changes dot. Version 1 Library files remain
importable and are upgraded in memory; new exports always use version 2.
- **Open… / Append…** — load a `.json` file: Open swaps the Library and
adopts the file's base name (confirms when the current Library is non-empty);
Append merges via the existing dedupe and reports `Added N · updated N ·
skipped N`. **JSON is the only importable format**, and imported SQL is never
run automatically.
- **Share / publish** — **Download Markdown** (`.md`, a `### heading` + fenced
` ```sql ` cookbook) and **Download SQL** (`.sql`, `/* name + description */`
comment blocks, `;`-delimited). Both are **one-way** — lossy by design (no ids,
chart, or view), so JSON stays the canonical round-trip format.
comment blocks, `;`-delimited). Both are **one-way** — lossy by design (no ids
or Spec metadata), so JSON stays the canonical round-trip format.

The Library name is editable inline (click it in the header) and is persisted
separately from the queries. The **•** dot appears after any change that hasn't
Expand Down
2 changes: 1 addition & 1 deletion docs/ONTIME-CHART-DEMO.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dataset (`ontime`, ~230M rows, 1987–2025) on the Antalya demo cluster.
| 9 | Flights by day of week — 2023 | Column | ordinal `dayofweek` axis |
| 10 | Worst average departure delay by airport — 2023 | Bar (horizontal) | a non-count measure (avg minutes), joined for names |

Each saved query stores its chart configuration, so it reopens exactly as designed. (Charts
Each saved query stores its panel configuration in the complete query Spec, so it reopens exactly as designed. (Charts
plot the first 500 rows; the full result is always available in the Table view.)

## Direct links
Expand Down
19 changes: 10 additions & 9 deletions examples/build-iceberg-dashboards.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -320,24 +320,25 @@ function buildEntries(specs, idPrefix) {
const id = `${idPrefix}-${i}`;
if (s.text) {
return {
id, name: s.name, sql: '', favorite: true, description: s.description,
panel: { cfg: { type: 'text', content: s.content } }, view: 'panel',
id, sql: '', specVersion: 1, spec: { name: s.name, favorite: true, description: s.description,
panel: { cfg: { type: 'text', content: s.content } }, view: 'panel' },
};
}
if (s.logs) {
return {
id, name: s.name, sql: s.sql, favorite: true, description: s.description,
panel: { cfg: { type: 'logs', ...s.logs } }, view: 'panel',
id, sql: s.sql, specVersion: 1, spec: { name: s.name, favorite: true, description: s.description,
panel: { cfg: { type: 'logs', ...s.logs } }, view: 'panel' },
};
}
if (s.detail) {
return { id, name: s.name, sql: s.sql, favorite: false, description: s.description, view: 'table' };
return { id, sql: s.sql, specVersion: 1,
spec: { name: s.name, favorite: false, description: s.description, view: 'table' } };
}
const key = schemaKey(s.sql);
console.log(`${id} ${s.cfg.type.padEnd(4)} key=${key}`);
return {
id, name: s.name, sql: s.sql, favorite: true, description: s.description,
panel: { cfg: s.cfg, key }, view: 'panel',
id, sql: s.sql, specVersion: 1, spec: { name: s.name, favorite: true, description: s.description,
panel: { cfg: s.cfg, key }, view: 'panel' },
};
});
}
Expand All @@ -348,8 +349,8 @@ for (const [file, specs, prefix] of [
['iceberg-dba-dashboard.json', DBA, 'iced'],
]) {
const queries = buildEntries(specs, prefix);
const doc = { format: 'altinity-sql-browser/saved-queries', version: 1, exportedAt: stamp, queries };
const doc = { format: 'altinity-sql-browser/saved-queries', version: 2, exportedAt: stamp, queries };
const out = resolve(here, file);
writeFileSync(out, JSON.stringify(doc, null, 2) + '\n');
console.log(`wrote ${out} (${queries.length} entries, ${queries.filter((q) => q.favorite).length} on the dashboard)`);
console.log(`wrote ${out} (${queries.length} entries, ${queries.filter((q) => q.spec.favorite).length} on the dashboard)`);
}
15 changes: 13 additions & 2 deletions examples/build-iceberg-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,20 @@ FORMAT TSVRaw`,
// ---------------------------------------------------------------------------
const doc = {
format: 'altinity-sql-browser/saved-queries',
version: 1,
version: 2,
exportedAt: new Date().toISOString(),
queries,
queries: queries.map(({ id, sql, name, favorite, description, panel, view }) => ({
id,
sql,
specVersion: 1,
spec: {
name,
favorite,
...(description ? { description } : {}),
...(panel ? { panel } : {}),
...(view ? { view } : {}),
},
})),
};

const out = resolve(here, 'iceberg-install.json');
Expand Down
19 changes: 11 additions & 8 deletions examples/build-ontime-charts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// public `ontime` flights dataset on the antalya cluster.
//
// Why a generator: the browser only restores a saved chart config when the
// entry's `chart.key` exactly equals schemaKey(resultColumns) = "name:type|…"
// entry's `spec.panel.key` exactly equals schemaKey(resultColumns) = "name:type|…"
// (see src/ui/results.js chartCfgFor / src/core/chart-data.js schemaKey).
// Hand-writing those type strings is error-prone, so we derive each key live
// from `DESCRIBE (<query>)` against the real cluster.
Expand All @@ -21,7 +21,7 @@ const CONNECTION = 'antalya';

// Each spec: a query + the chart we want it to open with. `cfg` matches the
// app's shape { type, x, y:[...], series }; x/series are column indices, y a
// list of measure-column indices. `view:'chart'` makes a click open the chart.
// list of measure-column indices. `view:'panel'` makes a click open the panel.
const SPECS = [
{
name: 'Busiest origin airports — 2023',
Expand Down Expand Up @@ -177,18 +177,21 @@ const queries = SPECS.map((s, i) => {
console.log(`#${i + 1} ${s.cfg.type.padEnd(4)} rows=${String(rows).padStart(5)} key=${key}`);
return {
id: 's' + (i + 1),
name: s.name,
sql: s.sql,
favorite: false,
description: s.description,
chart: { cfg: s.cfg, key },
view: 'chart',
specVersion: 1,
spec: {
name: s.name,
favorite: false,
description: s.description,
panel: { cfg: s.cfg, key },
view: 'panel',
},
};
});

const doc = {
format: 'altinity-sql-browser/saved-queries',
version: 1,
version: 2,
exportedAt: new Date().toISOString(),
queries,
};
Expand Down
13 changes: 6 additions & 7 deletions examples/build-system-explorer-charts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// https://gist.github.com/filimonov/271e5b27c085356c67db3c1bf2204506
//
// Why a generator: the browser only restores a saved chart config when the
// entry's `chart.key` exactly equals schemaKey(resultColumns) = "name:type|…"
// entry's `spec.panel.key` exactly equals schemaKey(resultColumns) = "name:type|…"
// (see src/ui/results.js chartCfgFor / src/core/chart-data.js schemaKey).
// Hand-writing those type strings is error-prone (Enum8/LowCardinality wrap
// exactly), so we derive each key live from `DESCRIBE (<query>)` against a
Expand Down Expand Up @@ -228,24 +228,23 @@ function schemaKey(sql) {
const queries = SPECS.map((s, i) => {
const base = {
id: 'sys-' + (i + 1),
name: s.name,
sql: s.sql,
favorite: !!s.cfg,
description: s.description,
specVersion: 1,
spec: { name: s.name, favorite: !!s.cfg, description: s.description },
};
if (!s.cfg) return base;
const key = schemaKey(s.sql);
console.log(`#${i + 1} ${s.cfg.type.padEnd(4)} key=${key}`);
return { ...base, chart: { cfg: s.cfg, key }, view: 'chart' };
return { ...base, spec: { ...base.spec, panel: { cfg: s.cfg, key }, view: 'panel' } };
});

const doc = {
format: 'altinity-sql-browser/saved-queries',
version: 1,
version: 2,
exportedAt: new Date().toISOString(),
queries,
};

const outPath = resolve(here, 'system-explorer-charts.json');
writeFileSync(outPath, JSON.stringify(doc, null, 2) + '\n');
console.log(`\nwrote ${outPath} (${queries.length} queries, ${queries.filter((q) => q.favorite).length} favorited for the Dashboard)`);
console.log(`\nwrote ${outPath} (${queries.length} queries, ${queries.filter((q) => q.spec.favorite).length} favorited for the Dashboard)`);
Loading