sabiql 3.0.0

A fast, driver-less TUI for browsing and editing PostgreSQL, MySQL, and SQLite databases
name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: short

jobs:
  sqlite-safe-mode-changes:
    name: SQLite Safe Mode Changes
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
    outputs:
      sqlite_safe_mode: ${{ steps.changes.outputs.sqlite_safe_mode }}
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - name: Test SQLite Safe Mode change detection
        run: bash scripts/test_sqlite_safe_mode_changes.sh
      - id: changes
        env:
          GH_TOKEN: ${{ github.token }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          EVENT_NAME: ${{ github.event_name }}
        run: |
          if [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then
            echo "sqlite_safe_mode=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          changed_files=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename')
          mapfile -t changed_files <<< "$changed_files"
          bash scripts/sqlite_safe_mode_changes.sh "${changed_files[@]}" >> "$GITHUB_OUTPUT"

  fmt:
    name: Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

  clippy:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
      - run: cargo clippy --workspace --all-targets --all-features -- -D warnings
      - run: cargo clippy --workspace --all-targets --no-default-features -- -D warnings
      - run: ./scripts/lint_all.sh

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: nextest,cargo-insta
      - id: test_all_features
        continue-on-error: true
        run: cargo nextest run --workspace --profile ci-all --all-features --show-progress=none --status-level=fail --final-status-level=fail
      - id: test_no_default_features
        continue-on-error: true
        run: cargo nextest run -p sabiql --profile ci-no-default --no-default-features --show-progress=none --status-level=fail --final-status-level=fail
      - name: Summarize failed tests
        if: steps.test_all_features.outcome == 'failure' || steps.test_no_default_features.outcome == 'failure'
        run: |
          python3 scripts/ci_test_failure_summary.py \
            target/nextest/ci-all/junit-all-features.xml \
            target/nextest/ci-no-default/junit-no-default-features.xml >> "$GITHUB_STEP_SUMMARY"
      - name: Fail job if tests failed
        if: steps.test_all_features.outcome == 'failure' || steps.test_no_default_features.outcome == 'failure'
        run: exit 1
      - name: Reject unreferenced snapshots
        run: cargo insta test --unreferenced=reject -p sabiql

  windows-test:
    name: Windows Test
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
        with:
          key: windows-test
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: nextest
      - name: Install SQLite
        shell: pwsh
        run: |
          $sqliteVersion = '3530400'
          $sqliteArchive = Join-Path $env:RUNNER_TEMP "sqlite-tools-win-x64-$sqliteVersion.zip"
          $sqliteTools = Join-Path $env:RUNNER_TEMP 'sqlite-tools'
          $sqliteUrl = "https://www.sqlite.org/2026/sqlite-tools-win-x64-$sqliteVersion.zip"
          curl.exe --fail --location --retry 3 --output $sqliteArchive $sqliteUrl
          $actualHash = (Get-FileHash -Path $sqliteArchive -Algorithm SHA256).Hash
          if ($actualHash -cne 'F46EE2475DE4CBE287E6E5F7D43C838796B14E7379CD216BDBB28D391429F9FC') {
            throw "SQLite archive checksum mismatch: $actualHash"
          }
          Expand-Archive -Path $sqliteArchive -DestinationPath $sqliteTools
          $sqliteTools | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
          & (Join-Path $sqliteTools 'sqlite3.exe') --version
      - run: cargo nextest run --workspace --profile ci-all --all-features --show-progress=none --status-level=fail --final-status-level=fail
      - run: cargo nextest run -p sabiql --profile ci-no-default --no-default-features --show-progress=none --status-level=fail --final-status-level=fail
      - run: cargo run --all-features -- --version

  integration:
    name: Integration Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: nextest
      - name: Start PostgreSQL
        run: docker compose up -d --wait postgres
      - name: Run psql integration tests (Tier 2)
        env:
          CARGO_TARGET_DIR: ${{ runner.temp }}/sabiql-integration-cargo-target
        run: cargo nextest run -p sabiql --run-ignored ignored-only -E 'test(tests::adapter_postgres)' --show-progress=none
      - name: Run PostgreSQL metadata provider lifecycle test
        env:
          CARGO_TARGET_DIR: ${{ runner.temp }}/sabiql-integration-cargo-target
        run: cargo nextest run -p sabiql-infra --run-ignored ignored-only -E 'test(provider_classifies_full_and_light_missing_relations_without_losing_empty_tables)' --show-progress=none
      - name: Run Oracle MySQL 8.4 integration tests (Tier 2)
        env:
          CARGO_TARGET_DIR: ${{ runner.temp }}/sabiql-integration-cargo-target
        run: bash scripts/mysql_integration.sh test

  sqlite-safe-mode:
    name: SQLite Safe Mode
    needs: sqlite-safe-mode-changes
    if: needs.sqlite-safe-mode-changes.outputs.sqlite_safe_mode == 'true'
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: nextest
      - name: Test SQLite 3.41.1
        run: |
          curl --fail --location --retry 3 \
            https://www.sqlite.org/2023/sqlite-autoconf-3410100.tar.gz \
            --output sqlite-autoconf-3410100.tar.gz
          echo "4dadfbeab9f8e16c695d4fbbc51c16b2f77fb97ff4c1c3d139919dfc038c9e33  sqlite-autoconf-3410100.tar.gz" | sha256sum --check --status
          tar -xzf sqlite-autoconf-3410100.tar.gz
          cd sqlite-autoconf-3410100
          ./configure --prefix="$HOME/.local/sqlite-minimum"
          make -j2
          make install
          PATH="$HOME/.local/sqlite-minimum/bin:$PATH" sqlite3 --version
          PATH="$HOME/.local/sqlite-minimum/bin:$PATH" cargo nextest run -p sabiql-infra
      - name: Reject SQLite 3.41.0
        run: |
          curl --fail --location --retry 3 \
            https://www.sqlite.org/2023/sqlite-autoconf-3410000.tar.gz \
            --output sqlite-autoconf-3410000.tar.gz
          echo "49f77ac53fd9aa5d7395f2499cb816410e5621984a121b858ccca05310b05c70  sqlite-autoconf-3410000.tar.gz" | sha256sum --check --status
          tar -xzf sqlite-autoconf-3410000.tar.gz
          cd sqlite-autoconf-3410000
          ./configure --prefix="$HOME/.local/sqlite-below-minimum"
          make -j2
          make install
          export PATH="$HOME/.local/sqlite-below-minimum/bin:$PATH"
          sqlite3 --version
          SABIQL_EXPECT_SQLITE_SAFE_MODE_REJECTION=1 cargo test -p sabiql-infra \
            adapters::sqlite::metadata::catalog::tests::metadata::rejects_sqlite_before_safe_mode_minimum_at_connection \
            -- --exact
          cargo test -p sabiql-app \
            model::connection::error::tests::error_info::from_db_operation_error_classifies_sqlite_safe_mode_requirement \
            -- --exact
      - name: Test system SQLite
        run: |
          sqlite3 --version
          cargo nextest run -p sabiql-infra

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
      - run: cargo build --workspace --release
      - run: cargo build --workspace --release --no-default-features

  audit:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: cargo-audit
      - run: bash ./scripts/assert_self_update_backend.sh
      - run: cargo audit

  machete:
    name: Unused Dependencies
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
        with:
          persist-credentials: false
      - uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
        with:
          tool: cargo-machete
      - run: cargo machete