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
36 changes: 35 additions & 1 deletion .github/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,25 @@ npm run e2e
```
e2e/
playwright.config.ts config; starts the app via `npm run e2e:server`
playwright.screenshots.config.ts
the README gallery run (see README screenshots below)
global-setup.ts brings the database stack up if it isn't already
e2e-server.mjs builds the frontend, then runs `go run -tags server ./cmd/e2e-server`
pages/ page objects, one per app surface
support/
support/ shared by both suites
fixtures.ts the `test` every spec imports: page-object fixtures + `seed`
databases.ts the driver matrix (POSTGRES / MYSQL / MARIADB / SQLITE)
seed.ts Seeder: create + populate a uniquely-named table through the UI
ports.ts port probe/wait helpers both global setups use
specs/ grouped by surface: editor/, results/, sidebar/, table-view/,
plus app-shell and connections at the root
screenshots/ everything only the gallery run needs
readme.spec.ts one serial test that captures 1.png - 12.png
global-setup.ts recreates the `forum` database from the dump
screenshot-db.ts the demo connections (Forum / Postgres) and the 2048x1152 viewport
fixtures/
forum.dump.sql the demo dataset restored before every run
users.csv 20 rows matching `public.users`, loaded by the import shot
```

Specs import shared code via the `@support/*` alias (see `tsconfig.json`), so nesting
Expand Down Expand Up @@ -234,6 +244,30 @@ the matrix suites replay it on all four drivers.
results grid, table view, tabs, etc.) or when touching `e2e/`, `cmd/e2e-server/`, or
server-mode event handling in `internal/app/`.

### README screenshots

A second Playwright run captures the whole README gallery - `1.png` - `12.png` under
[`.github/screenshots/`](screenshots/) - from one serial test, so every shot shares the
same workspace, theme and panel sizes:

```bash
task screenshots # or: cd e2e && npm run screenshots
```

`screenshots/global-setup.ts` recreates the `forum` database from
`screenshots/fixtures/forum.dump.sql` first, so the data in the gallery is reproducible.
Overrides: `XENSQL_SCREENSHOT_PG_*` (server), `XENSQL_SCREENSHOT_CSV` (the CSV in the
import shot - its path is visible in the dialog), `XENSQL_SCREENSHOT_PG_CONTAINER`
(restore via `docker exec` instead of `docker compose exec`).

Unlike the functional suite it runs at 2048x1152 and builds the frontend with
`XENSQL_FORCE_WINDOW_CHROME=1` so server mode still draws the window controls. The native
file picker can't open there, so the import shot answers the `PickImportFile` binding over
`POST /wails/runtime` with a fixture path.

**Not in CI**, and it rewrites the committed PNGs - regenerate deliberately, then review
the diff.

---

## Continuous integration
Expand Down
Binary file modified .github/screenshots/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/11.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ XenSQL-data/

# Playwright E2E
/e2e/XenSQL-data/
/e2e/XenSQL-data-screenshots/
/e2e/test-results/
/e2e/blob-report/
/e2e/playwright/.cache/
Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Work with **SQLite**, **PostgreSQL** and **MySQL / MariaDB** in a single fast de
- Run multi-statement scripts and get a result tab per output
- Explore schemas instantly - down to indexes, constraints and triggers
- Copy any object's DDL in one click
- Import a CSV or run a `.sql` script
- Save and reuse queries
- Export anything in one click
- Auto-update with one click
Expand All @@ -91,7 +92,12 @@ Work with **SQLite**, **PostgreSQL** and **MySQL / MariaDB** in a single fast de
<tr>
<td align="center"><img src=".github/screenshots/7.png?raw=true" width="100%"><br><sub><b>Connections</b></sub></td>
<td align="center"><img src=".github/screenshots/8.png?raw=true" width="100%"><br><sub><b>Quick Search</b></sub></td>
<td align="center"><img src=".github/screenshots/9.png?raw=true" width="100%"><br><sub><b>Appearance</b></sub></td>
<td align="center"><img src=".github/screenshots/9.png?raw=true" width="100%"><br><sub><b>DDL viewer</b></sub></td>
</tr>
<tr>
<td align="center"><img src=".github/screenshots/10.png?raw=true" width="100%"><br><sub><b>Plan viewer</b></sub></td>
<td align="center"><img src=".github/screenshots/11.png?raw=true" width="100%"><br><sub><b>CSV & SQL import</b></sub></td>
<td align="center"><img src=".github/screenshots/12.png?raw=true" width="100%"><br><sub><b>Appearance</b></sub></td>
</tr>
</table>

