fix: PG cell-edit serialization + recent-connections startup race (#6)
This commit was merged in pull request #6.
This commit is contained in:
@@ -140,13 +140,13 @@ jobs:
|
|||||||
|
|
||||||
# Ensure the release exists (idempotent — both jobs may call this).
|
# Ensure the release exists (idempotent — both jobs may call this).
|
||||||
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
if [ -z "$REL_ID" ]; then
|
if [ -z "$REL_ID" ]; then
|
||||||
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
||||||
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
||||||
"${API}/releases" \
|
"${API}/releases" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
||||||
@@ -189,7 +189,13 @@ jobs:
|
|||||||
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/pg_tools"
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
URL="https://get.enterprisedb.com/postgresql/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
URL="https://get.enterprisedb.com/postgresql/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
||||||
curl -fsSL "$URL" -o /tmp/pg.zip
|
CACHE="/c/ci/cache/postgresql-${PG_VER}-1-windows-x64-binaries.zip"
|
||||||
|
if [ -f "$CACHE" ]; then
|
||||||
|
echo "Using cached PG zip: $CACHE"
|
||||||
|
cp "$CACHE" /tmp/pg.zip
|
||||||
|
else
|
||||||
|
curl -fsSL "$URL" -o /tmp/pg.zip
|
||||||
|
fi
|
||||||
EXPECTED="3508d8f085bc3980f38211a82e3f31e5fcae9952105d3dc2f8be67b64a822baa"
|
EXPECTED="3508d8f085bc3980f38211a82e3f31e5fcae9952105d3dc2f8be67b64a822baa"
|
||||||
echo "$EXPECTED /tmp/pg.zip" | sha256sum -c -
|
echo "$EXPECTED /tmp/pg.zip" | sha256sum -c -
|
||||||
sha256sum /tmp/pg.zip
|
sha256sum /tmp/pg.zip
|
||||||
@@ -205,16 +211,34 @@ jobs:
|
|||||||
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/mysql_tools"
|
OUT="${GITHUB_WORKSPACE}/desktop/src-tauri/resources/mysql_tools"
|
||||||
mkdir -p "$OUT"
|
mkdir -p "$OUT"
|
||||||
URL="https://archive.mariadb.org/mariadb-${MARIADB_VER}/winx64-packages/mariadb-${MARIADB_VER}-winx64.zip"
|
URL="https://archive.mariadb.org/mariadb-${MARIADB_VER}/winx64-packages/mariadb-${MARIADB_VER}-winx64.zip"
|
||||||
curl -fsSL "$URL" -o /tmp/mariadb.zip
|
CACHE="/c/ci/cache/mariadb-${MARIADB_VER}-winx64.zip"
|
||||||
|
if [ -f "$CACHE" ]; then
|
||||||
|
echo "Using cached MariaDB zip: $CACHE"
|
||||||
|
cp "$CACHE" /tmp/mariadb.zip
|
||||||
|
else
|
||||||
|
curl -fsSL "$URL" -o /tmp/mariadb.zip
|
||||||
|
fi
|
||||||
sha256sum /tmp/mariadb.zip
|
sha256sum /tmp/mariadb.zip
|
||||||
unzip -o /tmp/mariadb.zip -d /tmp/mariadb
|
unzip -o /tmp/mariadb.zip -d /tmp/mariadb
|
||||||
WINROOT="/tmp/mariadb/mariadb-${MARIADB_VER}-winx64"
|
WINROOT="/tmp/mariadb/mariadb-${MARIADB_VER}-winx64"
|
||||||
cp "$WINROOT"/bin/mariadb-dump.exe "$WINROOT"/bin/mariadb.exe "$OUT"/
|
cp "$WINROOT"/bin/mariadb-dump.exe "$WINROOT"/bin/mariadb.exe "$OUT"/
|
||||||
cp "$WINROOT"/lib/libmariadb.dll "$OUT"/
|
cp "$WINROOT"/lib/libmariadb.dll "$OUT"/
|
||||||
|
|
||||||
|
# rustc resolves `link.exe` via PATH for the windows-msvc target, but Git
|
||||||
|
# for Windows ships GNU coreutils with its own `link.exe` that shadows
|
||||||
|
# MSVC's linker (`link: extra operand ...`). Load the MSVC dev
|
||||||
|
# environment so PATH/LIB/INCLUDE point at the real MSVC toolchain.
|
||||||
|
# Fails fast here (instead of at link time) if the toolchain is missing.
|
||||||
|
- name: Set up MSVC build environment
|
||||||
|
uses: ilammy/msvc-dev-cmd@v1
|
||||||
|
with:
|
||||||
|
arch: x64
|
||||||
|
|
||||||
- name: Build installers
|
- name: Build installers
|
||||||
shell: bash
|
shell: cmd
|
||||||
run: bun run --filter gridline-desktop tauri build
|
run: |
|
||||||
|
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
|
||||||
|
bun run --filter gridline-desktop tauri build
|
||||||
|
|
||||||
- name: Create release and upload Windows assets
|
- name: Create release and upload Windows assets
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -227,13 +251,13 @@ jobs:
|
|||||||
AUTH="Authorization: token ${GITHUB_TOKEN}"
|
AUTH="Authorization: token ${GITHUB_TOKEN}"
|
||||||
|
|
||||||
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
REL_ID=$(curl -fsS -H "$AUTH" "${API}/releases/tags/${TAG}" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 || true)
|
||||||
if [ -z "$REL_ID" ]; then
|
if [ -z "$REL_ID" ]; then
|
||||||
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
BODY="Gridline ${TAG#v} — Linux and Windows installers."
|
||||||
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
REL_ID=$(curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Gridline ${TAG#v}\",\"body\":\"${BODY}\",\"draft\":true}" \
|
||||||
"${API}/releases" \
|
"${API}/releases" \
|
||||||
| python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
BUNDLE="${GITHUB_WORKSPACE}/desktop/src-tauri/target/release/bundle"
|
||||||
|
|||||||
@@ -30,16 +30,16 @@
|
|||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.11**):
|
Grab the installer for your OS from the [latest release](https://git.ranio.xyz/adrianbonpin/gridline/releases/latest) — the links below point at the current release (**v0.7.12**):
|
||||||
|
|
||||||
| OS | Architecture | Download |
|
| OS | Architecture | Download |
|
||||||
| :--------------------------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
| :--------------------------- | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.11_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_aarch64.dmg) |
|
| **macOS** | Apple Silicon (M1/M2/M3/M4…) | [Gridline_0.7.12_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_aarch64.dmg) |
|
||||||
| **macOS** | Intel | [Gridline_0.7.11_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64.dmg) |
|
| **macOS** | Intel | [Gridline_0.7.12_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64.dmg) |
|
||||||
| **Windows** | x64 | [Gridline_0.7.11_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64-setup.exe) |
|
| **Windows** | x64 | [Gridline_0.7.12_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64-setup.exe) |
|
||||||
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.11_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.deb) |
|
| **Debian / Ubuntu** | amd64 | [Gridline_0.7.12_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.deb) |
|
||||||
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.11-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline-0.7.11-1.x86_64.rpm) |
|
| **Fedora / RHEL / openSUSE** | x86_64 | [Gridline-0.7.12-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline-0.7.12-1.x86_64.rpm) |
|
||||||
| **Other Linux** | amd64 | [Gridline_0.7.11_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.AppImage) |
|
| **Other Linux** | amd64 | [Gridline_0.7.12_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.AppImage) |
|
||||||
|
|
||||||
**macOS via Homebrew** (recommended for Mac users):
|
**macOS via Homebrew** (recommended for Mac users):
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ The cask strips the macOS quarantine flag automatically during install, so **no
|
|||||||
> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
|
> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.11
|
MAINTENANCE: These links are STATIC (versioned) — they point at the v0.7.12
|
||||||
release assets, not at a moving "latest" target. On every new release,
|
release assets, not at a moving "latest" target. On every new release,
|
||||||
update BOTH tables here (Download + Which file should I download?) to the
|
update BOTH tables here (Download + Which file should I download?) to the
|
||||||
new version's asset names, which are tauri-action's default naming:
|
new version's asset names, which are tauri-action's default naming:
|
||||||
@@ -99,6 +99,8 @@ Gridline is built for developers and small teams who manage multiple database en
|
|||||||
<details>
|
<details>
|
||||||
<summary>View the full changelog</summary>
|
<summary>View the full changelog</summary>
|
||||||
|
|
||||||
|
- **v0.7.12** — **Reliable cell editing for non-text columns**: editing a PostgreSQL cell whose type isn't `text` (integers, booleans, UUIDs, `json`/`jsonb`, timestamps, enums, numerics, …) previously failed with a Rust serialization error; edited values are now sent to PostgreSQL in text wire format so the server parses them into the column's own type. **Recent connections fix**: the Recent strip no longer renders empty on first launch when the connections list hadn't loaded yet. **Full CI release pipeline for Windows**: self-hosted Windows runner builds the `.exe`/`.msi` installers (bundled `pg_dump`/`pg_restore`/`psql` + MariaDB clients) and uploads them to the release alongside Linux; version bump.
|
||||||
|
|
||||||
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
|
- **v0.7.10** — **Copy connection URL**: from a connection card's ⋮ menu, copy the connection string for use in env vars / other tools — **Copy connection URL** (includes the password, fetched from the OS keychain on demand) and **Copy connection URL (no password)** (safe to share). Builds `postgresql://` / `mysql://` / `sqlite://` / `redis://` strings with percent-encoded credentials and the PG `sslmode` appended; version bump.
|
||||||
|
|
||||||
- **v0.7.9** — **macOS install/launch fixes**: the app's local store now opens under the OS app-data directory (a cwd-relative `gridline.db` made Finder-launched copies silently exit with a Rust panic before the UI started); macOS bundles are **ad-hoc signed at build time** (`signingIdentity "-"`), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusals) — users now get a standard one-time Gatekeeper prompt (see the [install notes](#installers-are-unsigned-for-now), incl. the `xattr` workaround); **OpenSSL is vendored** into the `ssh2` build so the release no longer depends on a Homebrew OpenSSL path; version bump.
|
- **v0.7.9** — **macOS install/launch fixes**: the app's local store now opens under the OS app-data directory (a cwd-relative `gridline.db` made Finder-launched copies silently exit with a Rust panic before the UI started); macOS bundles are **ad-hoc signed at build time** (`signingIdentity "-"`), replacing the Xcode linker-only signature that macOS treated as unsigned ("damaged and can't be opened", silent Finder refusals) — users now get a standard one-time Gatekeeper prompt (see the [install notes](#installers-are-unsigned-for-now), incl. the `xattr` workaround); **OpenSSL is vendored** into the `ssh2` build so the release no longer depends on a Homebrew OpenSSL path; version bump.
|
||||||
@@ -326,16 +328,16 @@ Code signing **will be added in the future** (Apple Developer Program + a Window
|
|||||||
|
|
||||||
#### Which file should I download?
|
#### Which file should I download?
|
||||||
|
|
||||||
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.11**):
|
Each release contains **one file per platform** — you only need the one that matches your computer. The links below point at the current release (**v0.7.12**):
|
||||||
|
|
||||||
| Your system | Download this | Notes |
|
| Your system | Download this | Notes |
|
||||||
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- |
|
||||||
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.11_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_aarch64.dmg) | `aarch64` = Apple's own chip |
|
| macOS **Apple Silicon** (M1/M2/M3/M4…) | [Gridline_0.7.12_aarch64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_aarch64.dmg) | `aarch64` = Apple's own chip |
|
||||||
| macOS **Intel** | [Gridline_0.7.11_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64.dmg) | `x64` = Intel/AMD |
|
| macOS **Intel** | [Gridline_0.7.12_x64.dmg](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64.dmg) | `x64` = Intel/AMD |
|
||||||
| **Windows** (most PCs) | [Gridline_0.7.11_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
| **Windows** (most PCs) | [Gridline_0.7.12_x64-setup.exe](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_x64-setup.exe) | The `.msi` is an alternate installer (for enterprises/IT admins) |
|
||||||
| **Debian / Ubuntu** | [Gridline_0.7.11_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.11_amd64.deb` |
|
| **Debian / Ubuntu** | [Gridline_0.7.12_amd64.deb](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.deb) | Install: `sudo apt install ./Gridline_0.7.12_amd64.deb` |
|
||||||
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.11-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline-0.7.11-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.11-1.x86_64.rpm` |
|
| **Fedora / RHEL / openSUSE** | [Gridline-0.7.12-1.x86_64.rpm](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline-0.7.12-1.x86_64.rpm) | Install: `sudo dnf install Gridline-0.7.12-1.x86_64.rpm` |
|
||||||
| **Any other Linux** | [Gridline_0.7.11_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.11/Gridline_0.7.11_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
| **Any other Linux** | [Gridline_0.7.12_amd64.AppImage](https://git.ranio.xyz/adrianbonpin/gridline/releases/download/v0.7.12/Gridline_0.7.12_amd64.AppImage) | Works on every distro: `chmod +x` the file, then double-click it |
|
||||||
|
|
||||||
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
|
**Not sure if your Mac is Intel or Apple Silicon?** Click the **Apple menu** → **About This Mac**. If it shows "Apple M1/M2/M3/M4…" download the `aarch64` file; if it shows an Intel chip, download `x64`. Downloading the wrong one won't run.
|
||||||
|
|
||||||
@@ -345,8 +347,8 @@ Cutting a release is one command — CI builds everything. **Releases are cut fr
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout prod && git pull
|
git checkout prod && git pull
|
||||||
git tag v0.7.11
|
git tag v0.7.12
|
||||||
git push origin v0.7.11
|
git push origin v0.7.12
|
||||||
```
|
```
|
||||||
|
|
||||||
Gitea Actions (`.gitea/workflows/release.yml`) builds the **Linux and Windows** installers on the self-hosted runners, then opens a **draft release** on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page. **macOS is built manually** on the dev Mac and its DMGs uploaded to the same release via `scripts/release-mac.sh`. Review the draft and hit **Publish release**.
|
Gitea Actions (`.gitea/workflows/release.yml`) builds the **Linux and Windows** installers on the self-hosted runners, then opens a **draft release** on the [Releases](https://git.ranio.xyz/adrianbonpin/gridline/releases) page. **macOS is built manually** on the dev Mac and its DMGs uploaded to the same release via `scripts/release-mac.sh`. Review the draft and hit **Publish release**.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gridline-desktop",
|
"name": "gridline-desktop",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.11",
|
"version": "0.7.12",
|
||||||
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
"description": "An open-source, high-performance database GUI client for PostgreSQL and beyond",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+2
-1
@@ -1783,8 +1783,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.10"
|
version = "0.7.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"deadpool-postgres",
|
"deadpool-postgres",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridline"
|
name = "gridline"
|
||||||
version = "0.7.11"
|
version = "0.7.12"
|
||||||
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
description = "An open-source, high-performance database GUI client for PostgreSQL and beyond"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -31,6 +31,7 @@ tauri-plugin-dialog = "2"
|
|||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
|
||||||
|
bytes = "1"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "tls-rustls", "json"] }
|
||||||
redis = { version = "0.27", features = ["tokio-comp"] }
|
redis = { version = "0.27", features = ["tokio-comp"] }
|
||||||
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
# async-ssh2 is not available at v0.4; using synchronous ssh2 via tokio::task::spawn_blocking per plan's fallback
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ use crate::models::db_viewer::{
|
|||||||
Change, ColumnInfo, ConstraintInfo, EnumInfo, ExtensionInfo, FunctionInfo, IndexInfo,
|
Change, ColumnInfo, ConstraintInfo, EnumInfo, ExtensionInfo, FunctionInfo, IndexInfo,
|
||||||
QueryResult, SequenceInfo, TableInfo, TriggerInfo,
|
QueryResult, SequenceInfo, TableInfo, TriggerInfo,
|
||||||
};
|
};
|
||||||
|
use bytes::BytesMut;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
use sqlx::Row;
|
use sqlx::Row;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio_postgres::types::ToSql;
|
use tokio_postgres::types::{Format, IsNull, ToSql, Type};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helper functions
|
// Helper functions
|
||||||
@@ -585,26 +587,75 @@ pub fn build_pg_insert_sql(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A value bound to a PostgreSQL parameter in TEXT wire format, accepting any
|
||||||
|
/// parameter type.
|
||||||
|
///
|
||||||
|
/// The frontend ships edited values as plain strings (and PK locators as
|
||||||
|
/// numbers/booleans), but binding a native `String` fails for every non-text
|
||||||
|
/// column type — `String::accepts` only matches `varchar`/`text`/`bpchar`/
|
||||||
|
/// `name` — with `error serializing parameter N: cannot convert between the
|
||||||
|
/// Rust type ... and the Postgres type ...`. Binding a native `i64` is also
|
||||||
|
/// wrong because `i64::accepts` only matches `int8`.
|
||||||
|
///
|
||||||
|
/// `PgText` accepts *any* parameter type and writes the value as UTF-8 text in
|
||||||
|
/// text wire format, so PostgreSQL itself parses the literal into the target
|
||||||
|
/// column type. This works uniformly for `integer`, `bigint`, `numeric`,
|
||||||
|
/// `boolean`, `uuid`, `json`/`jsonb`, timestamps, enums, etc. — exactly what
|
||||||
|
/// PostgreSQL does for an `'<literal>'` in a query.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PgText(String);
|
||||||
|
|
||||||
|
impl From<&str> for PgText {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
PgText(s.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for PgText {
|
||||||
|
fn to_sql(
|
||||||
|
&self,
|
||||||
|
_ty: &Type,
|
||||||
|
out: &mut BytesMut,
|
||||||
|
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
|
||||||
|
out.extend_from_slice(self.0.as_bytes());
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accepts(_ty: &Type) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_format(&self, _ty: &Type) -> Format {
|
||||||
|
Format::Text
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_sql_checked(
|
||||||
|
&self,
|
||||||
|
ty: &Type,
|
||||||
|
out: &mut BytesMut,
|
||||||
|
) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
|
||||||
|
self.to_sql(ty, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Convert a JSON value into a boxed PostgreSQL-bindable value.
|
/// Convert a JSON value into a boxed PostgreSQL-bindable value.
|
||||||
///
|
///
|
||||||
/// Maps common JSON types to `tokio_postgres::types::ToSql` implementors.
|
/// Every non-null value is bound as a `PgText` (text wire format) so the server
|
||||||
/// Unknown/complex types are stringified as a fallback.
|
/// parses it into the target column type; SQL `NULL` is bound as `None`. This
|
||||||
|
/// is deliberately type-agnostic — the caller already knows the column type
|
||||||
|
/// through the SQL context (e.g. `SET "col" = $1` lets PostgreSQL infer `$1`
|
||||||
|
/// as `col`'s type).
|
||||||
fn pg_box_value(v: &serde_json::Value) -> Box<dyn ToSql + Send + Sync> {
|
fn pg_box_value(v: &serde_json::Value) -> Box<dyn ToSql + Send + Sync> {
|
||||||
match v {
|
if v.is_null() {
|
||||||
serde_json::Value::Null => Box::new(Option::<String>::None),
|
return Box::new(Option::<PgText>::None);
|
||||||
serde_json::Value::Bool(b) => Box::new(*b),
|
|
||||||
serde_json::Value::Number(n) => {
|
|
||||||
if let Some(i) = n.as_i64() {
|
|
||||||
Box::new(i)
|
|
||||||
} else if let Some(f) = n.as_f64() {
|
|
||||||
Box::new(f)
|
|
||||||
} else {
|
|
||||||
Box::new(n.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::Value::String(s) => Box::new(s.clone()),
|
|
||||||
other => Box::new(other.to_string()),
|
|
||||||
}
|
}
|
||||||
|
let text = match v {
|
||||||
|
serde_json::Value::String(s) => s.clone(),
|
||||||
|
serde_json::Value::Bool(b) => b.to_string(),
|
||||||
|
serde_json::Value::Number(n) => n.to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
Box::new(PgText::from(text.as_str()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a JSON value into a `rusqlite::types::Value` for SQLite binding.
|
/// Convert a JSON value into a `rusqlite::types::Value` for SQLite binding.
|
||||||
@@ -3320,6 +3371,78 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// PgText: universal text-format parameter binding (serialization fix)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_accepts_any_postgres_type() {
|
||||||
|
// String::accepts only matches text-ish types, so a plain String can't
|
||||||
|
// bind to an integer/boolean/uuid/jsonb column. PgText must accept all
|
||||||
|
// of these (the server parses the text into the column's own type).
|
||||||
|
for ty in [
|
||||||
|
Type::TEXT,
|
||||||
|
Type::VARCHAR,
|
||||||
|
Type::INT4,
|
||||||
|
Type::INT8,
|
||||||
|
Type::FLOAT8,
|
||||||
|
Type::NUMERIC,
|
||||||
|
Type::BOOL,
|
||||||
|
Type::UUID,
|
||||||
|
Type::JSON,
|
||||||
|
Type::JSONB,
|
||||||
|
Type::TIMESTAMP,
|
||||||
|
Type::TIMESTAMPTZ,
|
||||||
|
Type::DATE,
|
||||||
|
Type::TIME,
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
PgText::accepts(&ty),
|
||||||
|
"PgText must accept {:?}",
|
||||||
|
ty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Sanity: a native String must NOT accept a non-text type (proving the
|
||||||
|
// root cause this fix addresses).
|
||||||
|
assert!(!<&str as ToSql>::accepts(&Type::INT4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_serializes_as_utf8_text() {
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use tokio_postgres::types::IsNull;
|
||||||
|
|
||||||
|
let val = pg_box_value(&serde_json::json!("42"));
|
||||||
|
let mut out = BytesMut::new();
|
||||||
|
let isnull = val.to_sql_checked(&Type::INT4, &mut out).unwrap();
|
||||||
|
assert!(matches!(isnull, IsNull::No));
|
||||||
|
assert_eq!(&out[..], b"42");
|
||||||
|
// Text wire format: the bytes are raw UTF-8, not binary-encoded int.
|
||||||
|
assert!(matches!(val.encode_format(&Type::INT4), Format::Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_boxes_null_to_sql_null() {
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use tokio_postgres::types::IsNull;
|
||||||
|
|
||||||
|
let val = pg_box_value(&serde_json::Value::Null);
|
||||||
|
let mut out = BytesMut::new();
|
||||||
|
assert!(matches!(
|
||||||
|
val.to_sql_checked(&Type::TEXT, &mut out).unwrap(),
|
||||||
|
IsNull::Yes
|
||||||
|
));
|
||||||
|
assert!(out.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pg_text_stringifies_booleans_and_numbers() {
|
||||||
|
let b = pg_box_value(&serde_json::json!(true));
|
||||||
|
assert!(matches!(b.encode_format(&Type::BOOL), Format::Text));
|
||||||
|
let n = pg_box_value(&serde_json::json!(5));
|
||||||
|
assert!(matches!(n.encode_format(&Type::INT4), Format::Text));
|
||||||
|
}
|
||||||
|
|
||||||
async fn fresh_pool_manager() -> tokio::sync::Mutex<ConnectionPoolManager> {
|
async fn fresh_pool_manager() -> tokio::sync::Mutex<ConnectionPoolManager> {
|
||||||
tokio::sync::Mutex::new(ConnectionPoolManager::new())
|
tokio::sync::Mutex::new(ConnectionPoolManager::new())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Gridline",
|
"productName": "Gridline",
|
||||||
"version": "0.7.11",
|
"version": "0.7.12",
|
||||||
"identifier": "com.adrianbonpin.gridline",
|
"identifier": "com.adrianbonpin.gridline",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import tauriConf from "../../src-tauri/tauri.conf.json";
|
import tauriConf from "../../src-tauri/tauri.conf.json";
|
||||||
|
|
||||||
describe("tauri bundle config (v0.7.11)", () => {
|
describe("tauri bundle config (v0.7.12)", () => {
|
||||||
it("declares bundled pg_tools resources", () => {
|
it("declares bundled pg_tools resources", () => {
|
||||||
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
expect(tauriConf.bundle.resources).toContain("resources/pg_tools/*");
|
||||||
});
|
});
|
||||||
it("version is 0.7.11", () => {
|
it("version is 0.7.12", () => {
|
||||||
expect(tauriConf.version).toBe("0.7.11");
|
expect(tauriConf.version).toBe("0.7.12");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4,7 +4,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import agents from "../../../AGENTS.md?raw";
|
import agents from "../../../AGENTS.md?raw";
|
||||||
import readme from "../../../README.md?raw";
|
import readme from "../../../README.md?raw";
|
||||||
|
|
||||||
describe("v0.7.11 docs coverage", () => {
|
describe("v0.7.12 docs coverage", () => {
|
||||||
it("AGENTS.md marks inline cell editing complete", () => {
|
it("AGENTS.md marks inline cell editing complete", () => {
|
||||||
expect(agents).toContain("Inline cell editing");
|
expect(agents).toContain("Inline cell editing");
|
||||||
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
expect(agents).toMatch(/Inline cell editing \| ✅/);
|
||||||
@@ -24,8 +24,8 @@ describe("v0.7.11 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
expect(agents).toMatch(/Connection status indicator on cards \| ✅/);
|
||||||
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
expect(agents).toMatch(/Move-to-folder bulk action \| ✅/);
|
||||||
});
|
});
|
||||||
it("README declares v0.7.11", () => {
|
it("README declares v0.7.12", () => {
|
||||||
expect(readme).toContain("0.7.11");
|
expect(readme).toContain("0.7.12");
|
||||||
});
|
});
|
||||||
it("AGENTS.md marks schema CRUD complete", () => {
|
it("AGENTS.md marks schema CRUD complete", () => {
|
||||||
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
expect(agents).toMatch(/Schema CRUD \| ✅/);
|
||||||
@@ -64,9 +64,9 @@ describe("v0.7.11 docs coverage", () => {
|
|||||||
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
expect(agents).toMatch(/Cancel long-running queries \| ✅/);
|
||||||
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
expect(agents).toMatch(/Settings export\/import \| ✅/);
|
||||||
});
|
});
|
||||||
it("README links to v0.7.11 assets in both download tables", () => {
|
it("README links to v0.7.12 assets in both download tables", () => {
|
||||||
expect(readme).toContain("releases/download/v0.7.11/");
|
expect(readme).toContain("releases/download/v0.7.12/");
|
||||||
expect(readme).toContain("Gridline_0.7.11_aarch64.dmg");
|
expect(readme).toContain("Gridline_0.7.12_aarch64.dmg");
|
||||||
expect(readme).toContain("Gridline-0.7.11-1.x86_64.rpm");
|
expect(readme).toContain("Gridline-0.7.12-1.x86_64.rpm");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||||||
import pkg from "../../package.json";
|
import pkg from "../../package.json";
|
||||||
|
|
||||||
describe("version", () => {
|
describe("version", () => {
|
||||||
it("declares v0.7.11 across the app shell", () => {
|
it("declares v0.7.12 across the app shell", () => {
|
||||||
expect(pkg.version).toBe("0.7.11");
|
expect(pkg.version).toBe("0.7.12");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -225,6 +225,33 @@ describe("favorites / recents / move-selection", () => {
|
|||||||
expect(commands.recordRecentConnection).toHaveBeenCalledWith("c1");
|
expect(commands.recordRecentConnection).toHaveBeenCalledWith("c1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loadAll refreshes recents once connections are known (startup race fix)", async () => {
|
||||||
|
const conn = makeConn({ id: "c1", name: "Prod" });
|
||||||
|
vi.spyOn(commands, "getConnections").mockResolvedValue([conn]);
|
||||||
|
vi.spyOn(commands, "getFolders").mockResolvedValue([]);
|
||||||
|
vi.spyOn(commands, "getTags").mockResolvedValue([]);
|
||||||
|
vi.spyOn(commands, "getRecentConnections").mockResolvedValue([
|
||||||
|
{ connection_id: "c1", opened_at: "2026-08-02T00:00:00Z" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simulate the app-shell mount: loadAll fires and populates connections.
|
||||||
|
await useConnectionStore.getState().loadAll();
|
||||||
|
|
||||||
|
// The recent strip must now show the connection that was opened previously.
|
||||||
|
expect(commands.getRecentConnections).toHaveBeenCalledWith(8);
|
||||||
|
expect(useConnectionStore.getState().recent).toEqual([conn]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loadRecent maps cached recent ids against the current connection list", async () => {
|
||||||
|
const conn = makeConn({ id: "c1", name: "Prod" });
|
||||||
|
useConnectionStore.setState({ connections: [conn] });
|
||||||
|
vi.spyOn(commands, "getRecentConnections").mockResolvedValue([
|
||||||
|
{ connection_id: "c1", opened_at: "2026-08-02T00:00:00Z" },
|
||||||
|
]);
|
||||||
|
await useConnectionStore.getState().loadRecent();
|
||||||
|
expect(useConnectionStore.getState().recent).toEqual([conn]);
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplicateConnection", () => {
|
describe("duplicateConnection", () => {
|
||||||
it("copies fields with a '(copy)' name and favorite false", async () => {
|
it("copies fields with a '(copy)' name and favorite false", async () => {
|
||||||
const src = makeConn({ id: "c1", name: "Prod", folder_id: "f1", tag_ids: ["t1"], favorite: true });
|
const src = makeConn({ id: "c1", name: "Prod", folder_id: "f1", tag_ids: ["t1"], favorite: true });
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ export const useConnectionStore = create<ConnectionState>((set, get) => ({
|
|||||||
set((s) => ({ connections: [...s.connections].sort((a, b) => Number(b.favorite) - Number(a.favorite)) }));
|
set((s) => ({ connections: [...s.connections].sort((a, b) => Number(b.favorite) - Number(a.favorite)) }));
|
||||||
// Also load tag order
|
// Also load tag order
|
||||||
get().loadTagOrder();
|
get().loadTagOrder();
|
||||||
|
// Refresh recents once connections are known so the recent strip is not
|
||||||
|
// computed against an empty list (avoids a startup race with loadRecent).
|
||||||
|
get().loadRecent().catch(() => {});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set({ loading: false, error: e instanceof Error ? e.message : String(e) });
|
set({ loading: false, error: e instanceof Error ? e.message : String(e) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import queryEditor from "../assets/screenshots/query-editor.png";
|
|||||||
import erDiagram from "../assets/screenshots/er-diagram.png";
|
import erDiagram from "../assets/screenshots/er-diagram.png";
|
||||||
import home from "../assets/screenshots/home.png";
|
import home from "../assets/screenshots/home.png";
|
||||||
import dataGrid from "../assets/screenshots/data-grid.png";
|
import dataGrid from "../assets/screenshots/data-grid.png";
|
||||||
const version = "0.7.11";
|
const version = "0.7.12";
|
||||||
const site = "https://getgridline.app";
|
const site = "https://getgridline.app";
|
||||||
const jsonLd = [
|
const jsonLd = [
|
||||||
buildSoftwareApplicationJsonLd({
|
buildSoftwareApplicationJsonLd({
|
||||||
|
|||||||
Reference in New Issue
Block a user