diff --git a/GEMINI.md b/.github/GEMINI.md
similarity index 100%
rename from GEMINI.md
rename to .github/GEMINI.md
diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml
index ac95286..c0c73f1 100644
--- a/.github/workflows/flutter-build.yml
+++ b/.github/workflows/flutter-build.yml
@@ -24,11 +24,6 @@ jobs:
- name: Install Linux build and test dependencies
run: sudo apt-get update && sudo apt-get install --yes clang cmake ninja-build pkg-config libgtk-3-dev libreoffice-calc xvfb
- - name: Set up Nim
- uses: jiro4989/setup-nim-action@v2
- with:
- nim-version: stable
-
- name: Resolve locked DecentDB version
id: decentdb-version
shell: bash
@@ -56,28 +51,19 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- - name: Check out DecentDB ${{ steps.decentdb-version.outputs.tag }}
- run: git clone --depth 1 --branch "${{ steps.decentdb-version.outputs.tag }}" https://github.com/sphildreth/decentdb.git ../decentdb
-
- - name: Build libpg_query (Linux)
- working-directory: ../decentdb
+ - name: Install pinned DecentDB native library
shell: bash
run: |
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- make -C /tmp/libpg_query
- fi
- mkdir -p build/lib
- cp /tmp/libpg_query/libpg_query.a build/lib/
-
- - name: Install DecentDB Nim dependencies
- working-directory: ../decentdb
- run: nimble setup -y
-
- - name: Build DecentDB native library
- working-directory: ../decentdb
- shell: bash
- run: nimble build_lib
+ set -euo pipefail
+ tag="${{ steps.decentdb-version.outputs.tag }}"
+ asset="decentdb-dart-native-${tag}-Linux-x64.tar.gz"
+ url="https://github.com/sphildreth/decentdb/releases/download/${tag}/${asset}"
+ work_dir="$RUNNER_TEMP/decentdb-native"
+ archive_path="$RUNNER_TEMP/$asset"
+ mkdir -p "$work_dir"
+ curl -fsSL "$url" -o "$archive_path"
+ tar -xzf "$archive_path" -C "$work_dir"
+ sudo install -m 0644 "$work_dir/libdecentdb.so" /usr/local/lib/libdecentdb.so
- name: Flutter pub get
working-directory: apps/decent-bench
@@ -93,7 +79,36 @@ jobs:
- name: Flutter integration test
working-directory: apps/decent-bench
- run: xvfb-run -a flutter test integration_test
+ shell: bash
+ run: |
+ set -euo pipefail
+ display_file="$(mktemp)"
+ Xvfb -displayfd 3 -screen 0 1600x1000x24 -ac 3>"$display_file" >"$RUNNER_TEMP/xvfb.log" 2>&1 &
+ xvfb_pid=$!
+ cleanup() {
+ kill "$xvfb_pid" 2>/dev/null || true
+ wait "$xvfb_pid" 2>/dev/null || true
+ rm -f "$display_file"
+ }
+ trap cleanup EXIT
+ for _ in $(seq 1 100); do
+ if [ -s "$display_file" ]; then
+ break
+ fi
+ sleep 0.1
+ done
+ export DISPLAY=":$(cat "$display_file")"
+ # Run integration tests; retry once on non-zero exit to handle the
+ # known Linux desktop shutdown crash where all test assertions pass
+ # but the binary exits non-zero during cleanup.
+ set +e
+ flutter test integration_test
+ it_exit=$?
+ set -e
+ if [ "$it_exit" -ne 0 ]; then
+ echo "::warning::Integration test exited with $it_exit on first attempt; retrying..."
+ flutter test integration_test
+ fi
desktop-package-verify:
runs-on: ${{ matrix.os }}
@@ -104,15 +119,20 @@ jobs:
- os: ubuntu-latest
flutter_target: linux
bundle_path: build/linux/x64/release/bundle
- native_lib: ../../../decentdb/build/libdecentdb.so
+ decentdb_release_suffix: Linux-x64
+ native_archive_extension: tar.gz
+ native_lib_name: libdecentdb.so
- os: macos-latest
flutter_target: macos
bundle_path: build/macos/Build/Products/Release/decent_bench.app
- native_lib: ../../../decentdb/build/libdecentdb.dylib
+ native_archive_extension: tar.gz
+ native_lib_name: libdecentdb.dylib
- os: windows-latest
flutter_target: windows
bundle_path: build/windows/x64/runner/Release
- native_lib: ../../../decentdb/build/c_api.dll
+ decentdb_release_suffix: Windows-x64
+ native_archive_extension: zip
+ native_lib_name: decentdb.dll
steps:
- name: Check out Decent Bench
@@ -128,11 +148,6 @@ jobs:
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install --yes clang cmake ninja-build pkg-config libgtk-3-dev
- - name: Set up Nim
- uses: jiro4989/setup-nim-action@v2
- with:
- nim-version: stable
-
- name: Resolve locked DecentDB version
id: decentdb-version
shell: bash
@@ -160,56 +175,42 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- - name: Check out DecentDB ${{ steps.decentdb-version.outputs.tag }}
- run: git clone --depth 1 --branch "${{ steps.decentdb-version.outputs.tag }}" https://github.com/sphildreth/decentdb.git ../decentdb
-
- - name: Build libpg_query (Linux)
- if: runner.os == 'Linux'
- working-directory: ../decentdb
- shell: bash
- run: |
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- make -C /tmp/libpg_query
- fi
- mkdir -p build/lib
- cp /tmp/libpg_query/libpg_query.a build/lib/
-
- - name: Build libpg_query (macOS)
- if: runner.os == 'macOS'
- working-directory: ../decentdb
+ - name: Download DecentDB Dart-native asset (Linux/macOS)
+ if: runner.os != 'Windows'
shell: bash
run: |
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- make -C /tmp/libpg_query
+ set -euo pipefail
+ tag="${{ steps.decentdb-version.outputs.tag }}"
+ # Use the matrix-provided suffix when available; for macOS the
+ # suffix is derived at runtime from RUNNER_ARCH so the workflow
+ # stays correct if GitHub changes the runner architecture.
+ release_suffix="${{ matrix.decentdb_release_suffix }}"
+ if [[ -z "$release_suffix" ]]; then
+ arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
+ release_suffix="${RUNNER_OS}-${arch}"
fi
- mkdir -p build/lib
- cp /tmp/libpg_query/libpg_query.a build/lib/
-
- - name: Build libpg_query (Windows)
+ asset="decentdb-dart-native-${tag}-${release_suffix}.${{ matrix.native_archive_extension }}"
+ url="https://github.com/sphildreth/decentdb/releases/download/${tag}/${asset}"
+ work_dir="$RUNNER_TEMP/decentdb-native"
+ archive_path="$RUNNER_TEMP/$asset"
+ mkdir -p "$work_dir"
+ curl -fsSL "$url" -o "$archive_path"
+ tar -xzf "$archive_path" -C "$work_dir"
+ echo "DECENTDB_NATIVE_PATH=$work_dir/${{ matrix.native_lib_name }}" >> "$GITHUB_ENV"
+
+ - name: Download DecentDB Dart-native asset (Windows)
if: runner.os == 'Windows'
- working-directory: ../decentdb
- shell: bash
+ shell: pwsh
run: |
- repo_root="$PWD"
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- cd /tmp/libpg_query
- make
- fi
- cd /tmp/libpg_query
- mkdir -p "$repo_root/build/lib"
- cp libpg_query.a "$repo_root/build/lib/"
-
- - name: Install DecentDB Nim dependencies
- working-directory: ../decentdb
- run: nimble setup -y
-
- - name: Build DecentDB native library
- working-directory: ../decentdb
- shell: bash
- run: nimble build_lib
+ $tag = "${{ steps.decentdb-version.outputs.tag }}"
+ $asset = "decentdb-dart-native-$tag-${{ matrix.decentdb_release_suffix }}.${{ matrix.native_archive_extension }}"
+ $url = "https://github.com/sphildreth/decentdb/releases/download/$tag/$asset"
+ $workDir = Join-Path $env:RUNNER_TEMP "decentdb-native"
+ $archivePath = Join-Path $env:RUNNER_TEMP $asset
+ New-Item -ItemType Directory -Force $workDir | Out-Null
+ Invoke-WebRequest -Uri $url -OutFile $archivePath
+ Expand-Archive -Path $archivePath -DestinationPath $workDir -Force
+ Add-Content -Path $env:GITHUB_ENV -Value "DECENTDB_NATIVE_PATH=$workDir\\${{ matrix.native_lib_name }}"
- name: Flutter pub get
working-directory: apps/decent-bench
@@ -249,7 +250,7 @@ jobs:
- name: Stage DecentDB native library into bundle
working-directory: apps/decent-bench
- run: dart run tool/stage_decentdb_native.dart --bundle "${{ matrix.bundle_path }}" --source "${{ matrix.native_lib }}"
+ run: dart run tool/stage_decentdb_native.dart --bundle "${{ matrix.bundle_path }}" --source "${{ env.DECENTDB_NATIVE_PATH }}"
- name: Verify bundled native library
working-directory: apps/decent-bench
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ad81d9c..3e673fa 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -22,17 +22,20 @@ jobs:
- os: ubuntu-latest
flutter_target: linux
bundle_path: build/linux/x64/release/bundle
- native_lib: ../../../decentdb/build/libdecentdb.so
+ native_archive_extension: tar.gz
+ native_lib_name: libdecentdb.so
archive_extension: tar.gz
- os: macos-latest
flutter_target: macos
bundle_path: build/macos/Build/Products/Release/decent_bench.app
- native_lib: ../../../decentdb/build/libdecentdb.dylib
+ native_archive_extension: tar.gz
+ native_lib_name: libdecentdb.dylib
archive_extension: tar.gz
- os: windows-latest
flutter_target: windows
bundle_path: build/windows/x64/runner/Release
- native_lib: ../../../decentdb/build/decentdb.dll
+ native_archive_extension: zip
+ native_lib_name: decentdb.dll
archive_extension: zip
steps:
@@ -45,15 +48,38 @@ jobs:
channel: stable
cache: true
+ - name: Resolve release platform metadata
+ id: release-meta
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ case "$RUNNER_ARCH" in
+ X64) arch_suffix=x64 ;;
+ ARM64) arch_suffix=arm64 ;;
+ *)
+ echo "Unsupported runner architecture: $RUNNER_ARCH" >&2
+ exit 1
+ ;;
+ esac
+
+ case "$RUNNER_OS" in
+ Linux) decentdb_release_suffix="Linux-$arch_suffix" ;;
+ macOS) decentdb_release_suffix="macOS-$arch_suffix" ;;
+ Windows) decentdb_release_suffix="Windows-$arch_suffix" ;;
+ *)
+ echo "Unsupported runner OS: $RUNNER_OS" >&2
+ exit 1
+ ;;
+ esac
+
+ echo "app_arch_suffix=$arch_suffix" >> "$GITHUB_OUTPUT"
+ echo "decentdb_release_suffix=$decentdb_release_suffix" >> "$GITHUB_OUTPUT"
+
- name: Install Linux desktop build dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install --yes clang cmake ninja-build pkg-config libgtk-3-dev
- - name: Set up Nim
- uses: jiro4989/setup-nim-action@v2
- with:
- nim-version: stable
-
- name: Resolve locked DecentDB version
id: decentdb-version
shell: bash
@@ -81,56 +107,34 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- - name: Check out DecentDB ${{ steps.decentdb-version.outputs.tag }}
- run: git clone --depth 1 --branch "${{ steps.decentdb-version.outputs.tag }}" https://github.com/sphildreth/decentdb.git ../decentdb
-
- - name: Build libpg_query (Linux)
- if: runner.os == 'Linux'
- working-directory: ../decentdb
- shell: bash
- run: |
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- make -C /tmp/libpg_query
- fi
- mkdir -p build/lib
- cp /tmp/libpg_query/libpg_query.a build/lib/
-
- - name: Build libpg_query (macOS)
- if: runner.os == 'macOS'
- working-directory: ../decentdb
+ - name: Download DecentDB Dart-native asset (Linux/macOS)
+ if: runner.os != 'Windows'
shell: bash
run: |
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- make -C /tmp/libpg_query
- fi
- mkdir -p build/lib
- cp /tmp/libpg_query/libpg_query.a build/lib/
-
- - name: Build libpg_query (Windows)
+ set -euo pipefail
+ tag="${{ steps.decentdb-version.outputs.tag }}"
+ asset="decentdb-dart-native-${tag}-${{ steps.release-meta.outputs.decentdb_release_suffix }}.${{ matrix.native_archive_extension }}"
+ url="https://github.com/sphildreth/decentdb/releases/download/${tag}/${asset}"
+ work_dir="$RUNNER_TEMP/decentdb-native"
+ archive_path="$RUNNER_TEMP/$asset"
+ mkdir -p "$work_dir"
+ curl -fsSL "$url" -o "$archive_path"
+ tar -xzf "$archive_path" -C "$work_dir"
+ echo "DECENTDB_NATIVE_PATH=$work_dir/${{ matrix.native_lib_name }}" >> "$GITHUB_ENV"
+
+ - name: Download DecentDB Dart-native asset (Windows)
if: runner.os == 'Windows'
- working-directory: ../decentdb
- shell: bash
+ shell: pwsh
run: |
- repo_root="$PWD"
- if [ ! -d "/tmp/libpg_query" ]; then
- git clone --depth 1 https://github.com/pganalyze/libpg_query.git /tmp/libpg_query
- cd /tmp/libpg_query
- make
- fi
- cd /tmp/libpg_query
- mkdir -p "$repo_root/build/lib"
- cp libpg_query.a "$repo_root/build/lib/"
-
- - name: Install DecentDB Nim dependencies
- working-directory: ../decentdb
- run: nimble setup -y
-
- - name: Build DecentDB native library
- working-directory: ../decentdb
- shell: bash
- run: nimble build_lib
+ $tag = "${{ steps.decentdb-version.outputs.tag }}"
+ $asset = "decentdb-dart-native-$tag-${{ steps.release-meta.outputs.decentdb_release_suffix }}.${{ matrix.native_archive_extension }}"
+ $url = "https://github.com/sphildreth/decentdb/releases/download/$tag/$asset"
+ $workDir = Join-Path $env:RUNNER_TEMP "decentdb-native"
+ $archivePath = Join-Path $env:RUNNER_TEMP $asset
+ New-Item -ItemType Directory -Force $workDir | Out-Null
+ Invoke-WebRequest -Uri $url -OutFile $archivePath
+ Expand-Archive -Path $archivePath -DestinationPath $workDir -Force
+ Add-Content -Path $env:GITHUB_ENV -Value "DECENTDB_NATIVE_PATH=$workDir\\${{ matrix.native_lib_name }}"
- name: Flutter pub get
working-directory: ${{ env.APP_DIR }}
@@ -199,11 +203,11 @@ jobs:
shell: bash
run: |
set -euo pipefail
- echo "name=decent-bench-${GITHUB_REF_NAME}-${RUNNER_OS}-x64.${{ matrix.archive_extension }}" >> "$GITHUB_OUTPUT"
+ echo "name=decent-bench-${GITHUB_REF_NAME}-${RUNNER_OS}-${{ steps.release-meta.outputs.app_arch_suffix }}.${{ matrix.archive_extension }}" >> "$GITHUB_OUTPUT"
- name: Stage DecentDB native library into bundle
working-directory: ${{ env.APP_DIR }}
- run: dart run tool/stage_decentdb_native.dart --bundle "${{ steps.bundle.outputs.path }}" --source "${{ matrix.native_lib }}"
+ run: dart run tool/stage_decentdb_native.dart --bundle "${{ steps.bundle.outputs.path }}" --source "${{ env.DECENTDB_NATIVE_PATH }}"
- name: Verify bundled native library
working-directory: ${{ env.APP_DIR }}
@@ -216,7 +220,7 @@ jobs:
run: |
set -euo pipefail
stage_root="$RUNNER_TEMP/release-stage"
- package_root="decent-bench-${GITHUB_REF_NAME}-${RUNNER_OS}-x64"
+ package_root="decent-bench-${GITHUB_REF_NAME}-${RUNNER_OS}-${{ steps.release-meta.outputs.app_arch_suffix }}"
stage_dir="$stage_root/$package_root"
bundle_path="${{ steps.bundle.outputs.path }}"
@@ -237,7 +241,7 @@ jobs:
shell: pwsh
run: |
$stageRoot = Join-Path $env:RUNNER_TEMP "release-stage"
- $packageRoot = "decent-bench-$env:GITHUB_REF_NAME-$env:RUNNER_OS-x64"
+ $packageRoot = "decent-bench-$env:GITHUB_REF_NAME-$env:RUNNER_OS-${{ steps.release-meta.outputs.app_arch_suffix }}"
$stageDir = Join-Path $stageRoot $packageRoot
$bundlePath = "${{ steps.bundle.outputs.path }}"
@@ -253,6 +257,7 @@ jobs:
with:
name: release-${{ matrix.os }}
path: ${{ env.APP_DIR }}/${{ steps.artifact.outputs.name }}
+ if-no-files-found: error
release:
name: Create GitHub Release
@@ -272,7 +277,8 @@ jobs:
tag_name: ${{ github.ref_name }}
prerelease: ${{ contains(github.ref_name, '-') }}
generate_release_notes: true
+ fail_on_unmatched_files: true
files: |
- artifacts/**/decent-bench-${{ github.ref_name }}-Linux-x64.tar.gz
- artifacts/**/decent-bench-${{ github.ref_name }}-macOS-x64.tar.gz
- artifacts/**/decent-bench-${{ github.ref_name }}-Windows-x64.zip
+ artifacts/**/decent-bench-${{ github.ref_name }}-Linux-*.tar.gz
+ artifacts/**/decent-bench-${{ github.ref_name }}-macOS-*.tar.gz
+ artifacts/**/decent-bench-${{ github.ref_name }}-Windows-*.zip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94beb32..f61bbc0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,38 @@ This file records notable project changes. It follows the
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format and uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+## [1.1.0] - 2026-04-21
+
+### Added
+
+- Added DecentDB native asset staging and improved native-library resolution so
+ desktop builds and packaged runners can acquire and load the engine more
+ reliably across environments.
+- Added headless CLI import mode plus broader import detection for wrapped
+ inputs, including additional archive handling and initial MS SQL Server
+ backup (`.bak`) workflow scaffolding.
+- Added `Tools -> View Log` to open the DecentDB-backed application log
+ database from inside the workspace.
+
+### Changed
+
+- Expanded import workflow coverage in the app, docs, ADRs, and tests to cover
+ the broader supported import scope and native/runtime packaging decisions.
+- Improved SQLite import summaries to show richer validation detail, including
+ imported object counts, per-table row totals, target file size, and WAL
+ status after import finalization.
+
+### Fixed
+
+- Fixed SQLite import handling for high-precision timestamp text and mixed
+ date-like columns so large real-world databases such as Navidrome import
+ correctly.
+- Fixed import finalization so successful imports checkpoint and flush their
+ WAL state before completion, avoiding misleading tiny database files paired
+ with large sidecar WAL files.
+- Fixed import failure UX so failed imports stop the workflow, surface a clear
+ blocking error dialog with summary and details, and no longer look like a
+ successful completion.
## [1.0.0] - 2026-03-14
@@ -29,5 +60,6 @@ This file records notable project changes. It follows the
metadata, bundled theme compatibility ranges, and project documentation with
that release line.
-[unreleased]: https://github.com/sphildreth/decent-bench/compare/v1.0.0...HEAD
+[unreleased]: https://github.com/sphildreth/decent-bench/compare/v1.1.0...HEAD
+[1.1.0]: https://github.com/sphildreth/decent-bench/releases/tag/v1.1.0
[1.0.0]: https://github.com/sphildreth/decent-bench/releases/tag/v1.0.0
diff --git a/README.md b/README.md
index 2f6b74e..8b9ff67 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
Features •
- Status •
+ Status •
Getting Started •
Developer Onboarding •
Roadmap •
@@ -44,18 +44,17 @@
## ✨ Features
- 🚀 **DecentDB-First:** A fully local-first workflow. Fast open/create, recent files, and intuitive drag-and-drop support.
-- 📥 **Smart Import Wizards:** Seamlessly import CSV, JSON, XML, HTML, SQLite, Excel, and SQL dumps (including `.zip`/`.gz` archives). Includes previews, rename/type-override transforms, progress reporting, and summary actions.
+- 📥 **Smart Import Wizards:** Import delimited text, JSON/NDJSON, XML, HTML tables, Excel, SQLite, SQL dumps, and wrapped archives (`.zip`, `.gz`, `.bz2`). Includes previews, rename/type-override transforms, progress reporting, and post-import summaries.
- 🛠️ **Modern SQL Workbench:** Iterate in a multi-tab editor with isolated per-tab results, schema-aware autocomplete, editable snippets, and deterministic formatting.
- ⚡ **Performance-Focused:** Background imports, paginated/streamed results grids, and best-effort query cancellation ensure the UI never freezes.
- 🧭 **Rich Engine Metadata:** Schema browsing is powered by DecentDB's rich
upstream schema snapshot (tables/views/indexes/triggers, checks, foreign keys,
generated columns, temp-object metadata, and canonical DDL).
-- 🎨 **Workspace Persistence:** Config and app state are safely stored as TOML, providing reliable per-database workspace restoration.
+- 🎨 **Workspace Persistence:** Application preferences are stored as TOML, and per-database workspace state is stored separately for reliable tab and query restoration.
+- 🪵 **Operational Visibility:** Open the DecentDB-backed application log database directly from `Tools -> View Log`.
+- 🧪 **Import Validation:** Blocking failure dialogs and richer import summaries make unsuccessful imports obvious and successful imports easier to verify.
- 📦 **Desktop Native:** Packaged for Linux, macOS, and Windows with a repeatable native-library staging helper.
-## 🚀 Status: v1.0.0
-
-**Decent Bench has reached 1.0.0!** The core MVP loop is fully implemented and stable: import or open a database, inspect schema, run SQL, stream/page through results, and export to CSV.
### Supported File Types
@@ -63,25 +62,44 @@
| --- | --- | --- |
| `.ddb` | **Open directly** | Main DecentDB workspace format. |
| `.db`, `.sqlite`, `.sqlite3` | **Import Wizard** | Background import with schema preview and table selection. |
-| `.csv`, `.tsv`, `.txt`, `.dat`, `.psv` | **Import Wizard** | Delimited text import with header, delimiter, quoting, preview, and type overrides. |
+| `.csv`, `.tsv` | **Import Wizard** | CSV/TSV import through the generic delimited-text pipeline. |
+| `.txt`, `.dat`, `.log`, `.psv` | **Import Wizard** | Generic delimited-text import with header, delimiter, quoting, malformed-row, preview, and type-override controls. |
| `.json`, `.ndjson`, `.jsonl` | **Import Wizard** | Structured and line-oriented JSON import with relational previews. |
| `.xml` | **Import Wizard** | XML import with flatten or parent-child normalization strategies. |
| `.html`, `.htm` | **Import Wizard** | HTML table extraction with table selection and header inference. |
| `.xlsx` | **Import Wizard** | Select worksheets and map DecentDB types automatically. |
-| `.sql` | **Import Wizard** | Supports common MariaDB/MySQL-style dumps (CREATE TABLE + INSERT). |
-| `.zip`, `.gz` | **Unwrap & Import** | Archive wrappers that automatically unwrap supported files and route them to the import flow. |
-| `.xls` | *Partial / Hint* | Legacy format. Handled with warnings or prompts to convert to `.xlsx` first. |
+| `.xls` | *Partial / Warning Path* | Routed through the legacy Excel path and may require conversion/normalization warnings. |
+| `.sql` | **Import Wizard** | Supports the current MariaDB/MySQL-style MVP-lite dump scope (`CREATE TABLE` plus common `INSERT ... VALUES`). |
+| `.zip` | **Unwrap & Import** | Archive wrapper that discovers supported inner files and routes them to the normal import flow. |
+| `.gz`, `.tar.gz`, `.tgz` | **Unwrap & Import** | Supports single-file gzip unwrap and tar+gzip archive inspection/extraction. |
+| `.bz2`, `.tar.bz2`, `.tbz2` | **Unwrap & Import** | Supports single-file bzip2 unwrap and tar+bzip2 archive inspection/extraction. |
+| `.bak` | **Environment-Gated / Scaffold** | Detected and routed to the MS SQL backup flow, which currently checks Docker availability and surfaces the planned container-assisted import path. |
+
+### Recognized But Not Yet Implemented
+
+The current build recognizes, but does not yet import, several formats and
+wrappers including `.ods`, `.yaml`, `.yml`, `.toml`, `.md`, `.duckdb`,
+`.mdb`, `.accdb`, `.dbf`, `.parquet`, `.pdf`, and `.xz`.
## 🚀 Getting Started (End Users)
-*Binary releases for Linux, macOS, and Windows will be available on the [Releases](https://github.com/sphildreth/decent-bench/releases) page.*
+*Binary releases for Linux, macOS, and Windows are listed on the [Releases](https://github.com/sphildreth/decent-bench/releases) page.*
### Command-line Launch
-Packaged desktop builds expose a narrow CLI entry for import flows:
+Packaged desktop builds expose a small CLI surface for direct-open and import
+flows:
```bash
+dbench /path/to/workspace.ddb
dbench --import /path/to/source.xlsx
+dbench --in /path/to/source.sqlite --out /tmp/import.ddb
```
-This reuses the drag-and-drop detection rules and instantly opens the right import wizard.
+
+- Passing a `.ddb` path opens that workspace directly.
+- `--import` reuses drag-and-drop detection rules and opens the matching wizard.
+- `--in` / `--out` run the shipped headless import path.
+- `--silent` suppresses headless progress output.
+- `--plan` is parsed but intentionally rejected for now; it is reserved for a
+ future plan-file execution flow.
## 💻 Developer Onboarding
@@ -92,6 +110,14 @@ Want to build from source or contribute? Welcome!
- **Git**
- **Flutter** (stable, desktop tooling enabled)
- OS-specific native toolchain (C++ compiler, etc.)
+- `tar` on systems where you want tar+gzip or tar+bzip2 archive detection and extraction
+- A matching **DecentDB native library** for the pinned app version
+
+Decent Bench pins the upstream Dart package by Git tag and expects the matching
+DecentDB desktop native library alongside it. CI and release packaging resolve
+that version from `apps/decent-bench/pubspec.lock` and download the matching
+`decentdb-dart-native--...` asset from
+[`sphildreth/decentdb` Releases](https://github.com/sphildreth/decentdb/releases).
### 1. Bootstrap the Flutter App
```bash
@@ -99,33 +125,56 @@ cd apps/decent-bench
flutter pub get
```
-### 2. Run Locally
+### 2. Install the Matching DecentDB Native Library
+The app and test tooling resolve the pinned `decentdb` tag from
+`apps/decent-bench/pubspec.lock` and download the matching
+`decentdb-dart-native--...` release asset from DecentDB Releases into a
+local cache when needed. The app still prefers a bundled native library first,
+then the cached pinned asset, then common system locations.
+
+### 3. Run Locally
```bash
flutter run -d linux
```
-### 3. Testing & Validation
+### 4. Testing & Validation
```bash
flutter analyze
flutter test
flutter test integration_test
```
-### 4. Packaging Desktop Builds
-Build the bundle, then use the staging helper to inject the DecentDB native library:
+These commands will fetch the matching pinned DecentDB native library on first
+use if it is not already cached.
+
+### 5. Packaging Desktop Builds
+Build the bundle, then use the staging helper to inject the DecentDB native
+library. The `--source` path can point at either an extracted
+`decentdb-dart-native--...` release asset or a local DecentDB build:
```bash
flutter build linux
dart run tool/stage_decentdb_native.dart --bundle build/linux/x64/release/bundle
```
-*(For macOS use `build/macos/Build/Products/Release/decent_bench.app` and Windows `build/windows/x64/runner/Release`)*.
+If `--source` is omitted, the helper resolves and downloads the pinned matching
+DecentDB native release asset automatically.
+
+*(For macOS use `build/macos/Build/Products/Release/decent_bench.app` and
+Windows `build/windows/x64/runner/Release`.)*
## 🏗️ Architecture & Configuration
-The application stores global configuration and workspace state locally:
+The application stores its local state under the platform app-support
+directory:
- **Linux:** `~/.config/decent-bench/`
- **macOS:** `~/Library/Application Support/Decent Bench/`
- **Windows:** `%APPDATA%\Decent Bench\`
+Typical files under that root include:
+- `config.toml` for application preferences
+- per-workspace `.json` state files for saved tabs/history/restoration
+- `decent-bench-log.ddb` for the application log database opened by
+ `Tools -> View Log`
+
**Project Source of Truth:**
- 📐 [`design/PRD.md`](design/PRD.md) — Product goals and user journeys
- 📝 [`design/SPEC.md`](design/SPEC.md) — Implementation scope (Authoritative)
@@ -134,20 +183,24 @@ The application stores global configuration and workspace state locally:
## 🗺️ Roadmap
-**Completed for 1.0.0:**
+**Shipped through 1.1.0:**
- ✅ Drag-and-drop open/import flows
- ✅ Expansive import support: CSV, JSON, XML, HTML, SQLite, Excel, and SQL dumps
-- ✅ ZIP & GZip archive wrappers for imports
+- ✅ ZIP, GZip, and BZip2 wrapper routing for imports
+- ✅ Headless CLI import mode
+- ✅ DecentDB native asset staging and pinned runtime resolution
+- ✅ In-app application log viewing
+- ✅ Clear blocking import-failure dialogs and richer import summaries
- ✅ Schema browsing and multi-tab SQL editing
- ✅ Autocomplete, snippets, and deterministic formatter
- ✅ Paged results, query cancellation, and CSV export
-- ✅ TOML config and persistent per-database workspaces
+- ✅ Local app config plus persistent per-database workspaces
-**Coming Next (Post-1.0):**
+**Coming Next (Post-1.1):**
- 🔜 **Expanded Exports:** JSON, Parquet, and Excel formats, plus schema exports and reusable export recipes.
-- 🔜 **New Database & Analytical Imports:** DuckDB, Parquet, PostgreSQL dumps, and legacy DBs (Access, DBF).
+- 🔜 **New Database & Analytical Imports:** DuckDB, Parquet, broader PostgreSQL dump handling, and legacy DBs (Access, DBF).
- 🔜 **New Document & Log Imports:** OpenDocument (`.ods`), YAML, Markdown/PDF tables, and continuous log streams.
-- 🔜 **Advanced Import Capabilities:** Computed-column transforms and native legacy binary `.xls` parsing.
+- 🔜 **Advanced Import Capabilities:** Computed-column transforms, native legacy binary `.xls` parsing, and full MS SQL `.bak` restore/extract execution.
## 🤝 Contributing
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 9c8d321..3b0e92b 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -6,7 +6,8 @@ Apache 2.0 distribution. This file tracks attributions and license details.
## Dependencies
- `decentdb`
- - Version/source: local path dependency at `../decentdb/bindings/dart/dart`
+ - Version/source: Git dependency from `https://github.com/sphildreth/decentdb`,
+ path `bindings/dart/dart`, ref `v2.3.0`
- License: Apache License 2.0
- Upstream project: `https://github.com/sphildreth/decentdb`
@@ -44,3 +45,20 @@ Apache 2.0 distribution. This file tracks attributions and license details.
- License: MIT
- Copyright: Lukas Renggli
- Source: `https://pub.dev/packages/xml`
+
+- `path` `^1.9.0`
+ - License: BSD-style license (Dart project)
+ - Copyright: Dart project authors
+ - Source: `https://pub.dev/packages/path`
+ - Note: `path` is a Dart SDK team package; attribution included for
+ completeness.
+
+## Transitive dependency notes
+
+The following transitive dependencies are brought in by direct dependencies.
+Their licenses are compatible with Apache 2.0 distribution:
+
+- `archive` brings `crypto` (MIT, Dart project authors) and `convert`
+ (BSD-style, Dart project authors)
+- `sqlite3` brings `collection` and `meta` (BSD-style, Dart project authors)
+- `excel` brings `equatable` (MIT), `ffi` (BSD-style), and `recase` (MIT)
diff --git a/apps/decent-bench/README.md b/apps/decent-bench/README.md
index 346262b..1946c01 100644
--- a/apps/decent-bench/README.md
+++ b/apps/decent-bench/README.md
@@ -1,7 +1,7 @@
# Decent Bench App
This directory contains the shipped Flutter desktop app source for Decent
-Bench `1.0.0`, which is the project's MVP release.
+Bench `1.1.0`, which builds on the project's shipped `1.0.0` MVP release.
## Current state
@@ -18,8 +18,9 @@ Bench `1.0.0`, which is the project's MVP release.
- CSV, TSV, generic delimited text, JSON, NDJSON/JSONL, XML, HTML tables, and
ZIP/GZip wrapper routing now use the generic import preview/execution path
- desktop runner folders (`linux/`, `macos/`, `windows/`) are checked in
-- the DecentDB Dart package is consumed from GitHub releases
- (`https://github.com/sphildreth/decentdb`)
+- the DecentDB Dart package is pinned from the upstream Git tag
+ (`https://github.com/sphildreth/decentdb`), and desktop packaging stages the
+ matching `decentdb-dart-native--...` release asset
- Excel import currently supports `.xlsx`; legacy `.xls` files route through
the existing conversion/normalization path and remain explicitly partial
- SQL dump import currently targets the MVP-lite parser scope documented in
@@ -56,6 +57,11 @@ The app expects a compatible DecentDB v2.x native library to be available via:
1. System library paths (`/usr/local/lib/`, `~/.local/lib/`)
2. Bundled with the app
+CI, local tests, and Linux desktop builds resolve the pinned `decentdb` tag from
+`pubspec.lock` and use the matching `decentdb-dart-native--...` asset from
+DecentDB Releases. You can still override the source library explicitly with
+`tool/stage_decentdb_native.dart --source ` when needed.
+
Workspace tab drafts are stored separately from `config.toml` under the
platform-specific `workspaces/` directory documented in the root
[README.md](/home/steven/source/decent-bench/README.md).
diff --git a/apps/decent-bench/integration_test/workspace_shell_test.dart b/apps/decent-bench/integration_test/workspace_shell_test.dart
index 3aeaabd..bcb0749 100644
--- a/apps/decent-bench/integration_test/workspace_shell_test.dart
+++ b/apps/decent-bench/integration_test/workspace_shell_test.dart
@@ -6,6 +6,7 @@ import 'package:decent_bench/app/startup_launch_options.dart';
import 'package:archive/archive.dart';
import 'package:decent_bench/features/workspace/application/workspace_controller.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -139,10 +140,10 @@ void main() {
final tempDir = await Directory.systemTemp.createTemp('decent-bench-it-');
final zipPath = p.join(tempDir.path, 'bundle.zip');
final archive = Archive()
- ..addFile(
- ArchiveFile('customers.csv', 14, 'id,name\n1,Ada\n'.codeUnits),
- );
- await File(zipPath).writeAsBytes(ZipEncoder().encode(archive)!, flush: true);
+ ..addFile(ArchiveFile('customers.csv', 14, 'id,name\n1,Ada\n'.codeUnits));
+ await File(
+ zipPath,
+ ).writeAsBytes(ZipEncoder().encode(archive)!, flush: true);
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = const Size(1600, 1000);
@@ -169,4 +170,213 @@ void main() {
expect(find.text('ZIP Wrapper Contents'), findsOneWidget);
expect(find.textContaining('customers.csv'), findsOneWidget);
});
+
+ testWidgets('creates and switches between multiple editor tabs', (
+ tester,
+ ) async {
+ final controller = WorkspaceController(
+ gateway: FakeWorkspaceGateway(),
+ configStore: InMemoryConfigStore(),
+ workspaceStateStore: InMemoryWorkspaceStateStore(),
+ );
+
+ tester.view.devicePixelRatio = 1;
+ tester.view.physicalSize = const Size(1600, 1000);
+ addTearDown(() {
+ tester.view.resetPhysicalSize();
+ tester.view.resetDevicePixelRatio();
+ controller.dispose();
+ });
+
+ await controller.initialize();
+ controller.createTab(sql: 'SELECT 1');
+ controller.createTab(sql: 'SELECT 2');
+
+ await tester.pumpWidget(
+ DecentBenchApp(
+ controller: controller,
+ autoInitialize: false,
+ logger: const NoOpAppLogger(),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Query'), findsAtLeastNWidgets(3));
+ });
+
+ testWidgets('opens preferences dialog from Options menu', (tester) async {
+ final controller = WorkspaceController(
+ gateway: FakeWorkspaceGateway(),
+ configStore: InMemoryConfigStore(),
+ workspaceStateStore: InMemoryWorkspaceStateStore(),
+ );
+ final messenger =
+ TestDefaultBinaryMessengerBinding
+ .instance
+ .defaultBinaryMessenger;
+
+ tester.view.devicePixelRatio = 1;
+ tester.view.physicalSize = const Size(1600, 1000);
+ addTearDown(() {
+ tester.view.resetPhysicalSize();
+ tester.view.resetDevicePixelRatio();
+ messenger.setMockMethodCallHandler(SystemChannels.menu, null);
+ controller.dispose();
+ });
+
+ messenger.setMockMethodCallHandler(SystemChannels.menu, (call) async {
+ if (call.method == 'Menu.isPluginAvailable') {
+ return false;
+ }
+ return null;
+ });
+
+ await controller.initialize();
+ await tester.pumpWidget(
+ DecentBenchApp(
+ controller: controller,
+ autoInitialize: false,
+ logger: const NoOpAppLogger(),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.text('Tools'), findsOneWidget);
+
+ await tester.tap(find.text('Tools'));
+ await tester.pump(const Duration(milliseconds: 200));
+
+ expect(
+ find.widgetWithText(MenuItemButton, 'Options / Preferences'),
+ findsOneWidget,
+ );
+
+ await tester.tap(find.widgetWithText(MenuItemButton, 'Options / Preferences'));
+ await tester.pumpAndSettle();
+
+ expect(find.text('Options / Preferences'), findsOneWidget);
+ expect(find.textContaining('Theme'), findsWidgets);
+ expect(find.textContaining('Editor'), findsWidgets);
+ });
+
+ testWidgets('launches JSON import wizard from startup options', (
+ tester,
+ ) async {
+ final controller = WorkspaceController(
+ gateway: FakeWorkspaceGateway(),
+ configStore: InMemoryConfigStore(),
+ workspaceStateStore: InMemoryWorkspaceStateStore(),
+ );
+ final tempDir = await Directory.systemTemp.createTemp('decent-bench-it-');
+ final jsonPath = p.join(tempDir.path, 'data.json');
+ await File(
+ jsonPath,
+ ).writeAsString('[{"id": 1, "name": "Ada"}, {"id": 2, "name": "Lin"}]');
+
+ tester.view.devicePixelRatio = 1;
+ tester.view.physicalSize = const Size(1600, 1000);
+ addTearDown(() async {
+ tester.view.resetPhysicalSize();
+ tester.view.resetDevicePixelRatio();
+ controller.dispose();
+ if (await tempDir.exists()) {
+ await tempDir.delete(recursive: true);
+ }
+ });
+
+ await controller.initialize();
+ await tester.pumpWidget(
+ DecentBenchApp(
+ controller: controller,
+ autoInitialize: false,
+ logger: const NoOpAppLogger(),
+ startupLaunchOptions: StartupLaunchOptions(importSourcePath: jsonPath),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Import Wizard'), findsOneWidget);
+ expect(find.textContaining('Tables: 1'), findsOneWidget);
+ });
+
+ testWidgets('launches XML import wizard from startup options', (
+ tester,
+ ) async {
+ final controller = WorkspaceController(
+ gateway: FakeWorkspaceGateway(),
+ configStore: InMemoryConfigStore(),
+ workspaceStateStore: InMemoryWorkspaceStateStore(),
+ );
+ final tempDir = await Directory.systemTemp.createTemp('decent-bench-it-');
+ final xmlPath = p.join(tempDir.path, 'catalog.xml');
+ await File(xmlPath).writeAsString(
+ ''
+ '- 1Widget
'
+ '- 2Gadget
',
+ );
+
+ tester.view.devicePixelRatio = 1;
+ tester.view.physicalSize = const Size(1600, 1000);
+ addTearDown(() async {
+ tester.view.resetPhysicalSize();
+ tester.view.resetDevicePixelRatio();
+ controller.dispose();
+ if (await tempDir.exists()) {
+ await tempDir.delete(recursive: true);
+ }
+ });
+
+ await controller.initialize();
+ await tester.pumpWidget(
+ DecentBenchApp(
+ controller: controller,
+ autoInitialize: false,
+ logger: const NoOpAppLogger(),
+ startupLaunchOptions: StartupLaunchOptions(importSourcePath: xmlPath),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Import Wizard'), findsOneWidget);
+ });
+
+ testWidgets('launches HTML table import wizard from startup options', (
+ tester,
+ ) async {
+ final controller = WorkspaceController(
+ gateway: FakeWorkspaceGateway(),
+ configStore: InMemoryConfigStore(),
+ workspaceStateStore: InMemoryWorkspaceStateStore(),
+ );
+ final tempDir = await Directory.systemTemp.createTemp('decent-bench-it-');
+ final htmlPath = p.join(tempDir.path, 'report.html');
+ await File(htmlPath).writeAsString(
+ '',
+ );
+
+ tester.view.devicePixelRatio = 1;
+ tester.view.physicalSize = const Size(1600, 1000);
+ addTearDown(() async {
+ tester.view.resetPhysicalSize();
+ tester.view.resetDevicePixelRatio();
+ controller.dispose();
+ if (await tempDir.exists()) {
+ await tempDir.delete(recursive: true);
+ }
+ });
+
+ await controller.initialize();
+ await tester.pumpWidget(
+ DecentBenchApp(
+ controller: controller,
+ autoInitialize: false,
+ logger: const NoOpAppLogger(),
+ startupLaunchOptions: StartupLaunchOptions(importSourcePath: htmlPath),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.textContaining('Import Wizard'), findsOneWidget);
+ });
}
diff --git a/apps/decent-bench/lib/app/app_metadata.dart b/apps/decent-bench/lib/app/app_metadata.dart
index c2f7666..ac94c80 100644
--- a/apps/decent-bench/lib/app/app_metadata.dart
+++ b/apps/decent-bench/lib/app/app_metadata.dart
@@ -1,2 +1,2 @@
-const String kDecentBenchVersion = '1.0.0';
+const String kDecentBenchVersion = '1.1.0';
const String kDecentBenchDisplayName = 'Decent Bench';
diff --git a/apps/decent-bench/lib/app/logging/import_log_details.dart b/apps/decent-bench/lib/app/logging/import_log_details.dart
index a2f08fa..f0475ed 100644
--- a/apps/decent-bench/lib/app/logging/import_log_details.dart
+++ b/apps/decent-bench/lib/app/logging/import_log_details.dart
@@ -139,6 +139,12 @@ Map buildSqliteImportSummaryLogDetails(
extra: {
'status_message': summary.statusMessage,
'index_count': summary.indexesCreated.length,
+ 'target_table_count': summary.targetTableCount,
+ 'target_index_count': summary.targetIndexCount,
+ 'target_view_count': summary.targetViewCount,
+ 'target_trigger_count': summary.targetTriggerCount,
+ 'database_file_bytes': summary.databaseFileBytes,
+ 'wal_file_bytes': summary.walFileBytes,
if (summary.indexesCreated.isNotEmpty)
'indexes_created': summary.indexesCreated,
'skipped_item_count': summary.skippedItems.length,
diff --git a/apps/decent-bench/lib/features/import/domain/import_models.dart b/apps/decent-bench/lib/features/import/domain/import_models.dart
index f83173a..92db377 100644
--- a/apps/decent-bench/lib/features/import/domain/import_models.dart
+++ b/apps/decent-bench/lib/features/import/domain/import_models.dart
@@ -56,6 +56,7 @@ enum ImportFormatKey {
duckdb,
access,
dbf,
+ msSqlBak,
sqlDump,
postgresPlainDump,
parquet,
diff --git a/apps/decent-bench/lib/features/import/infrastructure/import_detection_service.dart b/apps/decent-bench/lib/features/import/infrastructure/import_detection_service.dart
index 5546a44..b667376 100644
--- a/apps/decent-bench/lib/features/import/infrastructure/import_detection_service.dart
+++ b/apps/decent-bench/lib/features/import/infrastructure/import_detection_service.dart
@@ -1,3 +1,4 @@
+import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
@@ -31,6 +32,17 @@ class ImportDetectionService {
);
}
if (format.key == ImportFormatKey.gzipArchive) {
+ final ext = p.extension(sourcePath).toLowerCase();
+ final innerName = p.basenameWithoutExtension(sourcePath);
+ if (ext == '.tgz' || _looksLikeTar(innerName)) {
+ return _detectTarCandidates(
+ sourcePath: sourcePath,
+ format: format,
+ innerName: innerName,
+ listArgs: ['-tzf', sourcePath],
+ extractFlag: '-xzf',
+ );
+ }
final candidate = await _detectGzipCandidate(sourcePath);
return ImportDetectionResult(
sourcePath: sourcePath,
@@ -45,6 +57,33 @@ class ImportDetectionService {
: [candidate],
);
}
+ if (format.key == ImportFormatKey.bzip2Archive) {
+ final ext = p.extension(sourcePath).toLowerCase();
+ final innerName = p.basenameWithoutExtension(sourcePath);
+ if (ext == '.tbz2' || _looksLikeTar(innerName)) {
+ return _detectTarCandidates(
+ sourcePath: sourcePath,
+ format: format,
+ innerName: innerName,
+ listArgs: ['-tjf', sourcePath],
+ extractFlag: '-xjf',
+ );
+ }
+ final candidate = await _detectBzip2SingleFileCandidate(sourcePath);
+ return ImportDetectionResult(
+ sourcePath: sourcePath,
+ format: format,
+ warnings: candidate == null
+ ? [
+ 'The BZip2 filename does not indicate a supported inner '
+ 'source.',
+ ]
+ : warnings,
+ archiveCandidates: candidate == null
+ ? const []
+ : [candidate],
+ );
+ }
if (format.key == ImportFormatKey.sqlite && file.existsSync()) {
final header = await file
.openRead(0, 16)
@@ -55,17 +94,109 @@ class ImportDetectionService {
final signature = String.fromCharCodes(header);
if (!signature.startsWith('SQLite format 3')) {
warnings.add(
- 'The file uses a SQLite-like extension, but the header does not match the SQLite signature.',
+ 'The file uses a SQLite-like extension, but the header does not '
+ 'match the SQLite signature.',
+ );
+ }
+ }
+ return ImportDetectionResult(
+ sourcePath: sourcePath,
+ format: format,
+ warnings: warnings,
+ );
+ }
+
+ bool _looksLikeTar(String innerName) {
+ return innerName.toLowerCase().endsWith('.tar');
+ }
+
+ Future _detectTarCandidates({
+ required String sourcePath,
+ required ImportFormatDefinition format,
+ required String innerName,
+ required List listArgs,
+ required String extractFlag,
+ }) async {
+ final warnings = [];
+ final candidates = [];
+
+ try {
+ final result = await Process.run(
+ 'tar',
+ listArgs,
+ stdoutEncoding: utf8,
+ stderrEncoding: utf8,
+ );
+ if (result.exitCode != 0) {
+ warnings.add(
+ 'Failed to list tar archive contents: '
+ '${(result.stderr as String).trim()}',
+ );
+ return ImportDetectionResult(
+ sourcePath: sourcePath,
+ format: format,
+ warnings: warnings,
+ archiveCandidates: candidates,
);
}
+ final lines = LineSplitter.split(
+ result.stdout as String,
+ ).map((line) => line.trim()).where((line) => line.isNotEmpty).toList();
+ for (final entryPath in lines) {
+ if (entryPath.endsWith('/')) {
+ continue;
+ }
+ final innerFormat = _registry.detectByPath(entryPath);
+ if (innerFormat.key != ImportFormatKey.unknown) {
+ candidates.add(
+ ImportArchiveCandidate(
+ entryPath: entryPath,
+ displayName: entryPath,
+ innerFormatKey: innerFormat.key,
+ innerFormatLabel: innerFormat.label,
+ supportState: innerFormat.supportState,
+ ),
+ );
+ continue;
+ }
+ final inferred = _inferFormatForExtensionlessTarEntry(entryPath);
+ candidates.add(
+ ImportArchiveCandidate(
+ entryPath: entryPath,
+ displayName: entryPath,
+ innerFormatKey: inferred.key,
+ innerFormatLabel: inferred.label,
+ supportState: inferred.supportState,
+ ),
+ );
+ }
+ } on ProcessException catch (error) {
+ warnings.add(
+ 'tar command unavailable for archive listing: ${error.message}',
+ );
+ }
+
+ if (candidates.isEmpty) {
+ warnings.add('No importable files were found inside the tar archive.');
}
return ImportDetectionResult(
sourcePath: sourcePath,
format: format,
warnings: warnings,
+ archiveCandidates: candidates,
);
}
+ ImportFormatDefinition _inferFormatForExtensionlessTarEntry(
+ String entryPath,
+ ) {
+ final baseName = p.basename(entryPath);
+ if (baseName.contains('.')) {
+ return _registry.detectByPath(baseName);
+ }
+ return _registry.forKey(ImportFormatKey.tsv);
+ }
+
Future> _detectZipCandidates(
String sourcePath,
) async {
@@ -113,6 +244,23 @@ class ImportDetectionService {
);
}
+ Future _detectBzip2SingleFileCandidate(
+ String sourcePath,
+ ) async {
+ final innerName = p.basenameWithoutExtension(sourcePath);
+ final innerFormat = _registry.detectByPath(innerName);
+ if (innerFormat.key == ImportFormatKey.unknown) {
+ return null;
+ }
+ return ImportArchiveCandidate(
+ entryPath: innerName,
+ displayName: innerName,
+ innerFormatKey: innerFormat.key,
+ innerFormatLabel: innerFormat.label,
+ supportState: innerFormat.supportState,
+ );
+ }
+
Future extractArchiveCandidate({
required String archivePath,
required ImportFormatKey wrapperKey,
@@ -121,8 +269,10 @@ class ImportDetectionService {
final tempDir = await Directory.systemTemp.createTemp(
'decent-bench-import-',
);
- final outputPath = p.join(tempDir.path, p.basename(candidate.entryPath));
+ final entryBaseName = p.basename(candidate.entryPath);
+
if (wrapperKey == ImportFormatKey.zipArchive) {
+ final outputPath = p.join(tempDir.path, entryBaseName);
final archive = ZipDecoder().decodeBytes(
await File(archivePath).readAsBytes(),
);
@@ -136,10 +286,23 @@ class ImportDetectionService {
}
}
throw StateError(
- 'Archive entry `${candidate.entryPath}` was not found in $archivePath.',
+ 'Archive entry `${candidate.entryPath}` was not found in '
+ '$archivePath.',
);
}
+
if (wrapperKey == ImportFormatKey.gzipArchive) {
+ final ext = p.extension(archivePath).toLowerCase();
+ final innerName = p.basenameWithoutExtension(archivePath);
+ if (ext == '.tgz' || _looksLikeTar(innerName)) {
+ return _extractTarEntry(
+ archivePath: archivePath,
+ tempDir: tempDir,
+ entryPath: candidate.entryPath,
+ extractFlag: '-xzf',
+ );
+ }
+ final outputPath = p.join(tempDir.path, entryBaseName);
final decoded = GZipDecoder().decodeBytes(
await File(archivePath).readAsBytes(),
);
@@ -148,6 +311,72 @@ class ImportDetectionService {
output.writeAsBytesSync(decoded, flush: true);
return output.path;
}
+
+ if (wrapperKey == ImportFormatKey.bzip2Archive) {
+ final ext = p.extension(archivePath).toLowerCase();
+ final innerName = p.basenameWithoutExtension(archivePath);
+ if (ext == '.tbz2' || _looksLikeTar(innerName)) {
+ return _extractTarEntry(
+ archivePath: archivePath,
+ tempDir: tempDir,
+ entryPath: candidate.entryPath,
+ extractFlag: '-xjf',
+ );
+ }
+ final outputPath = p.join(tempDir.path, entryBaseName);
+ final decoded = BZip2Decoder().decodeBytes(
+ await File(archivePath).readAsBytes(),
+ );
+ final output = File(outputPath);
+ output.parent.createSync(recursive: true);
+ output.writeAsBytesSync(decoded, flush: true);
+ return output.path;
+ }
+
throw StateError('Unsupported wrapper extraction for ${wrapperKey.name}.');
}
+
+ String _extractTarEntry({
+ required String archivePath,
+ required Directory tempDir,
+ required String entryPath,
+ required String extractFlag,
+ }) {
+ final baseName = p.basename(entryPath);
+ final inferredFormat = _inferFormatForExtensionlessTarEntry(entryPath);
+ final extension = inferredFormat.extensions.isNotEmpty
+ ? inferredFormat.extensions.first
+ : '';
+
+ // Extract by the exact archive-relative path to avoid GNU tar-specific
+ // --no-anchored semantics and basename collisions across subdirectories.
+ final result = Process.runSync(
+ 'tar',
+ [extractFlag, archivePath, '-C', tempDir.path, entryPath],
+ stdoutEncoding: utf8,
+ stderrEncoding: utf8,
+ );
+ if (result.exitCode != 0) {
+ throw StateError(
+ 'Failed to extract `$entryPath` from tar archive: '
+ '${(result.stderr as String).trim()}',
+ );
+ }
+
+ final extractedPath = p.join(tempDir.path, entryPath);
+ final extractedFile = File(extractedPath);
+ if (!extractedFile.existsSync()) {
+ throw StateError(
+ 'Extracted entry not found at expected path `$extractedPath`.',
+ );
+ }
+
+ if (extension.isNotEmpty &&
+ !baseName.toLowerCase().endsWith(extension)) {
+ final renamed = '$extractedPath$extension';
+ extractedFile.renameSync(renamed);
+ return renamed;
+ }
+ return extractedPath;
+ }
}
diff --git a/apps/decent-bench/lib/features/import/infrastructure/import_execution_service.dart b/apps/decent-bench/lib/features/import/infrastructure/import_execution_service.dart
index 8e201ec..ee34192 100644
--- a/apps/decent-bench/lib/features/import/infrastructure/import_execution_service.dart
+++ b/apps/decent-bench/lib/features/import/infrastructure/import_execution_service.dart
@@ -375,6 +375,7 @@ Future _runGenericImport({
database.commit();
transactionOpen = false;
+ database.checkpoint();
return GenericImportSummary(
jobId: request.jobId,
sourcePath: request.sourcePath,
diff --git a/apps/decent-bench/lib/features/import/infrastructure/import_format_registry.dart b/apps/decent-bench/lib/features/import/infrastructure/import_format_registry.dart
index 3891890..a9a02f5 100644
--- a/apps/decent-bench/lib/features/import/infrastructure/import_format_registry.dart
+++ b/apps/decent-bench/lib/features/import/infrastructure/import_format_registry.dart
@@ -190,6 +190,15 @@ class ImportFormatRegistry {
implementationKind: ImportImplementationKind.recognizedUnsupported,
description: 'Legacy DBF database import.',
),
+ ImportFormatDefinition(
+ key: ImportFormatKey.msSqlBak,
+ label: 'MS SQL Server Backup',
+ family: ImportFamily.databaseDump,
+ supportState: ImportSupportState.investigate,
+ extensions: ['.bak'],
+ implementationKind: ImportImplementationKind.recognizedUnsupported,
+ description: 'Container-assisted MS SQL backup import (not yet implemented).',
+ ),
ImportFormatDefinition(
key: ImportFormatKey.sqlDump,
label: 'SQL Dump',
@@ -251,19 +260,22 @@ class ImportFormatRegistry {
label: 'GZip Wrapper',
family: ImportFamily.compressedArchive,
supportState: ImportSupportState.complete,
- extensions: ['.gz'],
+ extensions: ['.gz', '.tgz'],
implementationKind: ImportImplementationKind.wrapper,
description:
'Single-file wrapper that unwraps supported CSV/JSON/NDJSON/XML/HTML/SQL/Excel/SQLite files.',
),
ImportFormatDefinition(
key: ImportFormatKey.bzip2Archive,
- label: 'BZip2 Wrapper',
+ label: 'BZip2 / Tar+BZip2 Wrapper',
family: ImportFamily.compressedArchive,
- supportState: ImportSupportState.investigate,
- extensions: ['.bz2'],
- implementationKind: ImportImplementationKind.recognizedUnsupported,
- description: 'BZip2 compressed wrapper support.',
+ supportState: ImportSupportState.complete,
+ extensions: ['.bz2', '.tbz2'],
+ implementationKind: ImportImplementationKind.wrapper,
+ description:
+ 'BZip2 wrapper for single-file decompression and tar+bzip2 '
+ 'archive extraction. Uses the system tar command for large '
+ 'tar archives.',
),
ImportFormatDefinition(
key: ImportFormatKey.xzArchive,
diff --git a/apps/decent-bench/lib/features/import/presentation/generic_import_dialog.dart b/apps/decent-bench/lib/features/import/presentation/generic_import_dialog.dart
index 476bf1b..4bce85f 100644
--- a/apps/decent-bench/lib/features/import/presentation/generic_import_dialog.dart
+++ b/apps/decent-bench/lib/features/import/presentation/generic_import_dialog.dart
@@ -1,5 +1,6 @@
import 'dart:async';
+import 'package:decent_bench/shared/widgets/import_failure_dialog.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
@@ -79,6 +80,7 @@ class _GenericImportDialogState extends State {
bool _replaceExistingTarget = true;
String? _focusedTableId;
String? _lastSuggestedTargetPath;
+ String? _shownFailureMessage;
int _durationToNanos(Duration duration) => duration.inMicroseconds * 1000;
@@ -165,6 +167,7 @@ class _GenericImportDialogState extends State {
@override
Widget build(BuildContext context) {
+ _maybeShowFailureDialog();
return AlertDialog(
title: Text('${widget.initialFormat.label} Import Wizard'),
content: SizedBox(
@@ -200,6 +203,34 @@ class _GenericImportDialogState extends State {
);
}
+ void _maybeShowFailureDialog() {
+ if (_phase != GenericImportJobPhase.failed ||
+ _step != GenericImportWizardStep.summary ||
+ _error == null) {
+ _shownFailureMessage = null;
+ return;
+ }
+ if (_shownFailureMessage == _error) {
+ return;
+ }
+ _shownFailureMessage = _error;
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted || _error == null) {
+ return;
+ }
+ showImportFailureDialog(
+ context: context,
+ title: '${widget.initialFormat.label} import failed',
+ message: _error!,
+ onAcknowledged: () {
+ if (mounted) {
+ Navigator.of(context).pop();
+ }
+ },
+ );
+ });
+ }
+
Widget _buildStepHeader() {
return Wrap(
spacing: 8,
@@ -807,9 +838,17 @@ class _GenericImportDialogState extends State {
Widget _buildSummaryStep() {
if (_summary == null) {
- return const _EmptyState(
- title: 'No summary yet',
- message: 'Run the import to populate the summary view.',
+ return _EmptyState(
+ title: _phase == GenericImportJobPhase.failed
+ ? 'Import failed'
+ : _phase == GenericImportJobPhase.cancelled
+ ? 'Import cancelled'
+ : 'No summary yet',
+ message:
+ _error ??
+ (_phase == GenericImportJobPhase.cancelled
+ ? 'The import was cancelled before a summary was produced.'
+ : 'Run the import to populate the summary view.'),
);
}
return ListView(
@@ -907,7 +946,7 @@ class _GenericImportDialogState extends State {
GenericImportWizardStep.preview => 'Next',
GenericImportWizardStep.transforms => 'Next',
GenericImportWizardStep.execute => 'Run Import',
- GenericImportWizardStep.summary => 'Done',
+ GenericImportWizardStep.summary => _summary == null ? 'Close' : 'Done',
};
}
@@ -955,7 +994,11 @@ class _GenericImportDialogState extends State {
await _runImport();
break;
case GenericImportWizardStep.summary:
- if (!mounted || _summary == null) {
+ if (!mounted) {
+ return;
+ }
+ if (_summary == null) {
+ Navigator.of(context).pop();
return;
}
Navigator.of(context).pop(
@@ -1199,6 +1242,7 @@ class _GenericImportDialogState extends State {
_step = GenericImportWizardStep.summary;
});
case GenericImportUpdateKind.failed:
+ final message = update.message ?? 'The import failed.';
_logError(
'run_generic_import',
'Generic import failed.',
@@ -1210,7 +1254,7 @@ class _GenericImportDialogState extends State {
'format_key': request.formatKey.name,
'format_label': widget.initialFormat.label,
'selected_table_count': request.selectedTables.length,
- 'message': update.message,
+ 'message': message,
},
);
if (!mounted) {
@@ -1218,7 +1262,8 @@ class _GenericImportDialogState extends State {
}
setState(() {
_phase = GenericImportJobPhase.failed;
- _error = update.message ?? 'The import failed.';
+ _step = GenericImportWizardStep.summary;
+ _error = message;
});
}
});
diff --git a/apps/decent-bench/lib/features/import/utils/docker_cli.dart b/apps/decent-bench/lib/features/import/utils/docker_cli.dart
new file mode 100644
index 0000000..11b84a9
--- /dev/null
+++ b/apps/decent-bench/lib/features/import/utils/docker_cli.dart
@@ -0,0 +1,12 @@
+import 'dart:io';
+
+class DockerCli {
+ static Future isDockerAvailable() async {
+ try {
+ final result = await Process.run('docker', ['info']);
+ return result.exitCode == 0;
+ } catch (_) {
+ return false;
+ }
+ }
+}
diff --git a/apps/decent-bench/lib/features/workspace/application/workspace_controller.dart b/apps/decent-bench/lib/features/workspace/application/workspace_controller.dart
index 450cdbd..02866b3 100644
--- a/apps/decent-bench/lib/features/workspace/application/workspace_controller.dart
+++ b/apps/decent-bench/lib/features/workspace/application/workspace_controller.dart
@@ -382,6 +382,11 @@ class WorkspaceController extends ChangeNotifier {
}
}
+ Future openLogDatabase() async {
+ await _logger.initialize(minimumLevel: config.logging.verbosity);
+ await openDatabase(_logger.logDatabasePath, createIfMissing: false);
+ }
+
Future refreshSchema({bool showLoadingState = true}) async {
if (!hasOpenDatabase) {
return;
@@ -1842,11 +1847,14 @@ class WorkspaceController extends ChangeNotifier {
);
break;
case ExcelImportUpdateKind.failed:
+ final message = update.message ?? 'Excel import failed.';
excelImportSession = current.copyWith(
step: ExcelImportWizardStep.summary,
phase: ExcelImportJobPhase.failed,
- error: update.message ?? 'Excel import failed.',
+ error: message,
);
+ workspaceError = message;
+ workspaceMessage = null;
_logError(
'run_excel_import',
'Excel import failed.',
@@ -1857,7 +1865,7 @@ class WorkspaceController extends ChangeNotifier {
'source_path': current.sourcePath,
'target_path': current.targetPath,
'selected_sheet_count': current.selectedSheets.length,
- 'message': update.message,
+ 'message': message,
},
);
break;
@@ -2328,11 +2336,14 @@ class WorkspaceController extends ChangeNotifier {
);
break;
case SqlDumpImportUpdateKind.failed:
+ final message = update.message ?? 'SQL dump import failed.';
sqlDumpImportSession = current.copyWith(
step: SqlDumpImportWizardStep.summary,
phase: SqlDumpImportJobPhase.failed,
- error: update.message ?? 'SQL dump import failed.',
+ error: message,
);
+ workspaceError = message;
+ workspaceMessage = null;
_logError(
'run_sql_dump_import',
'SQL dump import failed.',
@@ -2343,7 +2354,7 @@ class WorkspaceController extends ChangeNotifier {
'source_path': current.sourcePath,
'target_path': current.targetPath,
'selected_table_count': current.selectedTables.length,
- 'message': update.message,
+ 'message': message,
},
);
break;
@@ -2844,11 +2855,14 @@ class WorkspaceController extends ChangeNotifier {
);
break;
case SqliteImportUpdateKind.failed:
+ final message = update.message ?? 'SQLite import failed.';
sqliteImportSession = current.copyWith(
step: SqliteImportWizardStep.summary,
phase: SqliteImportJobPhase.failed,
- error: update.message ?? 'SQLite import failed.',
+ error: message,
);
+ workspaceError = message;
+ workspaceMessage = null;
_logError(
'run_sqlite_import',
'SQLite import failed.',
@@ -2859,7 +2873,7 @@ class WorkspaceController extends ChangeNotifier {
'source_path': current.sourcePath,
'target_path': current.targetPath,
'selected_table_count': current.selectedTables.length,
- 'message': update.message,
+ 'message': message,
},
);
break;
@@ -3362,6 +3376,10 @@ class WorkspaceController extends ChangeNotifier {
error: message,
phase: phase ?? session.phase,
);
+ if ((phase ?? session.phase) == SqlDumpImportJobPhase.failed) {
+ workspaceError = message;
+ workspaceMessage = null;
+ }
_safeNotify();
_logError(
'sql_dump_import_error',
@@ -3387,6 +3405,10 @@ class WorkspaceController extends ChangeNotifier {
error: message,
phase: phase ?? session.phase,
);
+ if ((phase ?? session.phase) == ExcelImportJobPhase.failed) {
+ workspaceError = message;
+ workspaceMessage = null;
+ }
_safeNotify();
_logError(
'excel_import_error',
@@ -3412,6 +3434,10 @@ class WorkspaceController extends ChangeNotifier {
error: message,
phase: phase ?? session.phase,
);
+ if ((phase ?? session.phase) == SqliteImportJobPhase.failed) {
+ workspaceError = message;
+ workspaceMessage = null;
+ }
_safeNotify();
_logError(
'sqlite_import_error',
diff --git a/apps/decent-bench/lib/features/workspace/domain/sqlite_import_models.dart b/apps/decent-bench/lib/features/workspace/domain/sqlite_import_models.dart
index 59d68aa..2f3e126 100644
--- a/apps/decent-bench/lib/features/workspace/domain/sqlite_import_models.dart
+++ b/apps/decent-bench/lib/features/workspace/domain/sqlite_import_models.dart
@@ -579,6 +579,12 @@ class SqliteImportSummary {
required this.importedTables,
required this.rowsCopiedByTable,
required this.indexesCreated,
+ required this.targetTableCount,
+ required this.targetIndexCount,
+ required this.targetViewCount,
+ required this.targetTriggerCount,
+ required this.databaseFileBytes,
+ required this.walFileBytes,
required this.skippedItems,
required this.warnings,
required this.statusMessage,
@@ -591,6 +597,12 @@ class SqliteImportSummary {
final List importedTables;
final Map rowsCopiedByTable;
final List indexesCreated;
+ final int targetTableCount;
+ final int targetIndexCount;
+ final int targetViewCount;
+ final int targetTriggerCount;
+ final int databaseFileBytes;
+ final int walFileBytes;
final List skippedItems;
final List warnings;
final String statusMessage;
@@ -610,6 +622,12 @@ class SqliteImportSummary {
'importedTables': importedTables,
'rowsCopiedByTable': rowsCopiedByTable,
'indexesCreated': indexesCreated,
+ 'targetTableCount': targetTableCount,
+ 'targetIndexCount': targetIndexCount,
+ 'targetViewCount': targetViewCount,
+ 'targetTriggerCount': targetTriggerCount,
+ 'databaseFileBytes': databaseFileBytes,
+ 'walFileBytes': walFileBytes,
'skippedItems':