Expand Down Expand Up @@ -279,6 +285,35 @@ View and modify table data directly in the grid - no hand-written `UPDATE` / `DE

---

## πŸ“₯ Import

Load a **CSV** (or any delimited file) or run a **`.sql` script**, from the ⬆ button in the Schema
Explorer or a table's right-click menu.

### CSV
- **Delimiter auto-detected** (`,` `;` tab `|`) - the one that splits every line the same way wins,
so commas inside quoted values don't fool it; override it if you'd rather
- **Column mapping** - each source column shows its first value and the target column it loads
into; blank the target to skip that column entirely
- **Import into an existing table**, optionally emptying it first, **or create a new one** with
types inferred from the data (integer, decimal, boolean, date, timestamp, text) - each one
editable before you commit to it, and named in your engine's own dialect
- Headers optional, `NULL` text configurable (`\N`, `NULL`, whatever your exporter writes),
leading lines skippable for files with a preamble, whitespace trimmable
- UTF-8 BOMs stripped, ragged rows tolerated
- **Bad rows are skipped and reported**, not fatal - or tick **stop at the first error** if you'd
rather nothing partial lands
- Rows load in batches with live progress, and **Stop** ends a long run

### SQL scripts
A `.sql` file runs statement by statement on one connection, so `SET`, temp tables and scripted
transactions behave. You get a progress bar and a count - not thousands of result tabs.

> Batches are committed as they go, so a stopped or failed import keeps the rows it had already
> loaded and tells you how many that was. Read-only connections refuse imports outright.

---

## πŸ“€ Export

- **CSV** / **JSON** / **Markdown** / **SQL INSERT**
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,9 @@ tasks:
- defer: { task: e2e:down }
- task: e2e:up
- task: e2e:ui

screenshots:
summary: Capture README screenshots into .github/screenshots (starts Postgres + restores forum.dump.sql)
dir: e2e
cmds:
- npm run screenshots
2 changes: 1 addition & 1 deletion build/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ info:
description: "A fast, native SQL client built with Go, Wails and React." # The application description
copyright: "(c) 2026, Bare7a" # Copyright text
comments: "A fast, native SQL client built with Go, Wails and React." # Comments
version: "1.4.6" # The application version
version: "1.5.0" # The application version
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
# # Should match the name of your .icon file without the extension
# # If not set and Assets.car exists, defaults to "appicon"
Expand Down
4 changes: 2 additions & 2 deletions build/darwin/Info.dev.plist
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>CFBundleVersion</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>NSAppTransportSecurity</key>
Expand Down
4 changes: 2 additions & 2 deletions build/darwin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>CFBundleVersion</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>LSMinimumSystemVersion</key>
<string>12.0.0</string>
<key>NSHighResolutionCapable</key>
Expand Down
4 changes: 2 additions & 2 deletions build/ios/Info.dev.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.6-dev</string>
<string>1.5.0-dev</string>
<key>CFBundleVersion</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
Expand Down
4 changes: 2 additions & 2 deletions build/ios/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>CFBundleVersion</key>
<string>1.4.6</string>
<string>1.5.0</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
Expand Down
2 changes: 1 addition & 1 deletion build/linux/nfpm/nfpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
name: "XenSQL"
arch: ${GOARCH}
platform: "linux"
version: "1.4.6"
version: "1.5.0"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
Expand Down
4 changes: 2 additions & 2 deletions build/windows/info.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "1.4.6"
"file_version": "1.5.0"
},
"info": {
"0000": {
"ProductVersion": "1.4.6",
"ProductVersion": "1.5.0",
"CompanyName": "Bare7a",
"FileDescription": "A fast, native SQL client built with Go, Wails and React.",
"LegalCopyright": "(c) 2026, Bare7a",
Expand Down
2 changes: 1 addition & 1 deletion build/windows/nsis/wails_tools.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "XenSQL"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.4.6"
!define INFO_PRODUCTVERSION "1.5.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "(c) 2026, Bare7a"
Expand Down
2 changes: 1 addition & 1 deletion build/windows/wails.exe.manifest
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="eu.bare7a.xensql" version="1.4.6" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="eu.bare7a.xensql" version="1.5.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
Expand Down
5 changes: 4 additions & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,10 @@ <h2>Screenshots</h2>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/6.png" alt="Export dialog" loading="lazy" /><figcaption>Export</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/7.png" alt="Connection manager" loading="lazy" /><figcaption>Connections</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/8.png" alt="Quick Search palette" loading="lazy" /><figcaption>Quick Search</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/9.png" alt="Appearance settings" loading="lazy" /><figcaption>Appearance</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/9.png" alt="Table DDL in an editor tab beside the schema tree" loading="lazy" /><figcaption>DDL viewer</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/10.png" alt="EXPLAIN ANALYZE plan tree with a heat map and node details" loading="lazy" /><figcaption>Plan viewer</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/11.png" alt="Import dialog with a CSV mapped onto an existing table" loading="lazy" /><figcaption>CSV &amp; SQL import</figcaption></figure>
<figure><img src="https://raw.githubusercontent.com/Bare7a/XenSQL/master/.github/screenshots/12.png" alt="Appearance settings" loading="lazy" /><figcaption>Appearance</figcaption></figure>
</div>
</div>
</section>
Expand Down
14 changes: 12 additions & 2 deletions e2e/e2e-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ const serverHost = process.env.WAILS_SERVER_HOST ?? '127.0.0.1';
const serverPort = process.env.WAILS_SERVER_PORT ?? '8080';

// 1. Fresh data directory so tests start from a known-empty state.
rmSync(dataDir, { recursive: true, force: true });
if (process.env.XENSQL_SKIP_DATA_RESET !== '1') {
rmSync(dataDir, { recursive: true, force: true });
}
mkdirSync(dataDir, { recursive: true });

// 2. Build the frontend and copy it into the embed directory.
console.log('[e2e-server] building frontend (build:dev)...');
execSync('npm run build:dev', { cwd: frontendDir, stdio: 'inherit' });
execSync('npm run build:dev', {
cwd: frontendDir,
stdio: 'inherit',
env: {
...process.env,
// Forward so screenshot captures can show minimize/maximize/close in server mode.
VITE_FORCE_WINDOW_CHROME: process.env.XENSQL_FORCE_WINDOW_CHROME ?? '',
},
});

console.log('[e2e-server] staging assets into cmd/e2e-server/dist...');
stageDist();
Expand Down
24 changes: 1 addition & 23 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { execSync } from 'node:child_process';
import net from 'node:net';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { MARIADB, MYSQL, POSTGRES } from './support/databases';
import { probePort, waitForPort } from './support/ports';

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const compose = process.env.COMPOSE ?? 'docker compose';
Expand All @@ -14,28 +14,6 @@ const services = [POSTGRES, MYSQL, MARIADB].map((db) => ({
port: db.port as number,
}));

function probePort(host: string, port: number): Promise<boolean> {
return new Promise((resolve) => {
const socket = net.connect({ host, port });
const done = (ok: boolean) => {
socket.destroy();
resolve(ok);
};
socket.once('connect', () => done(true));
socket.once('error', () => done(false));
socket.setTimeout(1_000, () => done(false));
});
}

async function waitForPort(host: string, port: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await probePort(host, port)) return true;
await new Promise((r) => setTimeout(r, 500));
}
return false;
}

// Data directory is owned/reset by e2e-server.mjs; here we only ensure every
// database server is up and healthy.
export default async function globalSetup() {
Expand Down
1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"e2e:debug": "playwright test --debug",
"e2e:report": "playwright show-report",
"e2e:server": "node e2e-server.mjs",
"screenshots": "playwright test --config=playwright.screenshots.config.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
Expand Down
47 changes: 44 additions & 3 deletions e2e/pages/connections-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { expect, type Locator, type Page } from '@playwright/test';
import type { DbConfig } from '../support/databases';
import { html5DragTo } from '../support/dnd';

export type ConnectionDialogOptions = DbConfig & {
/** Hex color from DEFAULT_COLORS (e.g. `#ef4444`). */
color?: string;
readOnly?: boolean;
};

/** Connection switcher, connection dialog and the connection list (connect / disconnect / delete / reorder). */
export class ConnectionsPage {
readonly page: Page;
Expand Down Expand Up @@ -39,7 +45,7 @@ export class ConnectionsPage {
await this.dialog.waitFor({ state: 'visible' });
}

async fillDialog(cfg: DbConfig): Promise<void> {
async fillDialog(cfg: ConnectionDialogOptions): Promise<void> {
// Choose the driver first; switching drivers resets dependent fields.
await this.page.locator('#conn-driver').selectOption(cfg.driver);
await this.page.locator('#conn-name').fill(cfg.label);
Expand All @@ -53,6 +59,41 @@ export class ConnectionsPage {
} else {
await this.page.locator('#conn-file').fill(cfg.filePath ?? '');
}

if (cfg.readOnly != null) {
const box = this.dialog.locator('.form-group-checkbox input[type="checkbox"]').first();
if (cfg.readOnly) await box.check();
else await box.uncheck();
}

if (cfg.color) {
const swatch = this.dialog.locator('.color-swatch').evaluateAll(
(els, color) => {
const want = color.toLowerCase();
return els.findIndex((el) => {
const bg = getComputedStyle(el).backgroundColor;
// style="" uses the hex; computed is rgb(r, g, b).
const inline = (el as HTMLElement).style.backgroundColor || (el as HTMLElement).style.background;
return inline.toLowerCase() === want || rgbToHex(bg) === want;
});

function rgbToHex(rgb: string): string {
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!m) return '';
return (
'#' +
[m[1], m[2], m[3]]
.map((n) => Number(n).toString(16).padStart(2, '0'))
.join('')
);
}
},
cfg.color,
);
const idx = await swatch;
if (idx < 0) throw new Error(`No color swatch matching ${cfg.color}`);
await this.dialog.locator('.color-swatch').nth(idx).click();
}
}

async testConnectionInDialog(): Promise<void> {
Expand All @@ -65,14 +106,14 @@ export class ConnectionsPage {
}

/** Open the dialog, fill it for the given driver and save. */
async create(cfg: DbConfig): Promise<void> {
async create(cfg: ConnectionDialogOptions): Promise<void> {
await this.openNewDialog();
await this.fillDialog(cfg);
await this.saveDialog();
}

/** Save a connection and immediately connect to it - the common "given a live connection" setup. */
async createAndConnect(cfg: DbConfig): Promise<void> {
async createAndConnect(cfg: ConnectionDialogOptions): Promise<void> {
await this.create(cfg);
await this.connect(cfg.label);
}
Expand Down
Loading
Loading