Repository: syncthing/syncthing (language: go)

Issue:
Use SQLite database backend
This is a notice of intent to experiment. :) I tried to use a SQLite database backend several years ago and it turned out not great. There were several issues at the time; performance, lack of required concurrency in some cases, and difficulty in cross compilation. This time I intend to take a slightly different approach, with slightly different goals, and also things have changed in the wider ecosystem.

- Instead of using it as a dumb key-value store I will use wider tables with at least some fields of the underlying data exposed as columns. The intention is to use native SQLite indexes and processes for a lot of the metadata housekeeping we do manually today.
- If having long running write transactions is still a problem I'll work to split them up -- for example, having a puller iteration be for only a subset of files rather than all that are needed at the time. For ordering items (by size, modification time, etc) we can use SQL constructions with a limit clause.
- There are multiple implementations of SQLite available, the most common being the regular C implementation with a wrapper, which we can use for most builds. For the more exotic builds there is a Go translation of the C source code, which is equally cross compilable to what we have today. We can use either or both as required.
- I'm willing to accept a certain level of performance degradation if it results in improved stability and maintainability.




Discussion / hints:
Not sure if it makes sense for Syncthings database design, but it happens far too often that people use a single sqlite database for everything. Having multiple database files for unrelated data tends to avoid performance bottlenecks.

Fix patch:
diff --git a/.github/workflows/build-syncthing.yaml b/.github/workflows/build-syncthing.yaml
index 40810699c98..a574c7e9e86 100644
--- a/.github/workflows/build-syncthing.yaml
+++ b/.github/workflows/build-syncthing.yaml
@@ -13,8 +13,6 @@ env:
   GO_VERSION: "~1.24.0"
 
   # Optimize compatibility on the slow archictures.
-  GO386: softfloat
-  GOARM: "5"
   GOMIPS: softfloat
 
   # Avoid hilarious amounts of obscuring log output when running tests.
@@ -24,6 +22,8 @@ env:
   BUILD_USER: builder
   BUILD_HOST: github.syncthing.net
 
+  TAGS: "netgo osusergo sqlite_omit_load_extension"
+
 # A note on actions and third party code... The actions under actions/ (like
 # `uses: actions/checkout`) are maintained by GitHub, and we need to trust
 # GitHub to maintain their code and infrastructure or we're in deep shit in
@@ -85,6 +85,7 @@ jobs:
           LOKI_USER: ${{ secrets.LOKI_USER }}
           LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }}
           LOKI_LABELS: "go=${{ matrix.go }},runner=${{ matrix.runner }},repo=${{ github.repository }},ref=${{ github.ref }}"
+          CGO_ENABLED: "1"
 
   #
   # Meta checks for formatting, copyright, etc
@@ -136,17 +137,8 @@ jobs:
 
   package-windows:
     name: Package for Windows
-    runs-on: windows-latest
+    runs-on: ubuntu-latest
     steps:
-      - name: Set git to use LF
-        # Without this, the checkout will happen with CRLF line endings,
-        # which is fine for the source code but messes up tests that depend
-        # on data on disk being as expected. Ideally, those tests should be
-        # fixed, but not today.
-        run: |
-          git config --global core.autocrlf false
-          git config --global core.eol lf
-
       - uses: actions/checkout@v4
         with:
           fetch-depth: 0
@@ -158,17 +150,14 @@ jobs:
           cache: false
           check-latest: true
 
-      - name: Get actual Go version
-        run: |
-          go version
-          echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
+      - uses: mlugg/setup-zig@v1
 
       - uses: actions/cache@v4
         with:
           path: |
-            ~\AppData\Local\go-build
-            ~\go\pkg\mod
-          key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
+            ~/.cache/go-build
+            ~/go/pkg/mod
+          key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-windows-${{ hashFiles('**/go.sum') }}
 
       - name: Install dependencies
         run: |
@@ -176,15 +165,14 @@ jobs:
 
       - name: Create packages
         run: |
-          $targets = 'syncthing', 'stdiscosrv', 'strelaysrv'
-          $archs = 'amd64', 'arm', 'arm64', '386'
-          foreach ($arch in $archs) {
-            foreach ($tgt in $targets) {
-              go run build.go -goarch $arch zip $tgt
-            }
-          }
+          for tgt in syncthing stdiscosrv strelaysrv ; do
+            go run build.go -tags "${{env.TAGS}}" -goos windows -goarch amd64 -cc "zig cc -target x86_64-windows" zip $tgt
+            go run build.go -tags "${{env.TAGS}}" -goos windows -goarch 386 -cc "zig cc -target x86-windows" zip $tgt
+            go run build.go -tags "${{env.TAGS}}" -goos windows -goarch arm64 -cc "zig cc -target aarch64-windows" zip $tgt
+            # go run build.go -tags "${{env.TAGS}}" -goos windows -goarch arm -cc "zig cc -target thumb-windows" zip $tgt # failes with linker errors
+          done
         env:
-          CGO_ENABLED: "0"
+          CGO_ENABLED: "1"
 
       - name: Archive artifacts
         uses: actions/upload-artifact@v4
@@ -194,7 +182,7 @@ jobs:
 
   codesign-windows:
     name: Codesign for Windows
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
     environment: release
     runs-on: windows-latest
     needs:
@@ -269,6 +257,8 @@ jobs:
           go version
           echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
 
+      - uses: mlugg/setup-zig@v1
+
       - uses: actions/cache@v4
         with:
           path: |
@@ -278,14 +268,25 @@ jobs:
 
       - name: Create packages
         run: |
-          archs=$(go tool dist list | grep linux | sed 's#linux/##')
-          for goarch in $archs ; do
-            for tgt in syncthing stdiscosrv strelaysrv ; do
-              go run build.go -goarch "$goarch" tar "$tgt"
-            done
+          sudo apt-get install -y gcc-mips64-linux-gnuabi64 gcc-mips64el-linux-gnuabi64
+          for tgt in syncthing stdiscosrv strelaysrv ; do
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch amd64 -cc "zig cc -target x86_64-linux-musl" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch 386 -cc "zig cc -target x86-linux-musl" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch arm -cc "zig cc -target arm-linux-musleabi" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch arm64 -cc "zig cc -target aarch64-linux-musl" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips -cc "zig cc -target mips-linux-musleabi" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mipsle -cc "zig cc -target mipsel-linux-musleabi" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips64 -cc mips64-linux-gnuabi64-gcc tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips64le -cc mips64el-linux-gnuabi64-gcc tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch riscv64 -cc "zig cc -target riscv64-linux-musl" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch s390x -cc "zig cc -target s390x-linux-musl" tar "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch loong64 -cc "zig cc -target loongarch64-linux-musl" tar "$tgt"
+            # go run build.go -tags "${{env.TAGS}}" -goos linux -goarch ppc64 -cc "zig cc -target powerpc64-linux-musl" tar "$tgt" # fails with linkmode not supported
+            go run build.go -tags "${{env.TAGS}}" -goos linux -goarch ppc64le -cc "zig cc -target powerpc64le-linux-musl" tar "$tgt"
           done
         env:
-          CGO_ENABLED: "0"
+          CGO_ENABLED: "1"
+          EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
 
       - name: Archive artifacts
         uses: actions/upload-artifact@v4
@@ -303,6 +304,8 @@ jobs:
     name: Package for macOS
     if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
     environment: release
+    env:
+      CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
     runs-on: macos-latest
     steps:
       - uses: actions/checkout@v4
@@ -329,6 +332,7 @@ jobs:
           key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
 
       - name: Import signing certificate
+        if: env.CODESIGN_IDENTITY != ''
         run: |
           # Set up a run-specific keychain, making it available for the
           # `codesign` tool.
@@ -356,7 +360,7 @@ jobs:
       - name: Create package (amd64)
         run: |
           for tgt in syncthing stdiscosrv strelaysrv ; do
-            go run build.go -goarch amd64 zip "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -goarch amd64 zip "$tgt"
           done
         env:
           CGO_ENABLED: "1"
@@ -372,7 +376,7 @@ jobs:
           EOT
           chmod 755 xgo.sh
           for tgt in syncthing stdiscosrv strelaysrv ; do
-            go run build.go -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
+            go run build.go -tags "${{env.TAGS}}" -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
           done
         env:
           CGO_ENABLED: "1"
@@ -401,7 +405,7 @@ jobs:
 
   notarize-macos:
     name: Notarize for macOS
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
     environment: release
     needs:
       - package-macos
@@ -483,7 +487,7 @@ jobs:
             goarch="${plat#*/}"
             echo "::group ::$plat"
             for tgt in syncthing stdiscosrv strelaysrv ; do
-              if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" 2>/dev/null; then
+              if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" ; then
                 echo "::warning ::Failed to build $tgt for $plat"
               fi
             done
@@ -545,7 +549,7 @@ jobs:
 
   sign-for-upgrade:
     name: Sign for upgrade
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')  || startsWith(github.ref, 'refs/tags/v'))
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-')  || startsWith(github.ref, 'refs/tags/v'))
     environment: release
     needs:
       - codesign-windows
@@ -663,6 +667,8 @@ jobs:
         run: |
           gem install fpm
 
+      - uses: mlugg/setup-zig@v1
+
       - uses: actions/cache@v4
         with:
           path: |
@@ -670,15 +676,17 @@ jobs:
             ~/go/pkg/mod
           key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-debian-${{ hashFiles('**/go.sum') }}
 
-      - name: Package for Debian
+      - name: Package for Debian (CGO)
         run: |
-          for arch in amd64 i386 armhf armel arm64 ; do
-            for tgt in syncthing stdiscosrv strelaysrv ; do
-              go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb "$tgt"
-            done
+          for tgt in syncthing stdiscosrv strelaysrv ; do
+            go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch amd64 -cc "zig cc -target x86_64-linux-musl" deb "$tgt"
+            go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm -cc "zig cc -target arm-linux-musleabi" deb "$tgt"
+            go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm64 -cc "zig cc -target aarch64-linux-musl" deb "$tgt"
           done
         env:
           BUILD_USER: debian
+          CGO_ENABLED: "1"
+          EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
 
       - name: Archive artifacts
         uses: actions/upload-artifact@v4
@@ -692,7 +700,7 @@ jobs:
 
   publish-nightly:
     name: Publish nightly build
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
     environment: release
     needs:
       - sign-for-upgrade
@@ -742,7 +750,7 @@ jobs:
 
   publish-release-files:
     name: Publish release files
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
     environment: release
     needs:
       - sign-for-upgrade
@@ -809,7 +817,7 @@ jobs:
 
   publish-apt:
     name: Publish APT
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
+    if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v1'))
     environment: release
     needs:
       - package-debian
@@ -836,7 +844,9 @@ jobs:
       - name: Prepare packages
         run: |
           kind=stable
-          if [[ $VERSION == *-rc.[0-9] ]] ; then
+          if [[ $VERSION == v2* ]] ; then
+            kind=v2
+          elif [[ $VERSION == *-rc.[0-9] ]] ; then
             kind=candidate
           elif [[ $VERSION == *-* ]] ; then
             kind=nightly
@@ -888,8 +898,10 @@ jobs:
   docker-syncthing:
     name: Build and push Docker images
     runs-on: ubuntu-latest
-    if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
+    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
     environment: docker
+    env:
+      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
     permissions:
       contents: read
       packages: write
@@ -902,13 +914,13 @@ jobs:
         include:
           - pkg: syncthing
             dockerfile: Dockerfile
-            image: syncthing/syncthing
+            image: syncthing
           - pkg: strelaysrv
             dockerfile: Dockerfile.strelaysrv
-            image: syncthing/relaysrv
+            image: relaysrv
           - pkg: stdiscosrv
             dockerfile: Dockerfile.stdiscosrv
-            image: syncthing/discosrv
+            image: discosrv
     steps:
       - uses: actions/checkout@v4
         with:
@@ -926,6 +938,8 @@ jobs:
           go version
           echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
 
+      - uses: mlugg/setup-zig@v1
+
       - uses: actions/cache@v4
         with:
           path: |
@@ -933,33 +947,34 @@ jobs:
             ~/go/pkg/mod
           key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }}
 
-      - name: Build binaries
+      - name: Build binaries (CGO)
         run: |
-          for arch in amd64 arm64 arm; do
-            go run build.go -goos linux -goarch "$arch" -no-upgrade build ${{ matrix.pkg }}
-            mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
-          done
+          # amd64
+          go run build.go -goos linux -goarch amd64 -tags "${{env.TAGS}}" -cc "zig cc -target x86_64-linux-musl" -no-upgrade build ${{ matrix.pkg }}
+          mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-amd64
+
+          # arm64
+          go run build.go -goos linux -goarch arm64 -tags "${{env.TAGS}}" -cc "zig cc -target aarch64-linux-musl" -no-upgrade build ${{ matrix.pkg }}
+          mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-arm64
+
+          # arm
+          go run build.go -goos linux -goarch arm -tags "${{env.TAGS}}" -cc "zig cc -target arm-linux-musleabi" -no-upgrade build ${{ matrix.pkg }}
+          mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-arm
         env:
-          CGO_ENABLED: "0"
+          CGO_ENABLED: "1"
           BUILD_USER: docker
-
-      - name: Check if we will be able to push images
-        run: |
-          if [[ "${{ secrets.DOCKERHUB_TOKEN }}" != "" ]]; then
-            echo "DOCKER_PUSH=true" >> $GITHUB_ENV;
-          fi
+          EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
 
       - name: Login to Docker Hub
         uses: docker/login-action@v3
-        if: env.DOCKER_PUSH == 'true'
+        if: env.DOCKERHUB_USERNAME != ''
         with:
           registry: docker.io
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          username: ${{ env.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
       - name: Login to GHCR
         uses: docker/login-action@v3
-        if: env.DOCKER_PUSH == 'true'
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
@@ -972,18 +987,31 @@ jobs:
         run: |
           version=$(go run build.go version)
           version=${version#v}
+          repo=ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
+          ref="${{github.ref_name}}"
+          ref=${ref//\//-} # slashes to dashes
+
+          # List of tags for ghcr.io
           if [[ $version == @([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]) ]] ; then
-            echo Release version, pushing to :latest and version tags
             major=${version%.*.*}
             minor=${version%.*}
-            tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:$major,ghcr.io/${{ matrix.image }}:$major,docker.io/${{ matrix.image }}:$minor,ghcr.io/${{ matrix.image }}:$minor,docker.io/${{ matrix.image }}:latest,ghcr.io/${{ matrix.image }}:latest
+            tags=$repo:$version,$repo:$major,$repo:$minor,$repo:latest
           elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then
-            echo Release candidate, pushing to :rc and version tags
-            tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:rc,ghcr.io/${{ matrix.image }}:rc
+            tags=$repo:$version,$repo:rc
+          elif [[ $ref == "main" ]] ; then
+            tags=$repo:edge
           else
-            echo Development version, pushing to :edge
-            tags=docker.io/${{ matrix.image }}:edge,ghcr.io/${{ matrix.image }}:edge
+            tags=$repo:$ref
+          fi
+
+          # If we have a Docker Hub secret, also push to there.
+          if [[ $DOCKERHUB_USERNAME != "" ]] ; then
+            dockerhubtags="${tags//ghcr.io\/syncthing/docker.io\/syncthing}"
+            tags="$tags,$dockerhubtags"
           fi
+
+          echo Pushing to $tags
+
           echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV
           echo "VERSION=$version" >> $GITHUB_ENV
 
@@ -993,8 +1021,8 @@ jobs:
           context: .
           file: ${{ matrix.dockerfile }}
           platforms: linux/amd64,linux/arm64,linux/arm/7
-          push: ${{ env.DOCKER_PUSH == 'true' }}
           tags: ${{ env.DOCKER_TAGS }}
+          push: true
           labels: |
             org.opencontainers.image.version=${{ env.VERSION }}
             org.opencontainers.image.revision=${{ github.sha }}
diff --git a/.golangci.yml b/.golangci.yml
index dfabd12054f..68b2c855f16 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -3,6 +3,7 @@ linters:
   disable:
     - cyclop
     - depguard
+    - err113
     - exhaustive
     - exhaustruct
     - funlen
@@ -12,6 +13,7 @@ linters:
     - gocognit
     - goconst
     - gocyclo
+    - godot
     - godox
     - gofmt
     - goimports
@@ -21,15 +23,19 @@ linters:
     - ireturn
     - lll
     - maintidx
+    - musttag
     - nestif
+    - nlreturn
     - nonamedreturns
     - paralleltest
+    - prealloc
     - protogetter
     - scopelint
     - tagalign
     - tagliatelle
     - testpackage
     - varnamelen
+    - wrapcheck
     - wsl
 
 issues:
diff --git a/build.go b/build.go
index fd591a41eab..7b205edd9b5 100644
--- a/build.go
+++ b/build.go
@@ -288,10 +288,10 @@ func runCommand(cmd string, target target) {
 		build(target, tags)
 
 	case "test":
-		test(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
+		test(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
 
 	case "bench":
-		bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
+		bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
 
 	case "integration":
 		integration(false)
@@ -474,7 +474,7 @@ func install(target target, tags []string) {
 		defer shouldCleanupSyso(sysoPath)
 	}
 
-	args := []string{"install", "-v"}
+	args := []string{"install"}
 	args = appendParameters(args, tags, target.buildPkgs...)
 	runPrint(goCmd, args...)
 }
@@ -502,7 +502,7 @@ func build(target target, tags []string) {
 		defer shouldCleanupSyso(sysoPath)
 	}
 
-	args := []string{"build", "-v"}
+	args := []string{"build"}
 	if buildOut != "" {
 		args = append(args, "-o", buildOut)
 	}
@@ -514,13 +514,6 @@ func setBuildEnvVars() {
 	os.Setenv("GOOS", goos)
 	os.Setenv("GOARCH", goarch)
 	os.Setenv("CC", cc)
-	if os.Getenv("CGO_ENABLED") == "" {
-		switch goos {
-		case "darwin", "solaris":
-		default:
-			os.Setenv("CGO_ENABLED", "0")
-		}
-	}
 }
 
 func appendParameters(args []string, tags []string, pkgs ...string) []string {
@@ -736,12 +729,9 @@ func shouldBuildSyso(dir string) (string, error) {
 	sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
 
 	// See https://github.com/josephspurrier/goversioninfo#command-line-flags
-	armOption := ""
-	if strings.Contains(goarch, "arm") {
-		armOption = "-arm=true"
-	}
-
-	if _, err := runError("goversioninfo", "-o", sysoPath, armOption); err != nil {
+	arm := strings.HasPrefix(goarch, "arm")
+	a64 := strings.Contains(goarch, "64")
+	if _, err := runError("goversioninfo", "-o", sysoPath, fmt.Sprintf("-arm=%v", arm), fmt.Sprintf("-64=%v", a64)); err != nil {
 		return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
 	}
 
diff --git a/cmd/syncthing/cli/debug.go b/cmd/syncthing/cli/debug.go
index 9425e431020..9f18e1556e3 100644
--- a/cmd/syncthing/cli/debug.go
+++ b/cmd/syncthing/cli/debug.go
@@ -41,5 +41,4 @@ func (p *profileCommand) Run(ctx Context) error {
 type debugCommand struct {
 	File    fileCommand    `cmd:"" help:"Show information about a file (or directory/symlink)"`
 	Profile profileCommand `cmd:"" help:"Save a profile to help figuring out what Syncthing does"`
-	Index   indexCommand   `cmd:"" help:"Show information about the index (database)"`
 }
diff --git a/cmd/syncthing/cli/index.go b/cmd/syncthing/cli/index.go
deleted file mode 100644
index 04c9daa3716..00000000000
--- a/cmd/syncthing/cli/index.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cli
-
-import (
-	"github.com/alecthomas/kong"
-)
-
-type indexCommand struct {
-	Dump     struct{} `cmd:"" help:"Print the entire db"`
-	DumpSize struct{} `cmd:"" help:"Print the db size of different categories of information"`
-	Check    struct{} `cmd:"" help:"Check the database for inconsistencies"`
-	Account  struct{} `cmd:"" help:"Print key and value size statistics per key type"`
-}
-
-func (*indexCommand) Run(kongCtx *kong.Context) error {
-	switch kongCtx.Selected().Name {
-	case "dump":
-		return indexDump()
-	case "dump-size":
-		return indexDumpSize()
-	case "check":
-		return indexCheck()
-	case "account":
-		return indexAccount()
-	}
-	return nil
-}
diff --git a/cmd/syncthing/cli/index_accounting.go b/cmd/syncthing/cli/index_accounting.go
deleted file mode 100644
index cd22b16bb47..00000000000
--- a/cmd/syncthing/cli/index_accounting.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2020 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cli
-
-import (
-	"fmt"
-	"os"
-	"text/tabwriter"
-)
-
-// indexAccount prints key and data size statistics per class
-func indexAccount() error {
-	ldb, err := getDB()
-	if err != nil {
-		return err
-	}
-
-	it, err := ldb.NewPrefixIterator(nil)
-	if err != nil {
-		return err
-	}
-
-	var ksizes [256]int
-	var dsizes [256]int
-	var counts [256]int
-	var max [256]int
-
-	for it.Next() {
-		key := it.Key()
-		t := key[0]
-		ds := len(it.Value())
-		ks := len(key)
-		s := ks + ds
-
-		counts[t]++
-		ksizes[t] += ks
-		dsizes[t] += ds
-		if s > max[t] {
-			max[t] = s
-		}
-	}
-
-	tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', tabwriter.AlignRight)
-	toti, totds, totks := 0, 0, 0
-	for t := range ksizes {
-		if ksizes[t] > 0 {
-			// yes metric kilobytes 🤘
-			fmt.Fprintf(tw, "0x%02x:\t%d items,\t%d KB keys +\t%d KB data,\t%d B +\t%d B avg,\t%d B max\t\n", t, counts[t], ksizes[t]/1000, dsizes[t]/1000, ksizes[t]/counts[t], dsizes[t]/counts[t], max[t])
-			toti += counts[t]
-			totds += dsizes[t]
-			totks += ksizes[t]
-		}
-	}
-	fmt.Fprintf(tw, "Total\t%d items,\t%d KB keys +\t%d KB data.\t\n", toti, totks/1000, totds/1000)
-	tw.Flush()
-
-	return nil
-}
diff --git a/cmd/syncthing/cli/index_dump.go b/cmd/syncthing/cli/index_dump.go
deleted file mode 100644
index 6eff4717c37..00000000000
--- a/cmd/syncthing/cli/index_dump.go
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright (C) 2015 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cli
-
-import (
-	"encoding/binary"
-	"fmt"
-	"time"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/bep"
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-func indexDump() error {
-	ldb, err := getDB()
-	if err != nil {
-		return err
-	}
-	it, err := ldb.NewPrefixIterator(nil)
-	if err != nil {
-		return err
-	}
-	for it.Next() {
-		key := it.Key()
-		switch key[0] {
-		case db.KeyTypeDevice:
-			folder := binary.BigEndian.Uint32(key[1:])
-			device := binary.BigEndian.Uint32(key[1+4:])
-			name := nulString(key[1+4+4:])
-			fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
-
-			var f bep.FileInfo
-			err := proto.Unmarshal(it.Value(), &f)
-			if err != nil {
-				return err
-			}
-			fmt.Printf(" V:%v\n", &f)
-
-		case db.KeyTypeGlobal:
-			folder := binary.BigEndian.Uint32(key[1:])
-			name := nulString(key[1+4:])
-			var flv dbproto.VersionList
-			proto.Unmarshal(it.Value(), &flv)
-			fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, &flv)
-
-		case db.KeyTypeBlock:
-			folder := binary.BigEndian.Uint32(key[1:])
-			hash := key[1+4 : 1+4+32]
-			name := nulString(key[1+4+32:])
-			fmt.Printf("[block] F:%d H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
-
-		case db.KeyTypeDeviceStatistic:
-			fmt.Printf("[dstat] K:%x V:%x\n", key, it.Value())
-
-		case db.KeyTypeFolderStatistic:
-			fmt.Printf("[fstat] K:%x V:%x\n", key, it.Value())
-
-		case db.KeyTypeVirtualMtime:
-			folder := binary.BigEndian.Uint32(key[1:])
-			name := nulString(key[1+4:])
-			val := it.Value()
-			var realTime, virtualTime time.Time
-			realTime.UnmarshalBinary(val[:len(val)/2])
-			virtualTime.UnmarshalBinary(val[len(val)/2:])
-			fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, realTime, virtualTime)
-
-		case db.KeyTypeFolderIdx:
-			key := binary.BigEndian.Uint32(key[1:])
-			fmt.Printf("[folderidx] K:%d V:%q\n", key, it.Value())
-
-		case db.KeyTypeDeviceIdx:
-			key := binary.BigEndian.Uint32(key[1:])
-			val := it.Value()
-			device := "<nil>"
-			if len(val) > 0 {
-				dev, err := protocol.DeviceIDFromBytes(val)
-				if err != nil {
-					device = fmt.Sprintf("<invalid %d bytes>", len(val))
-				} else {
-					device = dev.String()
-				}
-			}
-			fmt.Printf("[deviceidx] K:%d V:%s\n", key, device)
-
-		case db.KeyTypeIndexID:
-			device := binary.BigEndian.Uint32(key[1:])
-			folder := binary.BigEndian.Uint32(key[5:])
-			fmt.Printf("[indexid] D:%d F:%d I:%x\n", device, folder, it.Value())
-
-		case db.KeyTypeFolderMeta:
-			folder := binary.BigEndian.Uint32(key[1:])
-			fmt.Printf("[foldermeta] F:%d", folder)
-			var cs dbproto.CountsSet
-			if err := proto.Unmarshal(it.Value(), &cs); err != nil {
-				fmt.Printf(" (invalid)\n")
-			} else {
-				fmt.Printf(" V:%v\n", &cs)
-			}
-
-		case db.KeyTypeMiscData:
-			fmt.Printf("[miscdata] K:%q V:%q\n", key[1:], it.Value())
-
-		case db.KeyTypeSequence:
-			folder := binary.BigEndian.Uint32(key[1:])
-			seq := binary.BigEndian.Uint64(key[5:])
-			fmt.Printf("[sequence] F:%d S:%d V:%q\n", folder, seq, it.Value())
-
-		case db.KeyTypeNeed:
-			folder := binary.BigEndian.Uint32(key[1:])
-			file := string(key[5:])
-			fmt.Printf("[need] F:%d V:%q\n", folder, file)
-
-		case db.KeyTypeBlockList:
-			fmt.Printf("[blocklist] H:%x\n", key[1:])
-
-		case db.KeyTypeBlockListMap:
-			folder := binary.BigEndian.Uint32(key[1:])
-			hash := key[5:37]
-			fileName := string(key[37:])
-			fmt.Printf("[blocklistmap] F:%d H:%x N:%s\n", folder, hash, fileName)
-
-		case db.KeyTypeVersion:
-			fmt.Printf("[version] H:%x", key[1:])
-			var v bep.Vector
-			err := proto.Unmarshal(it.Value(), &v)
-			if err != nil {
-				fmt.Printf(" (invalid)\n")
-			} else {
-				fmt.Printf(" V:%v\n", &v)
-			}
-
-		case db.KeyTypePendingFolder:
-			device := binary.BigEndian.Uint32(key[1:])
-			folder := string(key[5:])
-			var of dbproto.ObservedFolder
-			proto.Unmarshal(it.Value(), &of)
-			fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, &of)
-
-		case db.KeyTypePendingDevice:
-			device := "<invalid>"
-			dev, err := protocol.DeviceIDFromBytes(key[1:])
-			if err == nil {
-				device = dev.String()
-			}
-			var od dbproto.ObservedDevice
-			proto.Unmarshal(it.Value(), &od)
-			fmt.Printf("[pendingDevice] D:%v V:%v\n", device, &od)
-
-		default:
-			fmt.Printf("[??? %d]\n  %x\n  %x\n", key[0], key, it.Value())
-		}
-	}
-	return nil
-}
diff --git a/cmd/syncthing/cli/index_dumpsize.go b/cmd/syncthing/cli/index_dumpsize.go
deleted file mode 100644
index da7a6dbe6cb..00000000000
--- a/cmd/syncthing/cli/index_dumpsize.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (C) 2015 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cli
-
-import (
-	"encoding/binary"
-	"fmt"
-	"sort"
-
-	"github.com/syncthing/syncthing/lib/db"
-)
-
-func indexDumpSize() error {
-	type sizedElement struct {
-		key  string
-		size int
-	}
-
-	ldb, err := getDB()
-	if err != nil {
-		return err
-	}
-
-	it, err := ldb.NewPrefixIterator(nil)
-	if err != nil {
-		return err
-	}
-
-	var elems []sizedElement
-	for it.Next() {
-		var ele sizedElement
-
-		key := it.Key()
-		switch key[0] {
-		case db.KeyTypeDevice:
-			folder := binary.BigEndian.Uint32(key[1:])
-			device := binary.BigEndian.Uint32(key[1+4:])
-			name := nulString(key[1+4+4:])
-			ele.key = fmt.Sprintf("DEVICE:%d:%d:%s", folder, device, name)
-
-		case db.KeyTypeGlobal:
-			folder := binary.BigEndian.Uint32(key[1:])
-			name := nulString(key[1+4:])
-			ele.key = fmt.Sprintf("GLOBAL:%d:%s", folder, name)
-
-		case db.KeyTypeBlock:
-			folder := binary.BigEndian.Uint32(key[1:])
-			hash := key[1+4 : 1+4+32]
-			name := nulString(key[1+4+32:])
-			ele.key = fmt.Sprintf("BLOCK:%d:%x:%s", folder, hash, name)
-
-		case db.KeyTypeDeviceStatistic:
-			ele.key = fmt.Sprintf("DEVICESTATS:%s", key[1:])
-
-		case db.KeyTypeFolderStatistic:
-			ele.key = fmt.Sprintf("FOLDERSTATS:%s", key[1:])
-
-		case db.KeyTypeVirtualMtime:
-			ele.key = fmt.Sprintf("MTIME:%s", key[1:])
-
-		case db.KeyTypeFolderIdx:
-			id := binary.BigEndian.Uint32(key[1:])
-			ele.key = fmt.Sprintf("FOLDERIDX:%d", id)
-
-		case db.KeyTypeDeviceIdx:
-			id := binary.BigEndian.Uint32(key[1:])
-			ele.key = fmt.Sprintf("DEVICEIDX:%d", id)
-
-		default:
-			ele.key = fmt.Sprintf("UNKNOWN:%x", key)
-		}
-		ele.size = len(it.Value())
-		elems = append(elems, ele)
-	}
-
-	sort.Slice(elems, func(i, j int) bool {
-		return elems[i].size > elems[j].size
-	})
-	for _, ele := range elems {
-		fmt.Println(ele.key, ele.size)
-	}
-
-	return nil
-}
diff --git a/cmd/syncthing/cli/index_idxck.go b/cmd/syncthing/cli/index_idxck.go
deleted file mode 100644
index a0dab459a94..00000000000
--- a/cmd/syncthing/cli/index_idxck.go
+++ /dev/null
@@ -1,434 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package cli
-
-import (
-	"bytes"
-	"encoding/binary"
-	"errors"
-	"fmt"
-	"sort"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/bep"
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-type fileInfoKey struct {
-	folder uint32
-	device uint32
-	name   string
-}
-
-type globalKey struct {
-	folder uint32
-	name   string
-}
-
-type sequenceKey struct {
-	folder   uint32
-	sequence uint64
-}
-
-func indexCheck() (err error) {
-	ldb, err := getDB()
-	if err != nil {
-		return err
-	}
-
-	folders := make(map[uint32]string)
-	devices := make(map[uint32]string)
-	deviceToIDs := make(map[string]uint32)
-	fileInfos := make(map[fileInfoKey]*bep.FileInfo)
-	globals := make(map[globalKey]*dbproto.VersionList)
-	sequences := make(map[sequenceKey]string)
-	needs := make(map[globalKey]struct{})
-	blocklists := make(map[string]struct{})
-	versions := make(map[string]*bep.Vector)
-	usedBlocklists := make(map[string]struct{})
-	usedVersions := make(map[string]struct{})
-	var localDeviceKey uint32
-	success := true
-	defer func() {
-		if err == nil {
-			if success {
-				fmt.Println("Index check completed successfully.")
-			} else {
-				err = errors.New("Inconsistencies found in the index")
-			}
-		}
-	}()
-
-	it, err := ldb.NewPrefixIterator(nil)
-	if err != nil {
-		return err
-	}
-	for it.Next() {
-		key := it.Key()
-		switch key[0] {
-		case db.KeyTypeDevice:
-			folder := binary.BigEndian.Uint32(key[1:])
-			device := binary.BigEndian.Uint32(key[1+4:])
-			name := nulString(key[1+4+4:])
-
-			var f bep.FileInfo
-			err := proto.Unmarshal(it.Value(), &f)
-			if err != nil {
-				fmt.Println("Unable to unmarshal FileInfo:", err)
-				success = false
-				continue
-			}
-
-			fileInfos[fileInfoKey{folder, device, name}] = &f
-
-		case db.KeyTypeGlobal:
-			folder := binary.BigEndian.Uint32(key[1:])
-			name := nulString(key[1+4:])
-			var flv dbproto.VersionList
-			if err := proto.Unmarshal(it.Value(), &flv); err != nil {
-				fmt.Println("Unable to unmarshal VersionList:", err)
-				success = false
-				continue
-			}
-			globals[globalKey{folder, name}] = &flv
-
-		case db.KeyTypeFolderIdx:
-			key := binary.BigEndian.Uint32(it.Key()[1:])
-			folders[key] = string(it.Value())
-
-		case db.KeyTypeDeviceIdx:
-			key := binary.BigEndian.Uint32(it.Key()[1:])
-			devices[key] = string(it.Value())
-			deviceToIDs[string(it.Value())] = key
-			if bytes.Equal(it.Value(), protocol.LocalDeviceID[:]) {
-				localDeviceKey = key
-			}
-
-		case db.KeyTypeSequence:
-			folder := binary.BigEndian.Uint32(key[1:])
-			seq := binary.BigEndian.Uint64(key[5:])
-			val := it.Value()
-			sequences[sequenceKey{folder, seq}] = string(val[9:])
-
-		case db.KeyTypeNeed:
-			folder := binary.BigEndian.Uint32(key[1:])
-			name := nulString(key[1+4:])
-			needs[globalKey{folder, name}] = struct{}{}
-
-		case db.KeyTypeBlockList:
-			hash := string(key[1:])
-			blocklists[hash] = struct{}{}
-
-		case db.KeyTypeVersion:
-			hash := string(key[1:])
-			var v bep.Vector
-			if err := proto.Unmarshal(it.Value(), &v); err != nil {
-				fmt.Println("Unable to unmarshal Vector:", err)
-				success = false
-				continue
-			}
-			versions[hash] = &v
-		}
-	}
-
-	if localDeviceKey == 0 {
-		fmt.Println("Missing key for local device in device index (bailing out)")
-		success = false
-		return
-	}
-
-	var missingSeq []sequenceKey
-	for fk, fi := range fileInfos {
-		if fk.name != fi.Name {
-			fmt.Printf("Mismatching FileInfo name, %q (key) != %q (actual)\n", fk.name, fi.Name)
-			success = false
-		}
-
-		folder := folders[fk.folder]
-		if folder == "" {
-			fmt.Printf("Unknown folder ID %d for FileInfo %q\n", fk.folder, fk.name)
-			success = false
-			continue
-		}
-		if devices[fk.device] == "" {
-			fmt.Printf("Unknown device ID %d for FileInfo %q, folder %q\n", fk.folder, fk.name, folder)
-			success = false
-		}
-
-		if fk.device == localDeviceKey {
-			sk := sequenceKey{fk.folder, uint64(fi.Sequence)}
-			name, ok := sequences[sk]
-			if !ok {
-				fmt.Printf("Sequence entry missing for FileInfo %q, folder %q, seq %d\n", fi.Name, folder, fi.Sequence)
-				missingSeq = append(missingSeq, sk)
-				success = false
-				continue
-			}
-			if name != fi.Name {
-				fmt.Printf("Sequence entry refers to wrong name, %q (seq) != %q (FileInfo), folder %q, seq %d\n", name, fi.Name, folder, fi.Sequence)
-				success = false
-			}
-		}
-
-		if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
-			key := string(fi.BlocksHash)
-			if _, ok := blocklists[key]; !ok {
-				fmt.Printf("Missing block list for file %q, block list hash %x\n", fi.Name, fi.BlocksHash)
-				success = false
-			} else {
-				usedBlocklists[key] = struct{}{}
-			}
-		}
-
-		if fi.VersionHash != nil {
-			key := string(fi.VersionHash)
-			if _, ok := versions[key]; !ok {
-				fmt.Printf("Missing version vector for file %q, version hash %x\n", fi.Name, fi.VersionHash)
-				success = false
-			} else {
-				usedVersions[key] = struct{}{}
-			}
-		}
-
-		_, ok := globals[globalKey{fk.folder, fk.name}]
-		if !ok {
-			fmt.Printf("Missing global for file %q\n", fi.Name)
-			success = false
-			continue
-		}
-	}
-
-	// Aggregate the ranges of missing sequence entries, print them
-
-	sort.Slice(missingSeq, func(a, b int) bool {
-		if missingSeq[a].folder != missingSeq[b].folder {
-			return missingSeq[a].folder < missingSeq[b].folder
-		}
-		return missingSeq[a].sequence < missingSeq[b].sequence
-	})
-
-	var folder uint32
-	var startSeq, prevSeq uint64
-	for _, sk := range missingSeq {
-		if folder != sk.folder || sk.sequence != prevSeq+1 {
-			if folder != 0 {
-				fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
-			}
-			startSeq = sk.sequence
-			folder = sk.folder
-		}
-		prevSeq = sk.sequence
-	}
-	if folder != 0 {
-		fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
-	}
-
-	for gk, vl := range globals {
-		folder := folders[gk.folder]
-		if folder == "" {
-			fmt.Printf("Unknown folder ID %d for VersionList %q\n", gk.folder, gk.name)
-			success = false
-		}
-		checkGlobal := func(i int, device []byte, version protocol.Vector, invalid, deleted bool) {
-			dev, ok := deviceToIDs[string(device)]
-			if !ok {
-				fmt.Printf("VersionList %q, folder %q refers to unknown device %q\n", gk.name, folder, device)
-				success = false
-			}
-			fi, ok := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
-			if !ok {
-				fmt.Printf("VersionList %q, folder %q, entry %d refers to unknown FileInfo\n", gk.name, folder, i)
-				success = false
-			}
-
-			fiv := fi.Version
-			if fi.VersionHash != nil {
-				fiv = versions[string(fi.VersionHash)]
-			}
-			if !protocol.VectorFromWire(fiv).Equal(version) {
-				fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version)
-				success = false
-			}
-			ffi := protocol.FileInfoFromDB(fi)
-			if ffi.IsInvalid() != invalid {
-				fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, ffi.IsInvalid())
-				success = false
-			}
-			if ffi.IsDeleted() != deleted {
-				fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, ffi.IsDeleted())
-				success = false
-			}
-		}
-		for i, fv := range vl.Versions {
-			ver := protocol.VectorFromWire(fv.Version)
-			for _, device := range fv.Devices {
-				checkGlobal(i, device, ver, false, fv.Deleted)
-			}
-			for _, device := range fv.InvalidDevices {
-				checkGlobal(i, device, ver, true, fv.Deleted)
-			}
-		}
-
-		// If we need this file we should have a need entry for it. False
-		// positives from needsLocally for deleted files, where we might
-		// legitimately lack an entry if we never had it, and ignored files.
-		if needsLocally(vl) {
-			_, ok := needs[gk]
-			if !ok {
-				fv, _ := vlGetGlobal(vl)
-				devB, _ := fvFirstDevice(fv)
-				dev := deviceToIDs[string(devB)]
-				fi := protocol.FileInfoFromDB(fileInfos[fileInfoKey{gk.folder, dev, gk.name}])
-				if !fi.IsDeleted() && !fi.IsIgnored() {
-					fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
-				}
-			}
-		}
-	}
-
-	seenSeq := make(map[fileInfoKey]uint64)
-	for sk, name := range sequences {
-		folder := folders[sk.folder]
-		if folder == "" {
-			fmt.Printf("Unknown folder ID %d for sequence entry %d, %q\n", sk.folder, sk.sequence, name)
-			success = false
-			continue
-		}
-
-		if prev, ok := seenSeq[fileInfoKey{folder: sk.folder, name: name}]; ok {
-			fmt.Printf("Duplicate sequence entry for %q, folder %q, seq %d (prev %d)\n", name, folder, sk.sequence, prev)
-			success = false
-		}
-		seenSeq[fileInfoKey{folder: sk.folder, name: name}] = sk.sequence
-
-		fi, ok := fileInfos[fileInfoKey{sk.folder, localDeviceKey, name}]
-		if !ok {
-			fmt.Printf("Missing FileInfo for sequence entry %d, folder %q, %q\n", sk.sequence, folder, name)
-			success = false
-			continue
-		}
-		if fi.Sequence != int64(sk.sequence) {
-			fmt.Printf("Sequence mismatch for %q, folder %q, %d (key) != %d (FileInfo)\n", name, folder, sk.sequence, fi.Sequence)
-			success = false
-		}
-	}
-
-	for nk := range needs {
-		folder := folders[nk.folder]
-		if folder == "" {
-			fmt.Printf("Unknown folder ID %d for need entry %q\n", nk.folder, nk.name)
-			success = false
-			continue
-		}
-
-		vl, ok := globals[nk]
-		if !ok {
-			fmt.Printf("Missing global for need entry %q, folder %q\n", nk.name, folder)
-			success = false
-			continue
-		}
-
-		if !needsLocally(vl) {
-			fmt.Printf("Need entry for file we don't need, %q, folder %q\n", nk.name, folder)
-			success = false
-		}
-	}
-
-	if d := len(blocklists) - len(usedBlocklists); d > 0 {
-		fmt.Printf("%d block list entries out of %d needs GC\n", d, len(blocklists))
-	}
-	if d := len(versions) - len(usedVersions); d > 0 {
-		fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions))
-	}
-
-	return nil
-}
-
-func needsLocally(vl *dbproto.VersionList) bool {
-	gfv, gok := vlGetGlobal(vl)
-	if !gok { // That's weird, but we hardly need something non-existent
-		return false
-	}
-	fv, ok := vlGet(vl, protocol.LocalDeviceID[:])
-	return db.Need(gfv, ok, protocol.VectorFromWire(fv.Version))
-}
-
-// Get returns a FileVersion that contains the given device and whether it has
-// been found at all.
-func vlGet(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool) {
-	_, i, _, ok := vlFindDevice(vl, device)
-	if !ok {
-		return &dbproto.FileVersion{}, false
-	}
-	return vl.Versions[i], true
-}
-
-// GetGlobal returns the current global FileVersion. The returned FileVersion
-// may be invalid, if all FileVersions are invalid. Returns false only if
-// VersionList is empty.
-func vlGetGlobal(vl *dbproto.VersionList) (*dbproto.FileVersion, bool) {
-	i := vlFindGlobal(vl)
-	if i == -1 {
-		return nil, false
-	}
-	return vl.Versions[i], true
-}
-
-// findGlobal returns the first version that isn't invalid, or if all versions are
-// invalid just the first version (i.e. 0) or -1, if there's no versions at all.
-func vlFindGlobal(vl *dbproto.VersionList) int {
-	for i := range vl.Versions {
-		if !fvIsInvalid(vl.Versions[i]) {
-			return i
-		}
-	}
-	if len(vl.Versions) == 0 {
-		return -1
-	}
-	return 0
-}
-
-// findDevice returns whether the device is in InvalidVersions or Versions and
-// in InvalidDevices or Devices (true for invalid), the positions in the version
-// and device slices and whether it has been found at all.
-func vlFindDevice(vl *dbproto.VersionList, device []byte) (bool, int, int, bool) {
-	for i, v := range vl.Versions {
-		if j := deviceIndex(v.Devices, device); j != -1 {
-			return false, i, j, true
-		}
-		if j := deviceIndex(v.InvalidDevices, device); j != -1 {
-			return true, i, j, true
-		}
-	}
-	return false, -1, -1, false
-}
-
-func deviceIndex(devices [][]byte, device []byte) int {
-	for i, dev := range devices {
-		if bytes.Equal(device, dev) {
-			return i
-		}
-	}
-	return -1
-}
-
-func fvFirstDevice(fv *dbproto.FileVersion) ([]byte, bool) {
-	if len(fv.Devices) != 0 {
-		return fv.Devices[0], true
-	}
-	if len(fv.InvalidDevices) != 0 {
-		return fv.InvalidDevices[0], true
-	}
-	return nil, false
-}
-
-func fvIsInvalid(fv *dbproto.FileVersion) bool {
-	return fv == nil || len(fv.Devices) == 0
-}
diff --git a/cmd/syncthing/cli/utils.go b/cmd/syncthing/cli/utils.go
index 7b69bbf5786..fde6115755b 100644
--- a/cmd/syncthing/cli/utils.go
+++ b/cmd/syncthing/cli/utils.go
@@ -17,8 +17,6 @@ import (
 	"path/filepath"
 
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/locations"
 )
 
 func responseToBArray(response *http.Response) ([]byte, error) {
@@ -133,10 +131,6 @@ func prettyPrintResponse(response *http.Response) error {
 	return prettyPrintJSON(data)
 }
 
-func getDB() (backend.Backend, error) {
-	return backend.OpenLevelDBRO(locations.Get(locations.Database))
-}
-
 func nulString(bs []byte) string {
 	for i := range bs {
 		if bs[i] == 0 {
diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go
index 1faff628ef3..740ac7c5b35 100644
--- a/cmd/syncthing/main.go
+++ b/cmd/syncthing/main.go
@@ -22,6 +22,7 @@ import (
 	"path"
 	"path/filepath"
 	"regexp"
+	"runtime"
 	"runtime/pprof"
 	"sort"
 	"strconv"
@@ -38,10 +39,10 @@ import (
 	"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
 	"github.com/syncthing/syncthing/cmd/syncthing/decrypt"
 	"github.com/syncthing/syncthing/cmd/syncthing/generate"
+	"github.com/syncthing/syncthing/internal/db"
 	_ "github.com/syncthing/syncthing/lib/automaxprocs"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/dialer"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
@@ -139,42 +140,41 @@ var entrypoint struct {
 // serveOptions are the options for the `syncthing serve` command.
 type serveOptions struct {
 	cmdutil.CommonOptions
-	AllowNewerConfig bool   `help:"Allow loading newer than current config version"`
-	Audit            bool   `help:"Write events to audit file"`
-	AuditFile        string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
-	BrowserOnly      bool   `help:"Open GUI in browser"`
-	DataDir          string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
-	DeviceID         bool   `help:"Show the device ID"`
-	GenerateDir      string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` // DEPRECATED: replaced by subcommand!
-	GUIAddress       string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
-	GUIAPIKey        string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
-	LogFile          string `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"`
-	LogFlags         int    `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"`
-	LogMaxFiles      int    `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"`
-	LogMaxSize       int    `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"`
-	NoBrowser        bool   `help:"Do not start browser"`
-	NoRestart        bool   `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"`
-	NoUpgrade        bool   `env:"STNOUPGRADE" help:"Disable automatic upgrades"`
-	Paths            bool   `help:"Show configuration paths"`
-	Paused           bool   `help:"Start with all devices and folders paused"`
-	Unpaused         bool   `help:"Start with all devices and folders unpaused"`
-	Upgrade          bool   `help:"Perform upgrade"`
-	UpgradeCheck     bool   `help:"Check for available upgrade"`
-	UpgradeTo        string `placeholder:"URL" help:"Force upgrade directly from specified URL"`
-	Verbose          bool   `help:"Print verbose log output"`
-	Version          bool   `help:"Show version"`
+	AllowNewerConfig      bool          `help:"Allow loading newer than current config version"`
+	Audit                 bool          `help:"Write events to audit file"`
+	AuditFile             string        `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
+	BrowserOnly           bool          `help:"Open GUI in browser"`
+	DataDir               string        `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
+	DeviceID              bool          `help:"Show the device ID"`
+	GenerateDir           string        `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` // DEPRECATED: replaced by subcommand!
+	GUIAddress            string        `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
+	GUIAPIKey             string        `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
+	LogFile               string        `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"`
+	LogFlags              int           `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"`
+	LogMaxFiles           int           `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"`
+	LogMaxSize            int           `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"`
+	NoBrowser             bool          `help:"Do not start browser"`
+	NoRestart             bool          `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"`
+	NoUpgrade             bool          `env:"STNOUPGRADE" help:"Disable automatic upgrades"`
+	Paths                 bool          `help:"Show configuration paths"`
+	Paused                bool          `help:"Start with all devices and folders paused"`
+	Unpaused              bool          `help:"Start with all devices and folders unpaused"`
+	Upgrade               bool          `help:"Perform upgrade"`
+	UpgradeCheck          bool          `help:"Check for available upgrade"`
+	UpgradeTo             string        `placeholder:"URL" help:"Force upgrade directly from specified URL"`
+	Verbose               bool          `help:"Print verbose log output"`
+	Version               bool          `help:"Show version"`
+	DBMaintenanceInterval time.Duration `env:"STDBMAINTINTERVAL" help:"Database maintenance interval" default:"8h"`
 
 	// Debug options below
-	DebugDBIndirectGCInterval time.Duration `env:"STGCINDIRECTEVERY" help:"Database indirection GC interval"`
-	DebugDBRecheckInterval    time.Duration `env:"STRECHECKDBEVERY" help:"Database metadata recalculation interval"`
-	DebugGUIAssetsDir         string        `placeholder:"PATH" help:"Directory to load GUI assets from" env:"STGUIASSETS"`
-	DebugPerfStats            bool          `env:"STPERFSTATS" help:"Write running performance statistics to perf-$pid.csv (Unix only)"`
-	DebugProfileBlock         bool          `env:"STBLOCKPROFILE" help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds"`
-	DebugProfileCPU           bool          `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"STCPUPROFILE"`
-	DebugProfileHeap          bool          `env:"STHEAPPROFILE" help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases"`
-	DebugProfilerListen       string        `placeholder:"ADDR" env:"STPROFILER" help:"Network profiler listen address"`
-	DebugResetDatabase        bool          `name:"reset-database" help:"Reset the database, forcing a full rescan and resync"`
-	DebugResetDeltaIdxs       bool          `name:"reset-deltas" help:"Reset delta index IDs, forcing a full index exchange"`
+	DebugGUIAssetsDir   string `placeholder:"PATH" help:"Directory to load GUI assets from" env:"STGUIASSETS"`
+	DebugPerfStats      bool   `env:"STPERFSTATS" help:"Write running performance statistics to perf-$pid.csv (Unix only)"`
+	DebugProfileBlock   bool   `env:"STBLOCKPROFILE" help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds"`
+	DebugProfileCPU     bool   `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"STCPUPROFILE"`
+	DebugProfileHeap    bool   `env:"STHEAPPROFILE" help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases"`
+	DebugProfilerListen string `placeholder:"ADDR" env:"STPROFILER" help:"Network profiler listen address"`
+	DebugResetDatabase  bool   `name:"reset-database" help:"Reset the database, forcing a full rescan and resync"`
+	DebugResetDeltaIdxs bool   `name:"reset-deltas" help:"Reset delta index IDs, forcing a full index exchange"`
 
 	// Internal options, not shown to users
 	InternalRestarting   bool `env:"STRESTART" hidden:"1"`
@@ -592,8 +592,12 @@ func syncthingMain(options serveOptions) {
 		})
 	}
 
-	dbFile := locations.Get(locations.Database)
-	ldb, err := syncthing.OpenDBBackend(dbFile, cfgWrapper.Options().DatabaseTuning)
+	if err := syncthing.TryMigrateDatabase(); err != nil {
+		l.Warnln("Failed to migrate old-style database:", err)
+		os.Exit(1)
+	}
+
+	sdb, err := syncthing.OpenDatabase(locations.Get(locations.Database))
 	if err != nil {
 		l.Warnln("Error opening database:", err)
 		os.Exit(1)
@@ -602,11 +606,11 @@ func syncthingMain(options serveOptions) {
 	// Check if auto-upgrades is possible, and if yes, and it's enabled do an initial
 	// upgrade immediately. The auto-upgrade routine can only be started
 	// later after App is initialised.
-
 	autoUpgradePossible := autoUpgradePossible(options)
 	if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() {
 		// try to do upgrade directly and log the error if relevant.
-		release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
+		miscDB := db.NewMiscDB(sdb)
+		release, err := initialAutoUpgradeCheck(miscDB)
 		if err == nil {
 			err = upgrade.To(release)
 		}
@@ -617,7 +621,7 @@ func syncthingMain(options serveOptions) {
 				l.Infoln("Initial automatic upgrade:", err)
 			}
 		} else {
-			l.Infof("Upgraded to %q, exiting now.", release.Tag)
+			l.Infof("Upgraded to %q, should exit now.", release.Tag)
 			os.Exit(svcutil.ExitUpgrade.AsInt())
 		}
 	}
@@ -629,24 +633,17 @@ func syncthingMain(options serveOptions) {
 	}
 
 	appOpts := syncthing.Options{
-		NoUpgrade:            options.NoUpgrade,
-		ProfilerAddr:         options.DebugProfilerListen,
-		ResetDeltaIdxs:       options.DebugResetDeltaIdxs,
-		Verbose:              options.Verbose,
-		DBRecheckInterval:    options.DebugDBRecheckInterval,
-		DBIndirectGCInterval: options.DebugDBIndirectGCInterval,
+		NoUpgrade:             options.NoUpgrade,
+		ProfilerAddr:          options.DebugProfilerListen,
+		ResetDeltaIdxs:        options.DebugResetDeltaIdxs,
+		Verbose:               options.Verbose,
+		DBMaintenanceInterval: options.DBMaintenanceInterval,
 	}
 	if options.Audit {
 		appOpts.AuditWriter = auditWriter(options.AuditFile)
 	}
-	if dur, err := time.ParseDuration(os.Getenv("STRECHECKDBEVERY")); err == nil {
-		appOpts.DBRecheckInterval = dur
-	}
-	if dur, err := time.ParseDuration(os.Getenv("STGCINDIRECTEVERY")); err == nil {
-		appOpts.DBIndirectGCInterval = dur
-	}
 
-	app, err := syncthing.New(cfgWrapper, ldb, evLogger, cert, appOpts)
+	app, err := syncthing.New(cfgWrapper, sdb, evLogger, cert, appOpts)
 	if err != nil {
 		l.Warnln("Failed to start Syncthing:", err)
 		os.Exit(svcutil.ExitError.AsInt())
@@ -692,6 +689,7 @@ func syncthingMain(options serveOptions) {
 		pprof.StopCPUProfile()
 	}
 
+	runtime.KeepAlive(lf) // ensure lock is still held to this point
 	os.Exit(int(status))
 }
 
@@ -833,7 +831,7 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
 	}
 }
 
-func initialAutoUpgradeCheck(misc *db.NamespacedKV) (upgrade.Release, error) {
+func initialAutoUpgradeCheck(misc *db.Typed) (upgrade.Release, error) {
 	if last, ok, err := misc.Time(upgradeCheckKey); err == nil && ok && time.Since(last) < upgradeCheckInterval {
 		return upgrade.Release{}, errTooEarlyUpgradeCheck
 	}
diff --git a/go.mod b/go.mod
index 4ebd75662cb..bd70f08e70a 100644
--- a/go.mod
+++ b/go.mod
@@ -14,13 +14,14 @@ require (
 	github.com/go-ldap/ldap/v3 v3.4.10
 	github.com/gobwas/glob v0.2.3
 	github.com/gofrs/flock v0.12.1
-	github.com/greatroar/blobloom v0.8.0
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/jackpal/gateway v1.0.16
 	github.com/jackpal/go-nat-pmp v1.0.2
+	github.com/jmoiron/sqlx v1.4.0
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
 	github.com/maruel/panicparse/v2 v2.4.0
+	github.com/mattn/go-sqlite3 v1.14.24
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
 	github.com/maxmind/geoipupdate/v6 v6.1.0
 	github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
@@ -46,6 +47,7 @@ require (
 	golang.org/x/time v0.11.0
 	golang.org/x/tools v0.31.0
 	google.golang.org/protobuf v1.36.5
+	modernc.org/sqlite v1.36.0
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -57,6 +59,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/ebitengine/purego v0.8.2 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
@@ -70,7 +73,9 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/klauspost/compress v1.17.11 // indirect
 	github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/nxadm/tail v1.4.11 // indirect
 	github.com/onsi/ginkgo/v2 v2.20.2 // indirect
 	github.com/oschwald/maxminddb-golang v1.13.1 // indirect
@@ -81,6 +86,7 @@ require (
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/common v0.62.0 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/stretchr/objx v0.5.2 // indirect
@@ -93,6 +99,9 @@ require (
 	golang.org/x/mod v0.24.0 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	modernc.org/libc v1.61.13 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.8.2 // indirect
 )
 
 // https://github.com/gobwas/glob/pull/55
diff --git a/go.sum b/go.sum
index 2d7cbc8ae4c..4d740cd8943 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f h1:GmH5lT+moM7PbAJFBq57nH9WJ+wRnBXr/tyaYWbSAx8=
 github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f/go.mod h1:Nhfib1j/VFnLrXL9cHgA+/n2O6P5THuWelOnbfPNd78=
 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
@@ -39,6 +41,8 @@ github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkE
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
 github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -58,6 +62,8 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
@@ -89,8 +95,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
 github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
-github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4=
-github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -126,6 +130,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
 github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -138,10 +144,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
 github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
 github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI=
 github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
+github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ=
 github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA=
@@ -150,6 +163,8 @@ github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 h1:cUVxyR+U
 github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75/go.mod h1:pBbZyGwC5i16IBkjVKoy/sznA8jPD/K9iedwe1ESE6w=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
@@ -201,6 +216,8 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk
 github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY=
 github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -328,6 +345,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -397,5 +415,29 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
+modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
+modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
+modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
+modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
+modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
+modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/gui/default/syncthing/core/aboutModalView.html b/gui/default/syncthing/core/aboutModalView.html
index a58917d3088..bb5a4dcab4a 100644
--- a/gui/default/syncthing/core/aboutModalView.html
+++ b/gui/default/syncthing/core/aboutModalView.html
@@ -59,9 +59,12 @@ <h4 class="text-center" translate>The Syncthing Authors</h4>
           <li><a href="https://github.com/golang/protobuf">golang/protobuf</a>, Copyright &copy; 2010 The Go Authors.</li>
           <li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
           <li><a href="https://github.com/jackpal/gateway">jackpal/gateway</a>, Copyright &copy; 2010 Jack Palevich.</li>
+          <li><a href="https://github.com/jmoiron/sqlx">jmoiron/sqlx</a>, Copyright &copy; 2013 Jason Moiron.</li>
           <li><a href="https://github.com/kballard/go-shellquote">kballard/go-shellquote</a>, Copyright &copy; 2014 Kevin Ballard.</li>
           <li><a href="https://github.com/mattn/go-isatty">mattn/go-isatty</a>, Copyright &copy; Yasuhiro MATSUMOTO.</li>
+          <li><a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a>, Copyright &copy; 2014 Yasuhiro Matsumoto</li>
           <li><a href="https://github.com/matttproud/golang_protobuf_extensions">matttproud/golang_protobuf_extensions</a>, Copyright &copy; 2012 Matt T. Proud.</li>
+          <li><a href="https://modernc.org/sqlite">modernc.org/sqlite</a>, Copyright &copy; 2017 The Sqlite Authors</li>
           <li><a href="https://github.com/oschwald/geoip2-golang">oschwald/geoip2-golang</a>, Copyright &copy; 2015, Gregory J. Oschwald.</li>
           <li><a href="https://github.com/oschwald/maxminddb-golang">oschwald/maxminddb-golang</a>, Copyright &copy; 2015, Gregory J. Oschwald.</li>
           <li><a href="https://github.com/petermattis/goid">petermattis/goid</a>, Copyright &copy; 2015-2016 Peter Mattis.</li>
diff --git a/internal/db/counts.go b/internal/db/counts.go
new file mode 100644
index 00000000000..a24ecdb2f29
--- /dev/null
+++ b/internal/db/counts.go
@@ -0,0 +1,73 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+type Counts struct {
+	Files       int
+	Directories int
+	Symlinks    int
+	Deleted     int
+	Bytes       int64
+	Sequence    int64             // zero for the global state
+	DeviceID    protocol.DeviceID // device ID for remote devices, or special values for local/global
+	LocalFlags  uint32            // the local flag for this count bucket
+}
+
+func (c Counts) Add(other Counts) Counts {
+	return Counts{
+		Files:       c.Files + other.Files,
+		Directories: c.Directories + other.Directories,
+		Symlinks:    c.Symlinks + other.Symlinks,
+		Deleted:     c.Deleted + other.Deleted,
+		Bytes:       c.Bytes + other.Bytes,
+		Sequence:    c.Sequence + other.Sequence,
+		DeviceID:    protocol.EmptyDeviceID,
+		LocalFlags:  c.LocalFlags | other.LocalFlags,
+	}
+}
+
+func (c Counts) TotalItems() int {
+	return c.Files + c.Directories + c.Symlinks + c.Deleted
+}
+
+func (c Counts) String() string {
+	var flags strings.Builder
+	if c.LocalFlags&protocol.FlagLocalNeeded != 0 {
+		flags.WriteString("Need")
+	}
+	if c.LocalFlags&protocol.FlagLocalIgnored != 0 {
+		flags.WriteString("Ignored")
+	}
+	if c.LocalFlags&protocol.FlagLocalMustRescan != 0 {
+		flags.WriteString("Rescan")
+	}
+	if c.LocalFlags&protocol.FlagLocalReceiveOnly != 0 {
+		flags.WriteString("Recvonly")
+	}
+	if c.LocalFlags&protocol.FlagLocalUnsupported != 0 {
+		flags.WriteString("Unsupported")
+	}
+	if c.LocalFlags != 0 {
+		flags.WriteString(fmt.Sprintf("(%x)", c.LocalFlags))
+	}
+	if flags.Len() == 0 {
+		flags.WriteString("---")
+	}
+	return fmt.Sprintf("{Device:%v, Files:%d, Dirs:%d, Symlinks:%d, Del:%d, Bytes:%d, Seq:%d, Flags:%s}", c.DeviceID, c.Files, c.Directories, c.Symlinks, c.Deleted, c.Bytes, c.Sequence, flags.String())
+}
+
+// Equal compares the numbers only, not sequence/dev/flags.
+func (c Counts) Equal(o Counts) bool {
+	return c.Files == o.Files && c.Directories == o.Directories && c.Symlinks == o.Symlinks && c.Deleted == o.Deleted && c.Bytes == o.Bytes
+}
diff --git a/internal/db/interface.go b/internal/db/interface.go
new file mode 100644
index 00000000000..550143d7378
--- /dev/null
+++ b/internal/db/interface.go
@@ -0,0 +1,123 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db // import "github.com/syncthing/syncthing/internal/db/sqlite"
+
+import (
+	"iter"
+	"time"
+
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/thejerf/suture/v4"
+)
+
+type DB interface {
+	Service(maintenanceInterval time.Duration) suture.Service
+
+	// Basics
+	Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error
+	Close() error
+
+	// Single files
+	GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error)
+	GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error)
+	GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error)
+
+	// File iterators
+	//
+	// n.b. there is a slight inconsistency in the return types where some
+	// return a FileInfo iterator and some a FileMetadata iterator. The
+	// latter is more lightweight, and the discrepancy depends on how the
+	// functions tend to be used. We can introduce more variations as
+	// required.
+	AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error)
+	AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error)
+	AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error)
+	AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)
+	AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error)
+	AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error)
+	AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error)
+	AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error)
+	AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error)
+
+	// Cleanup
+	DropAllFiles(folder string, device protocol.DeviceID) error
+	DropDevice(device protocol.DeviceID) error
+	DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
+	DropFolder(folder string) error
+
+	// Various metadata
+	GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error)
+	ListFolders() ([]string, error)
+	ListDevicesForFolder(folder string) ([]protocol.DeviceID, error)
+	RemoteSequences(folder string) (map[protocol.DeviceID]int64, error)
+
+	// Counts
+	CountGlobal(folder string) (Counts, error)
+	CountLocal(folder string, device protocol.DeviceID) (Counts, error)
+	CountNeed(folder string, device protocol.DeviceID) (Counts, error)
+	CountReceiveOnlyChanged(folder string) (Counts, error)
+
+	// Index IDs
+	DropAllIndexIDs() error
+	GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error)
+	SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error
+
+	// MtimeFS
+	DeleteMtime(folder, name string) error
+	GetMtime(folder, name string) (ondisk, virtual time.Time)
+	PutMtime(folder, name string, ondisk, virtual time.Time) error
+
+	KV
+}
+
+// Generic KV store
+type KV interface {
+	GetKV(key string) ([]byte, error)
+	PutKV(key string, val []byte) error
+	DeleteKV(key string) error
+	PrefixKV(prefix string) (iter.Seq[KeyValue], func() error)
+}
+
+type BlockMapEntry struct {
+	BlocklistHash []byte
+	Offset        int64
+	BlockIndex    int
+	Size          int
+}
+
+type KeyValue struct {
+	Key   string
+	Value []byte
+}
+
+type FileMetadata struct {
+	Name       string
+	Sequence   int64
+	ModNanos   int64
+	Size       int64
+	LocalFlags int64
+	Type       protocol.FileInfoType
+	Deleted    bool
+	Invalid    bool
+}
+
+func (f *FileMetadata) ModTime() time.Time {
+	return time.Unix(0, f.ModNanos)
+}
+
+func (f *FileMetadata) IsReceiveOnlyChanged() bool {
+	return f.LocalFlags&protocol.FlagLocalReceiveOnly != 0
+}
+
+func (f *FileMetadata) IsDirectory() bool {
+	return f.Type == protocol.FileInfoTypeDirectory
+}
+
+func (f *FileMetadata) ShouldConflict() bool {
+	return f.LocalFlags&protocol.LocalConflictFlags != 0
+}
diff --git a/internal/db/metrics.go b/internal/db/metrics.go
new file mode 100644
index 00000000000..d59408dc4cf
--- /dev/null
+++ b/internal/db/metrics.go
@@ -0,0 +1,229 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db
+
+import (
+	"iter"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+var (
+	metricCurrentOperations = promauto.NewGaugeVec(prometheus.GaugeOpts{
+		Namespace: "syncthing",
+		Subsystem: "db",
+		Name:      "operations_current",
+	}, []string{"folder", "operation"})
+	metricTotalOperationSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "syncthing",
+		Subsystem: "db",
+		Name:      "operation_seconds_total",
+		Help:      "Total time spent in database operations, per folder and operation",
+	}, []string{"folder", "operation"})
+	metricTotalOperationsCount = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "syncthing",
+		Subsystem: "db",
+		Name:      "operations_total",
+		Help:      "Total number of database operations, per folder and operation",
+	}, []string{"folder", "operation"})
+	metricTotalFilesUpdatedCount = promauto.NewCounterVec(prometheus.CounterOpts{
+		Namespace: "syncthing",
+		Subsystem: "db",
+		Name:      "files_updated_total",
+		Help:      "Total number of files updated",
+	}, []string{"folder"})
+)
+
+func MetricsWrap(db DB) DB {
+	return metricsDB{db}
+}
+
+type metricsDB struct {
+	DB
+}
+
+func (m metricsDB) account(folder, op string) func() {
+	t0 := time.Now()
+	metricCurrentOperations.WithLabelValues(folder, op).Inc()
+	return func() {
+		if dur := time.Since(t0).Seconds(); dur > 0 {
+			metricTotalOperationSeconds.WithLabelValues(folder, op).Add(dur)
+		}
+		metricTotalOperationsCount.WithLabelValues(folder, op).Inc()
+		metricCurrentOperations.WithLabelValues(folder, op).Dec()
+	}
+}
+
+func (m metricsDB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error) {
+	defer m.account(folder, "AllLocalFilesWithBlocksHash")()
+	return m.DB.AllLocalFilesWithBlocksHash(folder, h)
+}
+
+func (m metricsDB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error) {
+	defer m.account("-", "AllLocalFilesWithBlocksHashAnyFolder")()
+	return m.DB.AllLocalFilesWithBlocksHashAnyFolder(h)
+}
+
+func (m metricsDB) AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error) {
+	defer m.account(folder, "AllGlobalFiles")()
+	return m.DB.AllGlobalFiles(folder)
+}
+
+func (m metricsDB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error) {
+	defer m.account(folder, "AllGlobalFilesPrefix")()
+	return m.DB.AllGlobalFilesPrefix(folder, prefix)
+}
+
+func (m metricsDB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
+	defer m.account(folder, "AllLocalFiles")()
+	return m.DB.AllLocalFiles(folder, device)
+}
+
+func (m metricsDB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
+	defer m.account(folder, "AllLocalFilesPrefix")()
+	return m.DB.AllLocalFilesWithPrefix(folder, device, prefix)
+}
+
+func (m metricsDB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
+	defer m.account(folder, "AllLocalFilesBySequence")()
+	return m.DB.AllLocalFilesBySequence(folder, device, startSeq, limit)
+}
+
+func (m metricsDB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
+	defer m.account(folder, "AllNeededGlobalFiles")()
+	return m.DB.AllNeededGlobalFiles(folder, device, order, limit, offset)
+}
+
+func (m metricsDB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
+	defer m.account(folder, "GetGlobalAvailability")()
+	return m.DB.GetGlobalAvailability(folder, file)
+}
+
+func (m metricsDB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error) {
+	defer m.account("-", "AllLocalBlocksWithHash")()
+	return m.DB.AllLocalBlocksWithHash(hash)
+}
+
+func (m metricsDB) Close() error {
+	defer m.account("-", "Close")()
+	return m.DB.Close()
+}
+
+func (m metricsDB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
+	defer m.account(folder, "ListDevicesForFolder")()
+	return m.DB.ListDevicesForFolder(folder)
+}
+
+func (m metricsDB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
+	defer m.account(folder, "RemoteSequences")()
+	return m.DB.RemoteSequences(folder)
+}
+
+func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
+	defer m.account(folder, "DropAllFiles")()
+	return m.DB.DropAllFiles(folder, device)
+}
+
+func (m metricsDB) DropDevice(device protocol.DeviceID) error {
+	defer m.account("-", "DropDevice")()
+	return m.DB.DropDevice(device)
+}
+
+func (m metricsDB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
+	defer m.account(folder, "DropFilesNamed")()
+	return m.DB.DropFilesNamed(folder, device, names)
+}
+
+func (m metricsDB) DropFolder(folder string) error {
+	defer m.account(folder, "DropFolder")()
+	return m.DB.DropFolder(folder)
+}
+
+func (m metricsDB) DropAllIndexIDs() error {
+	defer m.account("-", "IndexIDDropAll")()
+	return m.DB.DropAllIndexIDs()
+}
+
+func (m metricsDB) ListFolders() ([]string, error) {
+	defer m.account("-", "ListFolders")()
+	return m.DB.ListFolders()
+}
+
+func (m metricsDB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
+	defer m.account(folder, "GetGlobalFile")()
+	return m.DB.GetGlobalFile(folder, file)
+}
+
+func (m metricsDB) CountGlobal(folder string) (Counts, error) {
+	defer m.account(folder, "CountGlobal")()
+	return m.DB.CountGlobal(folder)
+}
+
+func (m metricsDB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
+	defer m.account(folder, "IndexIDGet")()
+	return m.DB.GetIndexID(folder, device)
+}
+
+func (m metricsDB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
+	defer m.account(folder, "GetDeviceFile")()
+	return m.DB.GetDeviceFile(folder, device, file)
+}
+
+func (m metricsDB) CountLocal(folder string, device protocol.DeviceID) (Counts, error) {
+	defer m.account(folder, "CountLocal")()
+	return m.DB.CountLocal(folder, device)
+}
+
+func (m metricsDB) CountNeed(folder string, device protocol.DeviceID) (Counts, error) {
+	defer m.account(folder, "CountNeed")()
+	return m.DB.CountNeed(folder, device)
+}
+
+func (m metricsDB) CountReceiveOnlyChanged(folder string) (Counts, error) {
+	defer m.account(folder, "CountReceiveOnlyChanged")()
+	return m.DB.CountReceiveOnlyChanged(folder)
+}
+
+func (m metricsDB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
+	defer m.account(folder, "GetDeviceSequence")()
+	return m.DB.GetDeviceSequence(folder, device)
+}
+
+func (m metricsDB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
+	defer m.account(folder, "IndexIDSet")()
+	return m.DB.SetIndexID(folder, device, id)
+}
+
+func (m metricsDB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
+	defer m.account(folder, "Update")()
+	defer metricTotalFilesUpdatedCount.WithLabelValues(folder).Add(float64(len(fs)))
+	return m.DB.Update(folder, device, fs)
+}
+
+func (m metricsDB) GetKV(key string) ([]byte, error) {
+	defer m.account("-", "GetKV")()
+	return m.DB.GetKV(key)
+}
+
+func (m metricsDB) PutKV(key string, val []byte) error {
+	defer m.account("-", "PutKV")()
+	return m.DB.PutKV(key, val)
+}
+
+func (m metricsDB) DeleteKV(key string) error {
+	defer m.account("-", "DeleteKV")()
+	return m.DB.DeleteKV(key)
+}
+
+func (m metricsDB) PrefixKV(prefix string) (iter.Seq[KeyValue], func() error) {
+	defer m.account("-", "PrefixKV")()
+	return m.DB.PrefixKV(prefix)
+}
diff --git a/lib/db/observed.go b/internal/db/observed.go
similarity index 55%
rename from lib/db/observed.go
rename to internal/db/observed.go
index 47705b6139e..04aa64f4a39 100644
--- a/lib/db/observed.go
+++ b/internal/db/observed.go
@@ -8,6 +8,7 @@ package db
 
 import (
 	"fmt"
+	"strings"
 	"time"
 
 	"google.golang.org/protobuf/proto"
@@ -17,6 +18,14 @@ import (
 	"github.com/syncthing/syncthing/lib/protocol"
 )
 
+type ObservedDB struct {
+	kv KV
+}
+
+func NewObservedDB(kv KV) *ObservedDB {
+	return &ObservedDB{kv: kv}
+}
+
 type ObservedFolder struct {
 	Time             time.Time `json:"time"`
 	Label            string    `json:"label"`
@@ -52,39 +61,42 @@ func (o *ObservedDevice) fromWire(w *dbproto.ObservedDevice) {
 	o.Address = w.GetAddress()
 }
 
-func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
-	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
+func (db *ObservedDB) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
+	key := "device/" + device.String()
 	od := &dbproto.ObservedDevice{
 		Time:    timestamppb.New(time.Now().Truncate(time.Second)),
 		Name:    name,
 		Address: address,
 	}
-	return db.Put(key, mustMarshal(od))
+	return db.kv.PutKV(key, mustMarshal(od))
 }
 
-func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
-	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
-	return db.Delete(key)
+func (db *ObservedDB) RemovePendingDevice(device protocol.DeviceID) error {
+	key := "device/" + device.String()
+	return db.kv.DeleteKV(key)
 }
 
 // PendingDevices enumerates all entries.  Invalid ones are dropped from the database
 // after a warning log message, as a side-effect.
-func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
-	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingDevice})
-	if err != nil {
-		return nil, err
-	}
-	defer iter.Release()
+func (db *ObservedDB) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
 	res := make(map[protocol.DeviceID]ObservedDevice)
-	for iter.Next() {
-		keyDev := db.keyer.DeviceFromPendingDeviceKey(iter.Key())
-		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
+	it, errFn := db.kv.PrefixKV("device/")
+	for kv := range it {
+		_, keyDev, ok := strings.Cut(kv.Key, "/")
+		if !ok {
+			if err := db.kv.DeleteKV(kv.Key); err != nil {
+				return nil, fmt.Errorf("delete invalid pending device: %w", err)
+			}
+			continue
+		}
+
+		deviceID, err := protocol.DeviceIDFromString(keyDev)
 		var protoD dbproto.ObservedDevice
 		var od ObservedDevice
 		if err != nil {
 			goto deleteKey
 		}
-		if err = proto.Unmarshal(iter.Value(), &protoD); err != nil {
+		if err = proto.Unmarshal(kv.Value, &protoD); err != nil {
 			goto deleteKey
 		}
 		od.fromWire(&protoD)
@@ -94,52 +106,37 @@ func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, erro
 		// Deleting invalid entries is the only possible "repair" measure and
 		// appropriate for the importance of pending entries.  They will come back
 		// soon if still relevant.
-		l.Infof("Invalid pending device entry, deleting from database: %x", iter.Key())
-		if err := db.Delete(iter.Key()); err != nil {
-			return nil, err
+		if err := db.kv.DeleteKV(kv.Key); err != nil {
+			return nil, fmt.Errorf("delete invalid pending device: %w", err)
 		}
 	}
-	return res, nil
+	return res, errFn()
 }
 
-func (db *Lowlevel) AddOrUpdatePendingFolder(id string, of ObservedFolder, device protocol.DeviceID) error {
-	key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
-	if err != nil {
-		return err
-	}
-	return db.Put(key, mustMarshal(of.toWire()))
+func (db *ObservedDB) AddOrUpdatePendingFolder(id string, of ObservedFolder, device protocol.DeviceID) error {
+	key := "folder/" + device.String() + "/" + id
+	return db.kv.PutKV(key, mustMarshal(of.toWire()))
 }
 
 // RemovePendingFolderForDevice removes entries for specific folder / device combinations.
-func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
-	key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
-	if err != nil {
-		return err
-	}
-	return db.Delete(key)
+func (db *ObservedDB) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
+	key := "folder/" + device.String() + "/" + id
+	return db.kv.DeleteKV(key)
 }
 
 // RemovePendingFolder removes all entries matching a specific folder ID.
-func (db *Lowlevel) RemovePendingFolder(id string) error {
-	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingFolder})
-	if err != nil {
-		return fmt.Errorf("creating iterator: %w", err)
-	}
-	defer iter.Release()
-	var iterErr error
-	for iter.Next() {
-		if id != string(db.keyer.FolderFromPendingFolderKey(iter.Key())) {
+func (db *ObservedDB) RemovePendingFolder(id string) error {
+	it, errFn := db.kv.PrefixKV("folder/")
+	for kv := range it {
+		parts := strings.Split(kv.Key, "/")
+		if len(parts) != 3 || parts[2] != id {
 			continue
 		}
-		if err = db.Delete(iter.Key()); err != nil {
-			if iterErr != nil {
-				l.Debugf("Repeat error removing pending folder: %v", err)
-			} else {
-				iterErr = err
-			}
+		if err := db.kv.DeleteKV(kv.Key); err != nil {
+			return fmt.Errorf("delete pending folder: %w", err)
 		}
 	}
-	return iterErr
+	return errFn()
 }
 
 // Consolidated information about a pending folder
@@ -147,41 +144,37 @@ type PendingFolder struct {
 	OfferedBy map[protocol.DeviceID]ObservedFolder `json:"offeredBy"`
 }
 
-func (db *Lowlevel) PendingFolders() (map[string]PendingFolder, error) {
+func (db *ObservedDB) PendingFolders() (map[string]PendingFolder, error) {
 	return db.PendingFoldersForDevice(protocol.EmptyDeviceID)
 }
 
 // PendingFoldersForDevice enumerates only entries matching the given device ID, unless it
 // is EmptyDeviceID.  Invalid ones are dropped from the database after a info log
 // message, as a side-effect.
-func (db *Lowlevel) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
-	var err error
-	prefixKey := []byte{KeyTypePendingFolder}
+func (db *ObservedDB) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
+	prefix := "folder/"
 	if device != protocol.EmptyDeviceID {
-		prefixKey, err = db.keyer.GeneratePendingFolderKey(nil, device[:], nil)
-		if err != nil {
-			return nil, err
-		}
-	}
-	iter, err := db.NewPrefixIterator(prefixKey)
-	if err != nil {
-		return nil, err
+		prefix += device.String() + "/"
 	}
-	defer iter.Release()
 	res := make(map[string]PendingFolder)
-	for iter.Next() {
-		keyDev, ok := db.keyer.DeviceFromPendingFolderKey(iter.Key())
-		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
+	it, errFn := db.kv.PrefixKV(prefix)
+	for kv := range it {
+		parts := strings.Split(kv.Key, "/")
+		if len(parts) != 3 {
+			continue
+		}
+		keyDev := parts[1]
+		deviceID, err := protocol.DeviceIDFromString(keyDev)
 		var protoF dbproto.ObservedFolder
 		var of ObservedFolder
 		var folderID string
-		if !ok || err != nil {
+		if err != nil {
 			goto deleteKey
 		}
-		if folderID = string(db.keyer.FolderFromPendingFolderKey(iter.Key())); len(folderID) < 1 {
+		if folderID = parts[2]; len(folderID) < 1 {
 			goto deleteKey
 		}
-		if err = proto.Unmarshal(iter.Value(), &protoF); err != nil {
+		if err = proto.Unmarshal(kv.Value, &protoF); err != nil {
 			goto deleteKey
 		}
 		if _, ok := res[folderID]; !ok {
@@ -196,10 +189,17 @@ func (db *Lowlevel) PendingFoldersForDevice(device protocol.DeviceID) (map[strin
 		// Deleting invalid entries is the only possible "repair" measure and
 		// appropriate for the importance of pending entries.  They will come back
 		// soon if still relevant.
-		l.Infof("Invalid pending folder entry, deleting from database: %x", iter.Key())
-		if err := db.Delete(iter.Key()); err != nil {
-			return nil, err
+		if err := db.kv.DeleteKV(kv.Key); err != nil {
+			return nil, fmt.Errorf("delete invalid pending folder: %w", err)
 		}
 	}
-	return res, nil
+	return res, errFn()
+}
+
+func mustMarshal(m proto.Message) []byte {
+	bs, err := proto.Marshal(m)
+	if err != nil {
+		panic(err)
+	}
+	return bs
 }
diff --git a/lib/db/backend/backend.go b/internal/db/olddb/backend/backend.go
similarity index 93%
rename from lib/db/backend/backend.go
rename to internal/db/olddb/backend/backend.go
index 0ab1795ebe9..789684d91a2 100644
--- a/lib/db/backend/backend.go
+++ b/internal/db/olddb/backend/backend.go
@@ -108,29 +108,8 @@ type Iterator interface {
 // is empty for a db in memory.
 type Backend interface {
 	Reader
-	Writer
 	NewReadTransaction() (ReadTransaction, error)
-	NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error)
 	Close() error
-	Compact() error
-	Location() string
-}
-
-type Tuning int
-
-const (
-	// N.b. these constants must match those in lib/config.Tuning!
-	TuningAuto Tuning = iota
-	TuningSmall
-	TuningLarge
-)
-
-func Open(path string, tuning Tuning) (Backend, error) {
-	return OpenLevelDB(path, tuning)
-}
-
-func OpenMemory() Backend {
-	return OpenLevelDBMemory()
 }
 
 var (
diff --git a/internal/db/olddb/backend/leveldb_backend.go b/internal/db/olddb/backend/leveldb_backend.go
new file mode 100644
index 00000000000..c44eb285c3f
--- /dev/null
+++ b/internal/db/olddb/backend/leveldb_backend.go
@@ -0,0 +1,113 @@
+// Copyright (C) 2018 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package backend
+
+import (
+	"github.com/syndtr/goleveldb/leveldb"
+	"github.com/syndtr/goleveldb/leveldb/iterator"
+	"github.com/syndtr/goleveldb/leveldb/util"
+)
+
+// leveldbBackend implements Backend on top of a leveldb
+type leveldbBackend struct {
+	ldb      *leveldb.DB
+	closeWG  *closeWaitGroup
+	location string
+}
+
+func newLeveldbBackend(ldb *leveldb.DB, location string) *leveldbBackend {
+	return &leveldbBackend{
+		ldb:      ldb,
+		closeWG:  &closeWaitGroup{},
+		location: location,
+	}
+}
+
+func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
+	return b.newSnapshot()
+}
+
+func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
+	rel, err := newReleaser(b.closeWG)
+	if err != nil {
+		return leveldbSnapshot{}, err
+	}
+	snap, err := b.ldb.GetSnapshot()
+	if err != nil {
+		rel.Release()
+		return leveldbSnapshot{}, wrapLeveldbErr(err)
+	}
+	return leveldbSnapshot{
+		snap: snap,
+		rel:  rel,
+	}, nil
+}
+
+func (b *leveldbBackend) Close() error {
+	b.closeWG.CloseWait()
+	return wrapLeveldbErr(b.ldb.Close())
+}
+
+func (b *leveldbBackend) Get(key []byte) ([]byte, error) {
+	val, err := b.ldb.Get(key, nil)
+	return val, wrapLeveldbErr(err)
+}
+
+func (b *leveldbBackend) NewPrefixIterator(prefix []byte) (Iterator, error) {
+	return &leveldbIterator{b.ldb.NewIterator(util.BytesPrefix(prefix), nil)}, nil
+}
+
+func (b *leveldbBackend) NewRangeIterator(first, last []byte) (Iterator, error) {
+	return &leveldbIterator{b.ldb.NewIterator(&util.Range{Start: first, Limit: last}, nil)}, nil
+}
+
+func (b *leveldbBackend) Location() string {
+	return b.location
+}
+
+// leveldbSnapshot implements backend.ReadTransaction
+type leveldbSnapshot struct {
+	snap *leveldb.Snapshot
+	rel  *releaser
+}
+
+func (l leveldbSnapshot) Get(key []byte) ([]byte, error) {
+	val, err := l.snap.Get(key, nil)
+	return val, wrapLeveldbErr(err)
+}
+
+func (l leveldbSnapshot) NewPrefixIterator(prefix []byte) (Iterator, error) {
+	return l.snap.NewIterator(util.BytesPrefix(prefix), nil), nil
+}
+
+func (l leveldbSnapshot) NewRangeIterator(first, last []byte) (Iterator, error) {
+	return l.snap.NewIterator(&util.Range{Start: first, Limit: last}, nil), nil
+}
+
+func (l leveldbSnapshot) Release() {
+	l.snap.Release()
+	l.rel.Release()
+}
+
+type leveldbIterator struct {
+	iterator.Iterator
+}
+
+func (it *leveldbIterator) Error() error {
+	return wrapLeveldbErr(it.Iterator.Error())
+}
+
+// wrapLeveldbErr wraps errors so that the backend package can recognize them
+func wrapLeveldbErr(err error) error {
+	switch err {
+	case leveldb.ErrClosed:
+		return errClosed
+	case leveldb.ErrNotFound:
+		return errNotFound
+	}
+	return err
+}
diff --git a/internal/db/olddb/backend/leveldb_open.go b/internal/db/olddb/backend/leveldb_open.go
new file mode 100644
index 00000000000..344426ae01e
--- /dev/null
+++ b/internal/db/olddb/backend/leveldb_open.go
@@ -0,0 +1,32 @@
+// Copyright (C) 2018 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package backend
+
+import (
+	"github.com/syndtr/goleveldb/leveldb"
+	"github.com/syndtr/goleveldb/leveldb/opt"
+)
+
+const dbMaxOpenFiles = 100
+
+// OpenLevelDBRO attempts to open the database at the given location, read
+// only.
+func OpenLevelDBRO(location string) (Backend, error) {
+	opts := &opt.Options{
+		OpenFilesCacheCapacity: dbMaxOpenFiles,
+		ReadOnly:               true,
+	}
+	ldb, err := open(location, opts)
+	if err != nil {
+		return nil, err
+	}
+	return newLeveldbBackend(ldb, location), nil
+}
+
+func open(location string, opts *opt.Options) (*leveldb.DB, error) {
+	return leveldb.OpenFile(location, opts)
+}
diff --git a/lib/db/keyer.go b/internal/db/olddb/keyer.go
similarity index 99%
rename from lib/db/keyer.go
rename to internal/db/olddb/keyer.go
index 137b5cc2922..3c335fd5254 100644
--- a/lib/db/keyer.go
+++ b/internal/db/olddb/keyer.go
@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package db
+package olddb
 
 import (
 	"encoding/binary"
diff --git a/internal/db/olddb/lowlevel.go b/internal/db/olddb/lowlevel.go
new file mode 100644
index 00000000000..3afad4c76cd
--- /dev/null
+++ b/internal/db/olddb/lowlevel.go
@@ -0,0 +1,70 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package olddb
+
+import (
+	"encoding/binary"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/db/olddb/backend"
+)
+
+// deprecatedLowlevel is the lowest level database interface. It has a very simple
+// purpose: hold the actual backend database, and the in-memory state
+// that belong to that database. In the same way that a single on disk
+// database can only be opened once, there should be only one deprecatedLowlevel for
+// any given backend.
+type deprecatedLowlevel struct {
+	backend.Backend
+	folderIdx *smallIndex
+	deviceIdx *smallIndex
+	keyer     keyer
+}
+
+func NewLowlevel(backend backend.Backend) (*deprecatedLowlevel, error) {
+	// Only log restarts in debug mode.
+	db := &deprecatedLowlevel{
+		Backend:   backend,
+		folderIdx: newSmallIndex(backend, []byte{KeyTypeFolderIdx}),
+		deviceIdx: newSmallIndex(backend, []byte{KeyTypeDeviceIdx}),
+	}
+	db.keyer = newDefaultKeyer(db.folderIdx, db.deviceIdx)
+	return db, nil
+}
+
+// ListFolders returns the list of folders currently in the database
+func (db *deprecatedLowlevel) ListFolders() []string {
+	return db.folderIdx.Values()
+}
+
+func (db *deprecatedLowlevel) IterateMtimes(fn func(folder, name string, ondisk, virtual time.Time) error) error {
+	it, err := db.NewPrefixIterator([]byte{KeyTypeVirtualMtime})
+	if err != nil {
+		return err
+	}
+	defer it.Release()
+	for it.Next() {
+		key := it.Key()[1:]
+		folderID, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key))
+		if !ok {
+			continue
+		}
+		name := key[4:]
+		val := it.Value()
+		var ondisk, virtual time.Time
+		if err := ondisk.UnmarshalBinary(val[:len(val)/2]); err != nil {
+			continue
+		}
+		if err := virtual.UnmarshalBinary(val[len(val)/2:]); err != nil {
+			continue
+		}
+		if err := fn(string(folderID), string(name), ondisk, virtual); err != nil {
+			return err
+		}
+	}
+	return it.Error()
+}
diff --git a/internal/db/olddb/set.go b/internal/db/olddb/set.go
new file mode 100644
index 00000000000..be16e23cc2d
--- /dev/null
+++ b/internal/db/olddb/set.go
@@ -0,0 +1,67 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// Package db provides a set type to track local/remote files with newness
+// checks. We must do a certain amount of normalization in here. We will get
+// fed paths with either native or wire-format separators and encodings
+// depending on who calls us. We transform paths to wire-format (NFC and
+// slashes) on the way to the database, and transform to native format
+// (varying separator and encoding) on the way back out.
+package olddb
+
+import (
+	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+type deprecatedFileSet struct {
+	folder string
+	db     *deprecatedLowlevel
+}
+
+// The Iterator is called with either a protocol.FileInfo or a
+// FileInfoTruncated (depending on the method) and returns true to
+// continue iteration, false to stop.
+type Iterator func(f protocol.FileInfo) bool
+
+func NewFileSet(folder string, db *deprecatedLowlevel) (*deprecatedFileSet, error) {
+	s := &deprecatedFileSet{
+		folder: folder,
+		db:     db,
+	}
+	return s, nil
+}
+
+type Snapshot struct {
+	folder string
+	t      readOnlyTransaction
+}
+
+func (s *deprecatedFileSet) Snapshot() (*Snapshot, error) {
+	t, err := s.db.newReadOnlyTransaction()
+	if err != nil {
+		return nil, err
+	}
+	return &Snapshot{
+		folder: s.folder,
+		t:      t,
+	}, nil
+}
+
+func (s *Snapshot) Release() {
+	s.t.close()
+}
+
+func (s *Snapshot) WithHaveSequence(startSeq int64, fn Iterator) error {
+	return s.t.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn))
+}
+
+func nativeFileIterator(fn Iterator) Iterator {
+	return func(fi protocol.FileInfo) bool {
+		fi.Name = osutil.NativeFilename(fi.Name)
+		return fn(fi)
+	}
+}
diff --git a/lib/db/smallindex.go b/internal/db/olddb/smallindex.go
similarity index 71%
rename from lib/db/smallindex.go
rename to internal/db/olddb/smallindex.go
index b426d906bd2..4182996daa4 100644
--- a/lib/db/smallindex.go
+++ b/internal/db/olddb/smallindex.go
@@ -4,13 +4,13 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package db
+package olddb
 
 import (
 	"encoding/binary"
 	"sort"
 
-	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/internal/db/olddb/backend"
 	"github.com/syncthing/syncthing/lib/sync"
 )
 
@@ -74,23 +74,7 @@ func (i *smallIndex) ID(val []byte) (uint32, error) {
 		return id, nil
 	}
 
-	id := i.nextID
-	i.nextID++
-
-	valStr := string(val)
-	i.val2id[valStr] = id
-	i.id2val[id] = valStr
-
-	key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
-	copy(key, i.prefix)
-	binary.BigEndian.PutUint32(key[len(i.prefix):], id)
-	if err := i.db.Put(key, val); err != nil {
-		i.mut.Unlock()
-		return 0, err
-	}
-
-	i.mut.Unlock()
-	return id, nil
+	panic("missing ID")
 }
 
 // Val returns the value for the given index number, or (nil, false) if there
@@ -106,33 +90,6 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) {
 	return []byte(val), true
 }
 
-func (i *smallIndex) Delete(val []byte) error {
-	i.mut.Lock()
-	defer i.mut.Unlock()
-
-	// Check the reverse mapping to get the ID for the value.
-	if id, ok := i.val2id[string(val)]; ok {
-		// Generate the corresponding database key.
-		key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
-		copy(key, i.prefix)
-		binary.BigEndian.PutUint32(key[len(i.prefix):], id)
-
-		// Put an empty value into the database. This indicates that the
-		// entry does not exist any more and prevents the ID from being
-		// reused in the future.
-		if err := i.db.Put(key, []byte{}); err != nil {
-			return err
-		}
-
-		// Delete reverse mapping.
-		delete(i.id2val, id)
-	}
-
-	// Delete forward mapping.
-	delete(i.val2id, string(val))
-	return nil
-}
-
 // Values returns the set of values in the index
 func (i *smallIndex) Values() []string {
 	// In principle this method should return [][]byte because all the other
diff --git a/internal/db/olddb/transactions.go b/internal/db/olddb/transactions.go
new file mode 100644
index 00000000000..6f6006b069a
--- /dev/null
+++ b/internal/db/olddb/transactions.go
@@ -0,0 +1,193 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package olddb
+
+import (
+	"fmt"
+
+	"google.golang.org/protobuf/proto"
+
+	"github.com/syncthing/syncthing/internal/db/olddb/backend"
+	"github.com/syncthing/syncthing/internal/gen/bep"
+	"github.com/syncthing/syncthing/internal/gen/dbproto"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+// A readOnlyTransaction represents a database snapshot.
+type readOnlyTransaction struct {
+	backend.ReadTransaction
+	keyer keyer
+}
+
+func (db *deprecatedLowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
+	tran, err := db.NewReadTransaction()
+	if err != nil {
+		return readOnlyTransaction{}, err
+	}
+	return db.readOnlyTransactionFromBackendTransaction(tran), nil
+}
+
+func (db *deprecatedLowlevel) readOnlyTransactionFromBackendTransaction(tran backend.ReadTransaction) readOnlyTransaction {
+	return readOnlyTransaction{
+		ReadTransaction: tran,
+		keyer:           db.keyer,
+	}
+}
+
+func (t readOnlyTransaction) close() {
+	t.Release()
+}
+
+func (t readOnlyTransaction) getFileByKey(key []byte) (protocol.FileInfo, bool, error) {
+	f, ok, err := t.getFileTrunc(key, false)
+	if err != nil || !ok {
+		return protocol.FileInfo{}, false, err
+	}
+	return f, true, nil
+}
+
+func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (protocol.FileInfo, bool, error) {
+	bs, err := t.Get(key)
+	if backend.IsNotFound(err) {
+		return protocol.FileInfo{}, false, nil
+	}
+	if err != nil {
+		return protocol.FileInfo{}, false, err
+	}
+	f, err := t.unmarshalTrunc(bs, trunc)
+	if backend.IsNotFound(err) {
+		return protocol.FileInfo{}, false, nil
+	}
+	if err != nil {
+		return protocol.FileInfo{}, false, err
+	}
+	return f, true, nil
+}
+
+func (t readOnlyTransaction) unmarshalTrunc(bs []byte, trunc bool) (protocol.FileInfo, error) {
+	if trunc {
+		var bfi dbproto.FileInfoTruncated
+		err := proto.Unmarshal(bs, &bfi)
+		if err != nil {
+			return protocol.FileInfo{}, err
+		}
+		if err := t.fillTruncated(&bfi); err != nil {
+			return protocol.FileInfo{}, err
+		}
+		return protocol.FileInfoFromDBTruncated(&bfi), nil
+	}
+
+	var bfi bep.FileInfo
+	err := proto.Unmarshal(bs, &bfi)
+	if err != nil {
+		return protocol.FileInfo{}, err
+	}
+	if err := t.fillFileInfo(&bfi); err != nil {
+		return protocol.FileInfo{}, err
+	}
+	return protocol.FileInfoFromDB(&bfi), nil
+}
+
+type blocksIndirectionError struct {
+	err error
+}
+
+func (e *blocksIndirectionError) Error() string {
+	return fmt.Sprintf("filling Blocks: %v", e.err)
+}
+
+func (e *blocksIndirectionError) Unwrap() error {
+	return e.err
+}
+
+// fillFileInfo follows the (possible) indirection of blocks and version
+// vector and fills it out.
+func (t readOnlyTransaction) fillFileInfo(fi *bep.FileInfo) error {
+	var key []byte
+
+	if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
+		// The blocks list is indirected and we need to load it.
+		key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
+		bs, err := t.Get(key)
+		if err != nil {
+			return &blocksIndirectionError{err}
+		}
+		var bl dbproto.BlockList
+		if err := proto.Unmarshal(bs, &bl); err != nil {
+			return err
+		}
+		fi.Blocks = bl.Blocks
+	}
+
+	if len(fi.VersionHash) != 0 {
+		key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
+		bs, err := t.Get(key)
+		if err != nil {
+			return fmt.Errorf("filling Version: %w", err)
+		}
+		var v bep.Vector
+		if err := proto.Unmarshal(bs, &v); err != nil {
+			return err
+		}
+		fi.Version = &v
+	}
+
+	return nil
+}
+
+// fillTruncated follows the (possible) indirection of version vector and
+// fills it.
+func (t readOnlyTransaction) fillTruncated(fi *dbproto.FileInfoTruncated) error {
+	var key []byte
+
+	if len(fi.VersionHash) == 0 {
+		return nil
+	}
+
+	key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
+	bs, err := t.Get(key)
+	if err != nil {
+		return err
+	}
+	var v bep.Vector
+	if err := proto.Unmarshal(bs, &v); err != nil {
+		return err
+	}
+	fi.Version = &v
+	return nil
+}
+
+func (t *readOnlyTransaction) withHaveSequence(folder []byte, startSeq int64, fn Iterator) error {
+	first, err := t.keyer.GenerateSequenceKey(nil, folder, startSeq)
+	if err != nil {
+		return err
+	}
+	last, err := t.keyer.GenerateSequenceKey(nil, folder, maxInt64)
+	if err != nil {
+		return err
+	}
+	dbi, err := t.NewRangeIterator(first, last)
+	if err != nil {
+		return err
+	}
+	defer dbi.Release()
+
+	for dbi.Next() {
+		f, ok, err := t.getFileByKey(dbi.Value())
+		if err != nil {
+			return err
+		}
+		if !ok {
+			continue
+		}
+
+		if !fn(f) {
+			return nil
+		}
+	}
+	return dbi.Error()
+}
diff --git a/internal/db/sqlite/db.go b/internal/db/sqlite/db.go
new file mode 100644
index 00000000000..cb2da41d8b8
--- /dev/null
+++ b/internal/db/sqlite/db.go
@@ -0,0 +1,77 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"sync"
+	"time"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/thejerf/suture/v4"
+)
+
+type DB struct {
+	sql            *sqlx.DB
+	localDeviceIdx int64
+	updateLock     sync.Mutex
+
+	statementsMut sync.RWMutex
+	statements    map[string]*sqlx.Stmt
+	tplInput      map[string]any
+}
+
+var _ db.DB = (*DB)(nil)
+
+func (s *DB) Close() error {
+	s.updateLock.Lock()
+	s.statementsMut.Lock()
+	defer s.updateLock.Unlock()
+	defer s.statementsMut.Unlock()
+	for _, stmt := range s.statements {
+		stmt.Close()
+	}
+	return wrap(s.sql.Close())
+}
+
+func (s *DB) Service(maintenanceInterval time.Duration) suture.Service {
+	return newService(s, maintenanceInterval)
+}
+
+func (s *DB) ListFolders() ([]string, error) {
+	var res []string
+	err := s.stmt(`
+		SELECT folder_id FROM folders
+		ORDER BY folder_id
+	`).Select(&res)
+	return res, wrap(err)
+}
+
+func (s *DB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
+	var res []string
+	err := s.stmt(`
+		SELECT d.device_id FROM counts s
+		INNER JOIN folders o ON o.idx = s.folder_idx
+		INNER JOIN devices d ON d.idx = s.device_idx
+		WHERE o.folder_id = ? AND s.count > 0 AND s.device_idx != {{.LocalDeviceIdx}}
+		GROUP BY d.device_id
+		ORDER BY d.device_id
+	`).Select(&res, folder)
+	if err != nil {
+		return nil, wrap(err)
+	}
+
+	devs := make([]protocol.DeviceID, len(res))
+	for i, s := range res {
+		devs[i], err = protocol.DeviceIDFromString(s)
+		if err != nil {
+			return nil, wrap(err)
+		}
+	}
+	return devs, nil
+}
diff --git a/internal/db/sqlite/db_counts.go b/internal/db/sqlite/db_counts.go
new file mode 100644
index 00000000000..24700dd3fe7
--- /dev/null
+++ b/internal/db/sqlite/db_counts.go
@@ -0,0 +1,137 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+type countsRow struct {
+	Type       protocol.FileInfoType
+	Count      int
+	Size       int64
+	Deleted    bool
+	LocalFlags int64 `db:"local_flags"`
+}
+
+func (s *DB) CountLocal(folder string, device protocol.DeviceID) (db.Counts, error) {
+	var res []countsRow
+	if err := s.stmt(`
+		SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
+		INNER JOIN folders o ON o.idx = s.folder_idx
+		INNER JOIN devices d ON d.idx = s.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ? AND s.local_flags & {{.FlagLocalIgnored}} = 0
+	`).Select(&res, folder, device.String()); err != nil {
+		return db.Counts{}, wrap(err)
+	}
+	return summarizeCounts(res), nil
+}
+
+func (s *DB) CountNeed(folder string, device protocol.DeviceID) (db.Counts, error) {
+	if device == protocol.LocalDeviceID {
+		return s.needSizeLocal(folder)
+	}
+	return s.needSizeRemote(folder, device)
+}
+
+func (s *DB) CountGlobal(folder string) (db.Counts, error) {
+	// Exclude ignored and receive-only changed files from the global count
+	// (legacy expectation? it's a bit weird since those files can in fact
+	// be global and you can get them with GetGlobal etc.)
+	var res []countsRow
+	err := s.stmt(`
+		SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
+		INNER JOIN folders o ON o.idx = s.folder_idx
+		WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalGlobal}} != 0 AND s.local_flags & {{or .FlagLocalReceiveOnly .FlagLocalIgnored}} = 0
+	`).Select(&res, folder)
+	if err != nil {
+		return db.Counts{}, wrap(err)
+	}
+	return summarizeCounts(res), nil
+}
+
+func (s *DB) CountReceiveOnlyChanged(folder string) (db.Counts, error) {
+	var res []countsRow
+	err := s.stmt(`
+		SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
+		INNER JOIN folders o ON o.idx = s.folder_idx
+		WHERE o.folder_id = ? AND local_flags & {{.FlagLocalReceiveOnly}} != 0
+	`).Select(&res, folder)
+	if err != nil {
+		return db.Counts{}, wrap(err)
+	}
+	return summarizeCounts(res), nil
+}
+
+func (s *DB) needSizeLocal(folder string) (db.Counts, error) {
+	// The need size for the local device is the sum of entries with the
+	// need bit set.
+	var res []countsRow
+	err := s.stmt(`
+		SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
+		INNER JOIN folders o ON o.idx = s.folder_idx
+		WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalNeeded}} != 0
+	`).Select(&res, folder)
+	if err != nil {
+		return db.Counts{}, wrap(err)
+	}
+	return summarizeCounts(res), nil
+}
+
+func (s *DB) needSizeRemote(folder string, device protocol.DeviceID) (db.Counts, error) {
+	var res []countsRow
+	// See neededGlobalFilesRemote for commentary as that is the same query without summing
+	if err := s.stmt(`
+		SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
+		INNER JOIN folders o ON o.idx = g.folder_idx
+		WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
+			SELECT 1 FROM FILES f
+			INNER JOIN devices d ON d.idx = f.device_idx
+			WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
+		)
+		GROUP BY g.type, g.local_flags, g.deleted
+
+		UNION ALL
+
+		SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
+		INNER JOIN folders o ON o.idx = g.folder_idx
+		WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
+			SELECT 1 FROM FILES f
+			INNER JOIN devices d ON d.idx = f.device_idx
+			WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
+		)
+		GROUP BY g.type, g.local_flags, g.deleted
+	`).Select(&res, folder, device.String(),
+		folder, device.String()); err != nil {
+		return db.Counts{}, wrap(err)
+	}
+
+	return summarizeCounts(res), nil
+}
+
+func summarizeCounts(res []countsRow) db.Counts {
+	c := db.Counts{
+		DeviceID: protocol.LocalDeviceID,
+	}
+	for _, r := range res {
+		switch {
+		case r.Deleted:
+			c.Deleted += r.Count
+		case r.Type == protocol.FileInfoTypeFile:
+			c.Files += r.Count
+			c.Bytes += r.Size
+		case r.Type == protocol.FileInfoTypeDirectory:
+			c.Directories += r.Count
+			c.Bytes += r.Size
+		case r.Type == protocol.FileInfoTypeSymlink:
+			c.Symlinks += r.Count
+			c.Bytes += r.Size
+		}
+	}
+	return c
+}
diff --git a/internal/db/sqlite/db_global.go b/internal/db/sqlite/db_global.go
new file mode 100644
index 00000000000..2ce7a0c9329
--- /dev/null
+++ b/internal/db/sqlite/db_global.go
@@ -0,0 +1,189 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"iter"
+
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func (s *DB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
+	file = osutil.NormalizedFilename(file)
+
+	var ind indirectFI
+	err := s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
+		INNER JOIN files f on fi.sequence = f.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		WHERE o.folder_id = ? AND f.name = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
+	`).Get(&ind, folder, file)
+	if errors.Is(err, sql.ErrNoRows) {
+		return protocol.FileInfo{}, false, nil
+	}
+	if err != nil {
+		return protocol.FileInfo{}, false, wrap(err)
+	}
+	fi, err := ind.FileInfo()
+	if err != nil {
+		return protocol.FileInfo{}, false, wrap(err)
+	}
+	return fi, true, nil
+}
+
+func (s *DB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
+	file = osutil.NormalizedFilename(file)
+
+	var devStrs []string
+	err := s.stmt(`
+		SELECT d.device_id FROM files f
+		INNER JOIN devices d ON d.idx = f.device_idx
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		INNER JOIN files g ON f.folder_idx = g.folder_idx AND g.version = f.version AND g.name = f.name
+		WHERE o.folder_id = ? AND g.name = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND f.device_idx != {{.LocalDeviceIdx}}
+		ORDER BY d.device_id
+	`).Select(&devStrs, folder, file)
+	if errors.Is(err, sql.ErrNoRows) {
+		return nil, nil
+	}
+	if err != nil {
+		return nil, wrap(err)
+	}
+
+	devs := make([]protocol.DeviceID, 0, len(devStrs))
+	for _, s := range devStrs {
+		d, err := protocol.DeviceIDFromString(s)
+		if err != nil {
+			return nil, wrap(err)
+		}
+		devs = append(devs, d)
+	}
+
+	return devs, nil
+}
+
+func (s *DB) AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error) {
+	it, errFn := iterStructs[db.FileMetadata](s.stmt(`
+		SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		WHERE o.folder_id = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
+		ORDER BY f.name
+	`).Queryx(folder))
+	return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
+		m.Name = osutil.NativeFilename(m.Name)
+		return m, nil
+	})
+}
+
+func (s *DB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[db.FileMetadata], func() error) {
+	if prefix == "" {
+		return s.AllGlobalFiles(folder)
+	}
+
+	prefix = osutil.NormalizedFilename(prefix)
+	end := prefixEnd(prefix)
+
+	it, errFn := iterStructs[db.FileMetadata](s.stmt(`
+		SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		WHERE o.folder_id = ? AND f.name >= ? AND f.name < ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
+		ORDER BY f.name
+	`).Queryx(folder, prefix, end))
+	return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
+		m.Name = osutil.NativeFilename(m.Name)
+		return m, nil
+	})
+}
+
+func (s *DB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
+	var selectOpts string
+	switch order {
+	case config.PullOrderRandom:
+		selectOpts = "ORDER BY RANDOM()"
+	case config.PullOrderAlphabetic:
+		selectOpts = "ORDER BY g.name ASC"
+	case config.PullOrderSmallestFirst:
+		selectOpts = "ORDER BY g.size ASC"
+	case config.PullOrderLargestFirst:
+		selectOpts = "ORDER BY g.size DESC"
+	case config.PullOrderOldestFirst:
+		selectOpts = "ORDER BY g.modified ASC"
+	case config.PullOrderNewestFirst:
+		selectOpts = "ORDER BY g.modified DESC"
+	}
+
+	if limit > 0 {
+		selectOpts += fmt.Sprintf(" LIMIT %d", limit)
+	}
+	if offset > 0 {
+		selectOpts += fmt.Sprintf(" OFFSET %d", offset)
+	}
+
+	if device == protocol.LocalDeviceID {
+		return s.neededGlobalFilesLocal(folder, selectOpts)
+	}
+
+	return s.neededGlobalFilesRemote(folder, device, selectOpts)
+}
+
+func (s *DB) neededGlobalFilesLocal(folder, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
+	// Select all the non-ignored files with the need bit set.
+	it, errFn := iterStructs[indirectFI](s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
+		INNER JOIN files g on fi.sequence = g.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
+		INNER JOIN folders o ON o.idx = g.folder_idx
+		WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalIgnored}} = 0 AND g.local_flags & {{.FlagLocalNeeded}} != 0
+	` + selectOpts).Queryx(folder))
+	return itererr.Map(it, errFn, indirectFI.FileInfo)
+}
+
+func (s *DB) neededGlobalFilesRemote(folder string, device protocol.DeviceID, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
+	// Select:
+	//
+	// - all the valid, non-deleted global files that don't have a corresponding
+	//   remote file with the same version.
+	//
+	// - all the valid, deleted global files that have a corresponding non-deleted
+	//   remote file (of any version)
+
+	it, errFn := iterStructs[indirectFI](s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
+		INNER JOIN files g on fi.sequence = g.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
+		INNER JOIN folders o ON o.idx = g.folder_idx
+		WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
+			SELECT 1 FROM FILES f
+			INNER JOIN devices d ON d.idx = f.device_idx
+			WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
+		)
+
+		UNION ALL
+
+		SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
+		INNER JOIN files g on fi.sequence = g.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
+		INNER JOIN folders o ON o.idx = g.folder_idx
+		WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
+			SELECT 1 FROM FILES f
+			INNER JOIN devices d ON d.idx = f.device_idx
+			WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
+		)
+	`+selectOpts).Queryx(
+		folder, device.String(),
+		folder, device.String(),
+	))
+	return itererr.Map(it, errFn, indirectFI.FileInfo)
+}
diff --git a/internal/db/sqlite/db_indexid.go b/internal/db/sqlite/db_indexid.go
new file mode 100644
index 00000000000..e8f3fa1abf4
--- /dev/null
+++ b/internal/db/sqlite/db_indexid.go
@@ -0,0 +1,163 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"database/sql"
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func (s *DB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
+	// Try a fast read-only query to begin with. If it does not find the ID
+	// we'll do the full thing under a lock.
+	var indexID string
+	if err := s.stmt(`
+		SELECT i.index_id FROM indexids i
+		INNER JOIN folders o ON o.idx  = i.folder_idx
+		INNER JOIN devices d ON d.idx  = i.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ?
+	`).Get(&indexID, folder, device.String()); err == nil && indexID != "" {
+		idx, err := indexIDFromHex(indexID)
+		return idx, wrap(err, "select")
+	}
+	if device != protocol.LocalDeviceID {
+		// For non-local devices we do not create the index ID, so return
+		// zero anyway if we don't have one.
+		return 0, nil
+	}
+
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	// We are now operating only for the local device ID
+
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return 0, wrap(err)
+	}
+
+	if err := s.stmt(`
+		SELECT index_id FROM indexids WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
+	`).Get(&indexID, folderIdx); err != nil && !errors.Is(err, sql.ErrNoRows) {
+		return 0, wrap(err, "select local")
+	}
+
+	if indexID == "" {
+		// Generate a new index ID. Some trickiness in the query as we need
+		// to find the max sequence of local files if there already exist
+		// any.
+		id := protocol.NewIndexID()
+		if _, err := s.stmt(`
+			INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
+				SELECT ?, {{.LocalDeviceIdx}}, ?, COALESCE(MAX(sequence), 0) FROM files
+				WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
+			ON CONFLICT DO UPDATE SET index_id = ?
+		`).Exec(folderIdx, indexIDToHex(id), folderIdx, indexIDToHex(id)); err != nil {
+			return 0, wrap(err, "insert")
+		}
+		return id, nil
+	}
+
+	return indexIDFromHex(indexID)
+}
+
+func (s *DB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err, "folder idx")
+	}
+	deviceIdx, err := s.deviceIdxLocked(device)
+	if err != nil {
+		return wrap(err, "device idx")
+	}
+
+	if _, err := s.stmt(`
+		INSERT OR REPLACE INTO indexids (folder_idx, device_idx, index_id, sequence) values (?, ?, ?, 0)
+	`).Exec(folderIdx, deviceIdx, indexIDToHex(id)); err != nil {
+		return wrap(err, "insert")
+	}
+	return nil
+}
+
+func (s *DB) DropAllIndexIDs() error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	_, err := s.stmt(`DELETE FROM indexids`).Exec()
+	return wrap(err)
+}
+
+func (s *DB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
+	var res sql.NullInt64
+	err := s.stmt(`
+		SELECT sequence FROM indexids i
+		INNER JOIN folders o ON o.idx = i.folder_idx
+		INNER JOIN devices d ON d.idx = i.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ?
+	`).Get(&res, folder, device.String())
+	if errors.Is(err, sql.ErrNoRows) {
+		return 0, nil
+	}
+	if err != nil {
+		return 0, wrap(err)
+	}
+	if !res.Valid {
+		return 0, nil
+	}
+	return res.Int64, nil
+}
+
+func (s *DB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
+	type row struct {
+		Device string
+		Seq    int64
+	}
+
+	it, errFn := iterStructs[row](s.stmt(`
+		SELECT d.device_id AS device, i.sequence AS seq FROM indexids i
+		INNER JOIN folders o ON o.idx = i.folder_idx
+		INNER JOIN devices d ON d.idx = i.device_idx
+		WHERE o.folder_id = ? AND i.device_idx != {{.LocalDeviceIdx}}
+	`).Queryx(folder))
+
+	res := make(map[protocol.DeviceID]int64)
+	for row, err := range itererr.Zip(it, errFn) {
+		if err != nil {
+			return nil, wrap(err)
+		}
+		dev, err := protocol.DeviceIDFromString(row.Device)
+		if err != nil {
+			return nil, wrap(err, "device ID")
+		}
+		res[dev] = row.Seq
+	}
+	return res, nil
+}
+
+func indexIDFromHex(s string) (protocol.IndexID, error) {
+	bs, err := hex.DecodeString(s)
+	if err != nil {
+		return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
+	}
+	var id protocol.IndexID
+	if err := id.Unmarshal(bs); err != nil {
+		return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
+	}
+	return id, nil
+}
+
+func indexIDToHex(i protocol.IndexID) string {
+	bs, _ := i.Marshal()
+	return hex.EncodeToString(bs)
+}
diff --git a/internal/db/sqlite/db_kv.go b/internal/db/sqlite/db_kv.go
new file mode 100644
index 00000000000..077ad3609ec
--- /dev/null
+++ b/internal/db/sqlite/db_kv.go
@@ -0,0 +1,78 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"iter"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/syncthing/syncthing/internal/db"
+)
+
+func (s *DB) GetKV(key string) ([]byte, error) {
+	var val []byte
+	if err := s.stmt(`
+		SELECT value FROM kv
+		WHERE key = ?
+	`).Get(&val, key); err != nil {
+		return nil, wrap(err)
+	}
+	return val, nil
+}
+
+func (s *DB) PutKV(key string, val []byte) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	_, err := s.stmt(`
+		INSERT OR REPLACE INTO kv (key, value)
+		VALUES (?, ?)
+	`).Exec(key, val)
+	return wrap(err)
+}
+
+func (s *DB) DeleteKV(key string) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	_, err := s.stmt(`
+		DELETE FROM kv WHERE key = ?
+	`).Exec(key)
+	return wrap(err)
+}
+
+func (s *DB) PrefixKV(prefix string) (iter.Seq[db.KeyValue], func() error) {
+	var rows *sqlx.Rows
+	var err error
+	if prefix == "" {
+		rows, err = s.stmt(`SELECT key, value FROM kv`).Queryx()
+	} else {
+		end := prefixEnd(prefix)
+		rows, err = s.stmt(`
+			SELECT key, value FROM kv
+			WHERE key >= ? AND key < ?
+		`).Queryx(prefix, end)
+	}
+	if err != nil {
+		return func(_ func(db.KeyValue) bool) {}, func() error { return err }
+	}
+
+	return func(yield func(db.KeyValue) bool) {
+			defer rows.Close()
+			for rows.Next() {
+				var key string
+				var val []byte
+				if err = rows.Scan(&key, &val); err != nil {
+					return
+				}
+				if !yield(db.KeyValue{Key: key, Value: val}) {
+					return
+				}
+			}
+			err = rows.Err()
+		}, func() error {
+			return err
+		}
+}
diff --git a/internal/db/sqlite/db_local.go b/internal/db/sqlite/db_local.go
new file mode 100644
index 00000000000..4b4064757f3
--- /dev/null
+++ b/internal/db/sqlite/db_local.go
@@ -0,0 +1,126 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"iter"
+
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func (s *DB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
+	file = osutil.NormalizedFilename(file)
+
+	var ind indirectFI
+	err := s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
+		INNER JOIN files f on fi.sequence = f.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
+		INNER JOIN devices d ON f.device_idx = d.idx
+		INNER JOIN folders o ON f.folder_idx = o.idx
+		WHERE o.folder_id = ? AND d.device_id = ? AND f.name = ?
+	`).Get(&ind, folder, device.String(), file)
+	if errors.Is(err, sql.ErrNoRows) {
+		return protocol.FileInfo{}, false, nil
+	}
+	if err != nil {
+		return protocol.FileInfo{}, false, wrap(err)
+	}
+	fi, err := ind.FileInfo()
+	if err != nil {
+		return protocol.FileInfo{}, false, wrap(err, "indirect")
+	}
+	return fi, true, nil
+}
+
+func (s *DB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
+	it, errFn := iterStructs[indirectFI](s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
+		INNER JOIN files f on fi.sequence = f.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		INNER JOIN devices d ON d.idx = f.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ?
+	`).Queryx(folder, device.String()))
+	return itererr.Map(it, errFn, indirectFI.FileInfo)
+}
+
+func (s *DB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
+	var limitStr string
+	if limit > 0 {
+		limitStr = fmt.Sprintf(" LIMIT %d", limit)
+	}
+	it, errFn := iterStructs[indirectFI](s.stmt(`
+		SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
+		INNER JOIN files f on fi.sequence = f.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		INNER JOIN devices d ON d.idx = f.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ? AND f.sequence >= ?
+		ORDER BY f.sequence`+limitStr).Queryx(
+		folder, device.String(), startSeq))
+	return itererr.Map(it, errFn, indirectFI.FileInfo)
+}
+
+func (s *DB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
+	if prefix == "" {
+		return s.AllLocalFiles(folder, device)
+	}
+
+	prefix = osutil.NormalizedFilename(prefix)
+	end := prefixEnd(prefix)
+
+	it, errFn := iterStructs[indirectFI](s.sql.Queryx(`
+		SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
+		INNER JOIN files f on fi.sequence = f.sequence
+		LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		INNER JOIN devices d ON d.idx = f.device_idx
+		WHERE o.folder_id = ? AND d.device_id = ? AND f.name >= ? AND f.name < ?
+	`, folder, device.String(), prefix, end))
+	return itererr.Map(it, errFn, indirectFI.FileInfo)
+}
+
+func (s *DB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[db.FileMetadata], func() error) {
+	return iterStructs[db.FileMetadata](s.stmt(`
+		SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		WHERE o.folder_id = ? AND f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
+	`).Queryx(folder, h))
+}
+
+func (s *DB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, db.FileMetadata], func() error) {
+	type row struct {
+		FolderID string `db:"folder_id"`
+		db.FileMetadata
+	}
+	it, errFn := iterStructs[row](s.stmt(`
+		SELECT o.folder_id, f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
+		INNER JOIN folders o ON o.idx = f.folder_idx
+		WHERE f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
+	`).Queryx(h))
+	return itererr.Map2(it, errFn, func(r row) (string, db.FileMetadata, error) {
+		return r.FolderID, r.FileMetadata, nil
+	})
+}
+
+func (s *DB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[db.BlockMapEntry], func() error) {
+	// We involve the files table in this select because deletion of blocks
+	// & blocklists is deferred (garbage collected) while the files list is
+	// not. This filters out blocks that are in fact deleted.
+	return iterStructs[db.BlockMapEntry](s.stmt(`
+		SELECT f.blocklist_hash as blocklisthash, b.idx as blockindex, b.offset, b.size FROM files f
+		LEFT JOIN blocks b ON f.blocklist_hash = b.blocklist_hash
+		WHERE f.device_idx = {{.LocalDeviceIdx}} AND b.hash = ?
+	`).Queryx(hash))
+}
diff --git a/internal/db/sqlite/db_mtimes.go b/internal/db/sqlite/db_mtimes.go
new file mode 100644
index 00000000000..1c3dc7765d2
--- /dev/null
+++ b/internal/db/sqlite/db_mtimes.go
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"time"
+)
+
+func (s *DB) GetMtime(folder, name string) (ondisk, virtual time.Time) {
+	var res struct {
+		Ondisk  int64
+		Virtual int64
+	}
+	if err := s.stmt(`
+		SELECT m.ondisk, m.virtual FROM mtimes m
+		INNER JOIN folders o ON o.idx = m.folder_idx
+		WHERE o.folder_id = ? AND m.name = ?
+	`).Get(&res, folder, name); err != nil {
+		return time.Time{}, time.Time{}
+	}
+	return time.Unix(0, res.Ondisk), time.Unix(0, res.Virtual)
+}
+
+func (s *DB) PutMtime(folder, name string, ondisk, virtual time.Time) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err)
+	}
+	_, err = s.stmt(`
+		INSERT OR REPLACE INTO mtimes (folder_idx, name, ondisk, virtual)
+		VALUES (?, ?, ?, ?)
+	`).Exec(folderIdx, name, ondisk.UnixNano(), virtual.UnixNano())
+	return wrap(err)
+}
+
+func (s *DB) DeleteMtime(folder, name string) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err)
+	}
+	_, err = s.stmt(`
+		DELETE FROM mtimes
+		WHERE folder_idx = ? AND name = ?
+	`).Exec(folderIdx, name)
+	return wrap(err)
+}
diff --git a/internal/db/sqlite/db_open.go b/internal/db/sqlite/db_open.go
new file mode 100644
index 00000000000..9de8e4d1b87
--- /dev/null
+++ b/internal/db/sqlite/db_open.go
@@ -0,0 +1,203 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"database/sql"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"text/template"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/syncthing/syncthing/lib/build"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+const maxDBConns = 128
+
+func Open(path string) (*DB, error) {
+	// Open the database with options to enable foreign keys and recursive
+	// triggers (needed for the delete+insert triggers on row replace).
+	sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
+	if err != nil {
+		return nil, wrap(err)
+	}
+	sqlDB.SetMaxOpenConns(maxDBConns)
+	if _, err := sqlDB.Exec(`PRAGMA journal_mode = WAL`); err != nil {
+		return nil, wrap(err, "PRAGMA journal_mode")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA optimize = 0x10002`); err != nil {
+		// https://www.sqlite.org/pragma.html#pragma_optimize
+		return nil, wrap(err, "PRAGMA optimize")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA journal_size_limit = 6144000`); err != nil {
+		// https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance
+		return nil, wrap(err, "PRAGMA journal_size_limit")
+	}
+	return openCommon(sqlDB)
+}
+
+// Open the database with options suitable for the migration inserts. This
+// is not a safe mode of operation for normal processing, use only for bulk
+// inserts with a close afterwards.
+func OpenForMigration(path string) (*DB, error) {
+	sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
+	if err != nil {
+		return nil, wrap(err, "open")
+	}
+	sqlDB.SetMaxOpenConns(1)
+	if _, err := sqlDB.Exec(`PRAGMA foreign_keys = 0`); err != nil {
+		return nil, wrap(err, "PRAGMA foreign_keys")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA journal_mode = OFF`); err != nil {
+		return nil, wrap(err, "PRAGMA journal_mode")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA synchronous = 0`); err != nil {
+		return nil, wrap(err, "PRAGMA synchronous")
+	}
+	return openCommon(sqlDB)
+}
+
+func OpenTemp() (*DB, error) {
+	// SQLite has a memory mode, but it works differently with concurrency
+	// compared to what we need with the WAL mode. So, no memory databases
+	// for now.
+	dir, err := os.MkdirTemp("", "syncthing-db")
+	if err != nil {
+		return nil, wrap(err)
+	}
+	path := filepath.Join(dir, "db")
+	l.Debugln("Test DB in", path)
+	return Open(path)
+}
+
+func openCommon(sqlDB *sqlx.DB) (*DB, error) {
+	if _, err := sqlDB.Exec(`PRAGMA auto_vacuum = INCREMENTAL`); err != nil {
+		return nil, wrap(err, "PRAGMA auto_vacuum")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA default_temp_store = MEMORY`); err != nil {
+		return nil, wrap(err, "PRAGMA default_temp_store")
+	}
+	if _, err := sqlDB.Exec(`PRAGMA temp_store = MEMORY`); err != nil {
+		return nil, wrap(err, "PRAGMA temp_store")
+	}
+
+	db := &DB{
+		sql:        sqlDB,
+		statements: make(map[string]*sqlx.Stmt),
+	}
+
+	if err := db.runScripts("sql/schema/*"); err != nil {
+		return nil, wrap(err)
+	}
+
+	ver, _ := db.getAppliedSchemaVersion()
+	if ver.SchemaVersion > 0 {
+		filter := func(scr string) bool {
+			scr = filepath.Base(scr)
+			nstr, _, ok := strings.Cut(scr, "-")
+			if !ok {
+				return false
+			}
+			n, err := strconv.ParseInt(nstr, 10, 32)
+			if err != nil {
+				return false
+			}
+			return int(n) > ver.SchemaVersion
+		}
+		if err := db.runScripts("sql/migrations/*", filter); err != nil {
+			return nil, wrap(err)
+		}
+	}
+
+	// Touch device IDs that should always exist and have a low index
+	// numbers, and will never change
+	db.localDeviceIdx, _ = db.deviceIdxLocked(protocol.LocalDeviceID)
+
+	// Set the current schema version, if not already set
+	if err := db.setAppliedSchemaVersion(currentSchemaVersion); err != nil {
+		return nil, wrap(err)
+	}
+
+	db.tplInput = map[string]any{
+		"FlagLocalUnsupported": protocol.FlagLocalUnsupported,
+		"FlagLocalIgnored":     protocol.FlagLocalIgnored,
+		"FlagLocalMustRescan":  protocol.FlagLocalMustRescan,
+		"FlagLocalReceiveOnly": protocol.FlagLocalReceiveOnly,
+		"FlagLocalGlobal":      protocol.FlagLocalGlobal,
+		"FlagLocalNeeded":      protocol.FlagLocalNeeded,
+		"LocalDeviceIdx":       db.localDeviceIdx,
+		"SyncthingVersion":     build.LongVersion,
+	}
+
+	return db, nil
+}
+
+var tplFuncs = template.FuncMap{
+	"or": func(vs ...int) int {
+		v := vs[0]
+		for _, ov := range vs[1:] {
+			v |= ov
+		}
+		return v
+	},
+}
+
+// stmt returns a prepared statement for the given SQL string, after
+// applying local template expansions. The statement is cached.
+func (s *DB) stmt(tpl string) stmt {
+	tpl = strings.TrimSpace(tpl)
+
+	// Fast concurrent lookup of cached statement
+	s.statementsMut.RLock()
+	stmt, ok := s.statements[tpl]
+	s.statementsMut.RUnlock()
+	if ok {
+		return stmt
+	}
+
+	// On miss, take the full lock, check again
+	s.statementsMut.Lock()
+	defer s.statementsMut.Unlock()
+	stmt, ok = s.statements[tpl]
+	if ok {
+		return stmt
+	}
+
+	// Apply template expansions
+	var sb strings.Builder
+	compTpl := template.Must(template.New("tpl").Funcs(tplFuncs).Parse(tpl))
+	if err := compTpl.Execute(&sb, s.tplInput); err != nil {
+		panic("bug: bad template: " + err.Error())
+	}
+
+	// Prepare and cache
+	stmt, err := s.sql.Preparex(sb.String())
+	if err != nil {
+		return failedStmt{err}
+	}
+	s.statements[tpl] = stmt
+	return stmt
+}
+
+type stmt interface {
+	Exec(args ...any) (sql.Result, error)
+	Get(dest any, args ...any) error
+	Queryx(args ...any) (*sqlx.Rows, error)
+	Select(dest any, args ...any) error
+}
+
+type failedStmt struct {
+	err error
+}
+
+func (f failedStmt) Exec(_ ...any) (sql.Result, error)   { return nil, f.err }
+func (f failedStmt) Get(_ any, _ ...any) error           { return f.err }
+func (f failedStmt) Queryx(_ ...any) (*sqlx.Rows, error) { return nil, f.err }
+func (f failedStmt) Select(_ any, _ ...any) error        { return f.err }
diff --git a/internal/db/sqlite/db_open_cgo.go b/internal/db/sqlite/db_open_cgo.go
new file mode 100644
index 00000000000..db86c6b4cb6
--- /dev/null
+++ b/internal/db/sqlite/db_open_cgo.go
@@ -0,0 +1,18 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//go:build cgo
+
+package sqlite
+
+import (
+	_ "github.com/mattn/go-sqlite3" // register sqlite3 database driver
+)
+
+const (
+	dbDriver      = "sqlite3"
+	commonOptions = "_fk=true&_rt=true&_cache_size=-65536&_sync=1&_txlock=immediate"
+)
diff --git a/internal/db/sqlite/db_open_nocgo.go b/internal/db/sqlite/db_open_nocgo.go
new file mode 100644
index 00000000000..ea33222e4ed
--- /dev/null
+++ b/internal/db/sqlite/db_open_nocgo.go
@@ -0,0 +1,23 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//go:build !cgo && !wazero
+
+package sqlite
+
+import (
+	"github.com/syncthing/syncthing/lib/build"
+	_ "modernc.org/sqlite" // register sqlite database driver
+)
+
+const (
+	dbDriver      = "sqlite"
+	commonOptions = "_pragma=foreign_keys(1)&_pragma=recursive_triggers(1)&_pragma=cache_size(-65536)&_pragma=synchronous(1)"
+)
+
+func init() {
+	build.AddTag("modernc-sqlite")
+}
diff --git a/internal/db/sqlite/db_prepared.go b/internal/db/sqlite/db_prepared.go
new file mode 100644
index 00000000000..b06281ea74c
--- /dev/null
+++ b/internal/db/sqlite/db_prepared.go
@@ -0,0 +1,44 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import "github.com/jmoiron/sqlx"
+
+type txPreparedStmts struct {
+	*sqlx.Tx
+	stmts map[string]*sqlx.Stmt
+}
+
+func (p *txPreparedStmts) Preparex(query string) (*sqlx.Stmt, error) {
+	if p.stmts == nil {
+		p.stmts = make(map[string]*sqlx.Stmt)
+	}
+	stmt, ok := p.stmts[query]
+	if ok {
+		return stmt, nil
+	}
+	stmt, err := p.Tx.Preparex(query)
+	if err != nil {
+		return nil, wrap(err)
+	}
+	p.stmts[query] = stmt
+	return stmt, nil
+}
+
+func (p *txPreparedStmts) Commit() error {
+	for _, s := range p.stmts {
+		s.Close()
+	}
+	return p.Tx.Commit()
+}
+
+func (p *txPreparedStmts) Rollback() error {
+	for _, s := range p.stmts {
+		s.Close()
+	}
+	return p.Tx.Rollback()
+}
diff --git a/internal/db/sqlite/db_schema.go b/internal/db/sqlite/db_schema.go
new file mode 100644
index 00000000000..89cebd7ac8a
--- /dev/null
+++ b/internal/db/sqlite/db_schema.go
@@ -0,0 +1,88 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"embed"
+	"io/fs"
+	"strings"
+	"time"
+
+	"github.com/syncthing/syncthing/lib/build"
+)
+
+const currentSchemaVersion = 1
+
+//go:embed sql/**
+var embedded embed.FS
+
+func (s *DB) runScripts(glob string, filter ...func(s string) bool) error {
+	scripts, err := fs.Glob(embedded, glob)
+	if err != nil {
+		return wrap(err)
+	}
+
+	tx, err := s.sql.Begin()
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+
+nextScript:
+	for _, scr := range scripts {
+		for _, fn := range filter {
+			if !fn(scr) {
+				l.Debugln("Skipping script", scr)
+				continue nextScript
+			}
+		}
+		l.Debugln("Executing script", scr)
+		bs, err := fs.ReadFile(embedded, scr)
+		if err != nil {
+			return wrap(err, scr)
+		}
+		// SQLite requires one statement per exec, so we split the init
+		// files on lines containing only a semicolon and execute them
+		// separately. We require it on a separate line because there are
+		// also statement-internal semicolons in the triggers.
+		for _, stmt := range strings.Split(string(bs), "\n;") {
+			if _, err := tx.Exec(stmt); err != nil {
+				return wrap(err, stmt)
+			}
+		}
+	}
+
+	return wrap(tx.Commit())
+}
+
+type schemaVersion struct {
+	SchemaVersion    int
+	AppliedAt        int64
+	SyncthingVersion string
+}
+
+func (s *schemaVersion) AppliedTime() time.Time {
+	return time.Unix(0, s.AppliedAt)
+}
+
+func (s *DB) setAppliedSchemaVersion(ver int) error {
+	_, err := s.stmt(`
+		INSERT OR IGNORE INTO schemamigrations (schema_version, applied_at, syncthing_version)
+		VALUES (?, ?, ?)
+	`).Exec(ver, time.Now().UnixNano(), build.LongVersion)
+	return wrap(err)
+}
+
+func (s *DB) getAppliedSchemaVersion() (schemaVersion, error) {
+	var v schemaVersion
+	err := s.stmt(`
+		SELECT schema_version as schemaversion, applied_at as appliedat, syncthing_version as syncthingversion FROM schemamigrations
+		ORDER BY schema_version DESC
+		LIMIT 1
+	`).Get(&v)
+	return v, wrap(err)
+}
diff --git a/internal/db/sqlite/db_service.go b/internal/db/sqlite/db_service.go
new file mode 100644
index 00000000000..3786fbc105d
--- /dev/null
+++ b/internal/db/sqlite/db_service.go
@@ -0,0 +1,141 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"context"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/db"
+)
+
+const (
+	internalMetaPrefix = "dbsvc"
+	lastMaintKey       = "lastMaint"
+)
+
+type Service struct {
+	sdb                 *DB
+	maintenanceInterval time.Duration
+	internalMeta        *db.Typed
+}
+
+func newService(sdb *DB, maintenanceInterval time.Duration) *Service {
+	return &Service{
+		sdb:                 sdb,
+		maintenanceInterval: maintenanceInterval,
+		internalMeta:        db.NewTyped(sdb, internalMetaPrefix),
+	}
+}
+
+func (s *Service) Serve(ctx context.Context) error {
+	// Run periodic maintenance
+
+	// Figure out when we last ran maintenance and schedule accordingly. If
+	// it was never, do it now.
+	lastMaint, _, _ := s.internalMeta.Time(lastMaintKey)
+	nextMaint := lastMaint.Add(s.maintenanceInterval)
+	wait := time.Until(nextMaint)
+	if wait < 0 {
+		wait = time.Minute
+	}
+	l.Debugln("Next periodic run in", wait)
+
+	timer := time.NewTimer(wait)
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-timer.C:
+		}
+
+		if err := s.periodic(ctx); err != nil {
+			return wrap(err)
+		}
+
+		timer.Reset(s.maintenanceInterval)
+		l.Debugln("Next periodic run in", s.maintenanceInterval)
+		_ = s.internalMeta.PutTime(lastMaintKey, time.Now())
+	}
+}
+
+func (s *Service) periodic(ctx context.Context) error {
+	t0 := time.Now()
+	l.Debugln("Periodic start")
+
+	s.sdb.updateLock.Lock()
+	defer s.sdb.updateLock.Unlock()
+
+	t1 := time.Now()
+	defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
+
+	if err := s.garbageCollectBlocklistsAndBlocksLocked(ctx); err != nil {
+		return wrap(err)
+	}
+
+	_, _ = s.sdb.sql.ExecContext(ctx, `ANALYZE`)
+	_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA optimize`)
+	_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA incremental_vacuum`)
+	_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
+
+	return nil
+}
+
+func (s *Service) garbageCollectBlocklistsAndBlocksLocked(ctx context.Context) error {
+	// Remove all blocklists not referred to by any files and, by extension,
+	// any blocks not referred to by a blocklist. This is an expensive
+	// operation when run normally, especially if there are a lot of blocks
+	// to collect.
+	//
+	// We make this orders of magnitude faster by disabling foreign keys for
+	// the transaction and doing the cleanup manually. This requires using
+	// an explicit connection and disabling foreign keys before starting the
+	// transaction. We make sure to clean up on the way out.
+
+	conn, err := s.sdb.sql.Connx(ctx)
+	if err != nil {
+		return wrap(err)
+	}
+	defer conn.Close()
+
+	if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = 0`); err != nil {
+		return wrap(err)
+	}
+	defer func() { //nolint:contextcheck
+		_, _ = conn.ExecContext(context.Background(), `PRAGMA foreign_keys = 1`)
+	}()
+
+	tx, err := conn.BeginTxx(ctx, nil)
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+
+	if res, err := tx.ExecContext(ctx, `
+		DELETE FROM blocklists
+		WHERE NOT EXISTS (
+			SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
+		)`); err != nil {
+		return wrap(err, "delete blocklists")
+	} else if shouldDebug() {
+		rows, err := res.RowsAffected()
+		l.Debugln("Blocklist GC:", rows, err)
+	}
+
+	if res, err := tx.ExecContext(ctx, `
+		DELETE FROM blocks
+		WHERE NOT EXISTS (
+			SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
+		)`); err != nil {
+		return wrap(err, "delete blocks")
+	} else if shouldDebug() {
+		rows, err := res.RowsAffected()
+		l.Debugln("Blocks GC:", rows, err)
+	}
+
+	return wrap(tx.Commit())
+}
diff --git a/internal/db/sqlite/db_update.go b/internal/db/sqlite/db_update.go
new file mode 100644
index 00000000000..1cfa652c5d3
--- /dev/null
+++ b/internal/db/sqlite/db_update.go
@@ -0,0 +1,549 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"cmp"
+	"context"
+	"fmt"
+	"runtime"
+	"slices"
+	"strings"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/syncthing/syncthing/internal/gen/dbproto"
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/sliceutil"
+	"google.golang.org/protobuf/proto"
+)
+
+func (s *DB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err)
+	}
+	deviceIdx, err := s.deviceIdxLocked(device)
+	if err != nil {
+		return wrap(err)
+	}
+
+	tx, err := s.sql.BeginTxx(context.Background(), nil)
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+	txp := &txPreparedStmts{Tx: tx}
+
+	//nolint:sqlclosecheck
+	insertFileStmt, err := txp.Preparex(`
+		INSERT OR REPLACE INTO files (folder_idx, device_idx, remote_sequence, name, type, modified, size, version, deleted, invalid, local_flags, blocklist_hash)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+		RETURNING sequence
+	`)
+	if err != nil {
+		return wrap(err, "prepare insert file")
+	}
+
+	//nolint:sqlclosecheck
+	insertFileInfoStmt, err := txp.Preparex(`
+		INSERT INTO fileinfos (sequence, fiprotobuf)
+		VALUES (?, ?)
+	`)
+	if err != nil {
+		return wrap(err, "prepare insert fileinfo")
+	}
+
+	//nolint:sqlclosecheck
+	insertBlockListStmt, err := txp.Preparex(`
+		INSERT OR IGNORE INTO blocklists (blocklist_hash, blprotobuf)
+		VALUES (?, ?)
+	`)
+	if err != nil {
+		return wrap(err, "prepare insert blocklist")
+	}
+
+	var prevRemoteSeq int64
+	for i, f := range fs {
+		f.Name = osutil.NormalizedFilename(f.Name)
+
+		var blockshash *[]byte
+		if len(f.Blocks) > 0 {
+			f.BlocksHash = protocol.BlocksHash(f.Blocks)
+			blockshash = &f.BlocksHash
+		} else {
+			f.BlocksHash = nil
+		}
+
+		if f.Type == protocol.FileInfoTypeDirectory {
+			f.Size = 128 // synthetic directory size
+		}
+
+		// Insert the file.
+		//
+		// If it is a remote file, set remote_sequence otherwise leave it at
+		// null. Returns the new local sequence.
+		var remoteSeq *int64
+		if device != protocol.LocalDeviceID {
+			if i > 0 && f.Sequence == prevRemoteSeq {
+				return fmt.Errorf("duplicate remote sequence number %d", prevRemoteSeq)
+			}
+			prevRemoteSeq = f.Sequence
+			remoteSeq = &f.Sequence
+		}
+		var localSeq int64
+		if err := insertFileStmt.Get(&localSeq, folderIdx, deviceIdx, remoteSeq, f.Name, f.Type, f.ModTime().UnixNano(), f.Size, f.Version.String(), f.IsDeleted(), f.IsInvalid(), f.LocalFlags, blockshash); err != nil {
+			return wrap(err, "insert file")
+		}
+
+		if len(f.Blocks) > 0 {
+			// Indirect the block list
+			blocks := sliceutil.Map(f.Blocks, protocol.BlockInfo.ToWire)
+			bs, err := proto.Marshal(&dbproto.BlockList{Blocks: blocks})
+			if err != nil {
+				return wrap(err, "marshal blocklist")
+			}
+			if _, err := insertBlockListStmt.Exec(f.BlocksHash, bs); err != nil {
+				return wrap(err, "insert blocklist")
+			}
+
+			if device == protocol.LocalDeviceID {
+				// Insert all blocks
+				if err := s.insertBlocksLocked(txp, f.BlocksHash, f.Blocks); err != nil {
+					return wrap(err, "insert blocks")
+				}
+			}
+
+			f.Blocks = nil
+		}
+
+		// Insert the fileinfo
+		if device == protocol.LocalDeviceID {
+			f.Sequence = localSeq
+		}
+		bs, err := proto.Marshal(f.ToWire(true))
+		if err != nil {
+			return wrap(err, "marshal fileinfo")
+		}
+		if _, err := insertFileInfoStmt.Exec(localSeq, bs); err != nil {
+			return wrap(err, "insert fileinfo")
+		}
+
+		// Update global and need
+		if err := s.recalcGlobalForFile(txp, folderIdx, f.Name); err != nil {
+			return wrap(err)
+		}
+	}
+
+	return wrap(tx.Commit())
+}
+
+func (s *DB) DropFolder(folder string) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+	_, err := s.stmt(`
+		DELETE FROM folders
+		WHERE folder_id = ?
+	`).Exec(folder)
+	return wrap(err)
+}
+
+func (s *DB) DropDevice(device protocol.DeviceID) error {
+	if device == protocol.LocalDeviceID {
+		panic("bug: cannot drop local device")
+	}
+
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	deviceIdx, err := s.deviceIdxLocked(device)
+	if err != nil {
+		return wrap(err)
+	}
+
+	tx, err := s.sql.BeginTxx(context.Background(), nil)
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+	txp := &txPreparedStmts{Tx: tx}
+
+	// Find all folders where the device is involved
+	var folderIdxs []int64
+	if err := tx.Select(&folderIdxs, `
+		SELECT folder_idx
+		FROM counts
+		WHERE device_idx = ? AND count > 0
+		GROUP BY folder_idx
+	`, deviceIdx); err != nil {
+		return wrap(err)
+	}
+
+	// Drop the device, which cascades to delete all files etc for it
+	if _, err := tx.Exec(`DELETE FROM devices WHERE device_id = ?`, device.String()); err != nil {
+		return wrap(err)
+	}
+
+	// Recalc the globals for all affected folders
+	for _, idx := range folderIdxs {
+		if err := s.recalcGlobalForFolder(txp, idx); err != nil {
+			return wrap(err)
+		}
+	}
+
+	return wrap(tx.Commit())
+}
+
+func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	// This is a two part operation, first dropping all the files and then
+	// recalculating the global state for the entire folder.
+
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err)
+	}
+	deviceIdx, err := s.deviceIdxLocked(device)
+	if err != nil {
+		return wrap(err)
+	}
+
+	tx, err := s.sql.BeginTxx(context.Background(), nil)
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+	txp := &txPreparedStmts{Tx: tx}
+
+	// Drop all the file entries
+
+	result, err := tx.Exec(`
+		DELETE FROM files
+		WHERE folder_idx = ? AND device_idx = ?
+	`, folderIdx, deviceIdx)
+	if err != nil {
+		return wrap(err)
+	}
+	if n, err := result.RowsAffected(); err == nil && n == 0 {
+		// The delete affected no rows, so we don't need to redo the entire
+		// global/need calculation.
+		return wrap(tx.Commit())
+	}
+
+	// Recalc global for the entire folder
+
+	if err := s.recalcGlobalForFolder(txp, folderIdx); err != nil {
+		return wrap(err)
+	}
+	return wrap(tx.Commit())
+}
+
+func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
+	for i := range names {
+		names[i] = osutil.NormalizedFilename(names[i])
+	}
+
+	s.updateLock.Lock()
+	defer s.updateLock.Unlock()
+
+	folderIdx, err := s.folderIdxLocked(folder)
+	if err != nil {
+		return wrap(err)
+	}
+	deviceIdx, err := s.deviceIdxLocked(device)
+	if err != nil {
+		return wrap(err)
+	}
+
+	tx, err := s.sql.BeginTxx(context.Background(), nil)
+	if err != nil {
+		return wrap(err)
+	}
+	defer tx.Rollback() //nolint:errcheck
+	txp := &txPreparedStmts{Tx: tx}
+
+	// Drop the named files
+
+	query, args, err := sqlx.In(`
+		DELETE FROM files
+		WHERE folder_idx = ? AND device_idx = ? AND name IN (?)
+	`, folderIdx, deviceIdx, names)
+	if err != nil {
+		return wrap(err)
+	}
+	if _, err := tx.Exec(query, args...); err != nil {
+		return wrap(err)
+	}
+
+	// Recalc globals for the named files
+
+	for _, name := range names {
+		if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
+			return wrap(err)
+		}
+	}
+
+	return wrap(tx.Commit())
+}
+
+func (*DB) insertBlocksLocked(tx *txPreparedStmts, blocklistHash []byte, blocks []protocol.BlockInfo) error {
+	if len(blocks) == 0 {
+		return nil
+	}
+	bs := make([]map[string]any, len(blocks))
+	for i, b := range blocks {
+		bs[i] = map[string]any{
+			"hash":           b.Hash,
+			"blocklist_hash": blocklistHash,
+			"idx":            i,
+			"offset":         b.Offset,
+			"size":           b.Size,
+		}
+	}
+	_, err := tx.NamedExec(`
+		INSERT OR IGNORE INTO blocks (hash, blocklist_hash, idx, offset, size)
+		VALUES (:hash, :blocklist_hash, :idx, :offset, :size)
+	`, bs)
+	return wrap(err)
+}
+
+func (s *DB) recalcGlobalForFolder(txp *txPreparedStmts, folderIdx int64) error {
+	// Select files where there is no global, those are the ones we need to
+	// recalculate.
+	//nolint:sqlclosecheck
+	namesStmt, err := txp.Preparex(`
+		SELECT f.name FROM files f
+		WHERE f.folder_idx = ? AND NOT EXISTS (
+			SELECT 1 FROM files g
+			WHERE g.folder_idx = ? AND g.name = f.name AND g.local_flags & ? != 0
+		)
+		GROUP BY name
+	`)
+	if err != nil {
+		return wrap(err)
+	}
+	rows, err := namesStmt.Queryx(folderIdx, folderIdx, protocol.FlagLocalGlobal)
+	if err != nil {
+		return wrap(err)
+	}
+	defer rows.Close()
+	for rows.Next() {
+		var name string
+		if err := rows.Scan(&name); err != nil {
+			return wrap(err)
+		}
+		if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
+			return wrap(err)
+		}
+	}
+	return wrap(rows.Err())
+}
+
+func (s *DB) recalcGlobalForFile(txp *txPreparedStmts, folderIdx int64, file string) error {
+	//nolint:sqlclosecheck
+	selStmt, err := txp.Preparex(`
+		SELECT name, folder_idx, device_idx, sequence, modified, version, deleted, invalid, local_flags FROM files
+		WHERE folder_idx = ? AND name = ?
+	`)
+	if err != nil {
+		return wrap(err)
+	}
+	es, err := itererr.Collect(iterStructs[fileRow](selStmt.Queryx(folderIdx, file)))
+	if err != nil {
+		return wrap(err)
+	}
+	if len(es) == 0 {
+		// shouldn't happen
+		return nil
+	}
+
+	// Sort the entries; the global entry is at the head of the list
+	slices.SortFunc(es, fileRow.Compare)
+
+	// The global version is the first one in the list that is not invalid,
+	// or just the first one in the list if all are invalid.
+	var global fileRow
+	globIdx := slices.IndexFunc(es, func(e fileRow) bool { return !e.Invalid })
+	if globIdx < 0 {
+		globIdx = 0
+	}
+	global = es[globIdx]
+
+	// We "have" the file if the position in the list of versions is at the
+	// global version or better, or if the version is the same as the global
+	// file (we might be further down the list due to invalid flags), or if
+	// the global is deleted and we don't have it at all...
+	localIdx := slices.IndexFunc(es, func(e fileRow) bool { return e.DeviceIdx == s.localDeviceIdx })
+	hasLocal := localIdx >= 0 && localIdx <= globIdx || // have a better or equal version
+		localIdx >= 0 && es[localIdx].Version.Equal(global.Version.Vector) || // have an equal version but invalid/ignored
+		localIdx < 0 && global.Deleted // missing it, but the global is also deleted
+
+	// Set the global flag on the global entry. Set the need flag if the
+	// local device needs this file, unless it's invalid.
+	global.LocalFlags |= protocol.FlagLocalGlobal
+	if hasLocal || global.Invalid {
+		global.LocalFlags &= ^protocol.FlagLocalNeeded
+	} else {
+		global.LocalFlags |= protocol.FlagLocalNeeded
+	}
+	//nolint:sqlclosecheck
+	upStmt, err := txp.Prepare(`
+		UPDATE files SET local_flags = ?
+		WHERE folder_idx = ? AND device_idx = ? AND sequence = ?
+	`)
+	if err != nil {
+		return wrap(err)
+	}
+	if _, err := upStmt.Exec(global.LocalFlags, global.FolderIdx, global.DeviceIdx, global.Sequence); err != nil {
+		return wrap(err)
+	}
+
+	// Clear the need and global flags on all other entries
+	//nolint:sqlclosecheck
+	upStmt, err = txp.Prepare(`
+		UPDATE files SET local_flags = local_flags & ?
+		WHERE folder_idx = ? AND name = ? AND sequence != ? AND local_flags & ? != 0
+	`)
+	if err != nil {
+		return wrap(err)
+	}
+	if _, err := upStmt.Exec(^(protocol.FlagLocalNeeded | protocol.FlagLocalGlobal), folderIdx, global.Name, global.Sequence, protocol.FlagLocalNeeded|protocol.FlagLocalGlobal); err != nil {
+		return wrap(err)
+	}
+
+	return nil
+}
+
+func (s *DB) folderIdxLocked(folderID string) (int64, error) {
+	if _, err := s.stmt(`
+		INSERT OR IGNORE INTO folders(folder_id)
+		VALUES (?)
+	`).Exec(folderID); err != nil {
+		return 0, wrap(err)
+	}
+	var idx int64
+	if err := s.stmt(`
+		SELECT idx FROM folders
+		WHERE folder_id = ?
+	`).Get(&idx, folderID); err != nil {
+		return 0, wrap(err)
+	}
+
+	return idx, nil
+}
+
+func (s *DB) deviceIdxLocked(deviceID protocol.DeviceID) (int64, error) {
+	devStr := deviceID.String()
+	if _, err := s.stmt(`
+		INSERT OR IGNORE INTO devices(device_id)
+		VALUES (?)
+	`).Exec(devStr); err != nil {
+		return 0, wrap(err)
+	}
+	var idx int64
+	if err := s.stmt(`
+		SELECT idx FROM devices
+		WHERE device_id = ?
+	`).Get(&idx, devStr); err != nil {
+		return 0, wrap(err)
+	}
+
+	return idx, nil
+}
+
+// wrap returns the error wrapped with the calling function name and
+// optional extra context strings as prefix. A nil error wraps to nil.
+func wrap(err error, context ...string) error {
+	if err == nil {
+		return nil
+	}
+
+	prefix := "error"
+	pc, _, _, ok := runtime.Caller(1)
+	details := runtime.FuncForPC(pc)
+	if ok && details != nil {
+		prefix = strings.ToLower(details.Name())
+		if dotIdx := strings.LastIndex(prefix, "."); dotIdx > 0 {
+			prefix = prefix[dotIdx+1:]
+		}
+	}
+
+	if len(context) > 0 {
+		for i := range context {
+			context[i] = strings.TrimSpace(context[i])
+		}
+		extra := strings.Join(context, ", ")
+		return fmt.Errorf("%s (%s): %w", prefix, extra, err)
+	}
+
+	return fmt.Errorf("%s: %w", prefix, err)
+}
+
+type fileRow struct {
+	Name       string
+	Version    dbVector
+	FolderIdx  int64 `db:"folder_idx"`
+	DeviceIdx  int64 `db:"device_idx"`
+	Sequence   int64
+	Modified   int64
+	Size       int64
+	LocalFlags int64 `db:"local_flags"`
+	Deleted    bool
+	Invalid    bool
+}
+
+func (e fileRow) Compare(other fileRow) int {
+	// From FileInfo.WinsConflict
+	vc := e.Version.Vector.Compare(other.Version.Vector)
+	switch vc {
+	case protocol.Equal:
+		if e.Invalid != other.Invalid {
+			if e.Invalid {
+				return 1
+			}
+			return -1
+		}
+
+		// Compare the device ID index, lower is better. This is only
+		// deterministic to the extent that LocalDeviceID will always be the
+		// lowest one, order between remote devices is random (and
+		// irrelevant).
+		return cmp.Compare(e.DeviceIdx, other.DeviceIdx)
+	case protocol.Greater: // we are newer
+		return -1
+	case protocol.Lesser: // we are older
+		return 1
+	case protocol.ConcurrentGreater, protocol.ConcurrentLesser: // there is a conflict
+		if e.Invalid != other.Invalid {
+			if e.Invalid { // we are invalid, we lose
+				return 1
+			}
+			return -1 // they are invalid, we win
+		}
+		if e.Deleted != other.Deleted {
+			if e.Deleted { // we are deleted, we lose
+				return 1
+			}
+			return -1 // they are deleted, we win
+		}
+		if d := cmp.Compare(e.Modified, other.Modified); d != 0 {
+			return -d // positive d means we were newer, so we win (negative return)
+		}
+		if vc == protocol.ConcurrentGreater {
+			return -1 // we have a better device ID, we win
+		}
+		return 1 // they win
+	default:
+		return 0
+	}
+}
diff --git a/lib/db/debug.go b/internal/db/sqlite/debug.go
similarity index 58%
rename from lib/db/debug.go
rename to internal/db/sqlite/debug.go
index 26abbaf0129..acc711a2e85 100644
--- a/lib/db/debug.go
+++ b/internal/db/sqlite/debug.go
@@ -1,17 +1,15 @@
-// Copyright (C) 2014 The Syncthing Authors.
+// Copyright (C) 2025 The Syncthing Authors.
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package db
+package sqlite
 
 import (
 	"github.com/syncthing/syncthing/lib/logger"
 )
 
-var l = logger.DefaultLogger.NewFacility("db", "The database layer")
+var l = logger.DefaultLogger.NewFacility("sqlite", "SQLite database")
 
-func shouldDebug() bool {
-	return l.ShouldDebug("db")
-}
+func shouldDebug() bool { return l.ShouldDebug("sqlite") }
diff --git a/internal/db/sqlite/sql/README.md b/internal/db/sqlite/sql/README.md
new file mode 100644
index 00000000000..42929fd5167
--- /dev/null
+++ b/internal/db/sqlite/sql/README.md
@@ -0,0 +1,8 @@
+These SQL scripts are embedded in the binary.
+
+Scripts in `schema/` are run at every startup, in alphanumerical order.
+
+Scripts in `migrations/` are run when a migration is needed; the must begin
+with a number that equals the schema version that results from that
+migration. Migrations are not run on initial database creation, so the
+scripts in `schema/` should create the latest version.
diff --git a/internal/db/sqlite/sql/migrations/01-placeholder.sql b/internal/db/sqlite/sql/migrations/01-placeholder.sql
new file mode 100644
index 00000000000..30ec3e8c3cd
--- /dev/null
+++ b/internal/db/sqlite/sql/migrations/01-placeholder.sql
@@ -0,0 +1,7 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- The next migration should be number two.
\ No newline at end of file
diff --git a/internal/db/sqlite/sql/schema/00-indexes.sql b/internal/db/sqlite/sql/schema/00-indexes.sql
new file mode 100644
index 00000000000..1152d3b36ed
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/00-indexes.sql
@@ -0,0 +1,19 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- folders map folder IDs as used by Syncthing to database folder indexes
+CREATE TABLE IF NOT EXISTS folders (
+    idx INTEGER NOT NULL PRIMARY KEY,
+    folder_id TEXT NOT NULL UNIQUE COLLATE BINARY
+) STRICT
+;
+
+-- devices map device IDs as used by Syncthing to database device indexes
+CREATE TABLE IF NOT EXISTS devices (
+    idx INTEGER NOT NULL PRIMARY KEY,
+    device_id TEXT NOT NULL UNIQUE COLLATE BINARY
+) STRICT
+;
diff --git a/internal/db/sqlite/sql/schema/10-schema.sql b/internal/db/sqlite/sql/schema/10-schema.sql
new file mode 100644
index 00000000000..ce636dbd43b
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/10-schema.sql
@@ -0,0 +1,14 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- Schema migrations hold the list of historical migrations applied
+CREATE TABLE IF NOT EXISTS schemamigrations (
+    schema_version INTEGER NOT NULL,
+    applied_at INTEGER NOT NULL, -- unix nanos
+    syncthing_version TEXT NOT NULL COLLATE BINARY,
+    PRIMARY KEY(schema_version)
+) STRICT
+;
diff --git a/internal/db/sqlite/sql/schema/20-files.sql b/internal/db/sqlite/sql/schema/20-files.sql
new file mode 100644
index 00000000000..ca16be9c3bc
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/20-files.sql
@@ -0,0 +1,62 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- Files
+--
+-- The files table contains all files announced by any device. Files present
+-- on this device are filed under the LocalDeviceID, not the actual current
+-- device ID, for simplicity, consistency and portability. One announced
+-- version of each file is considered the "global" version - the latest one,
+-- that all other devices strive to replicate. This instance gets the Global
+-- flag bit set. There may be other identical instances of this file
+-- announced by other devices, but only one onstance gets the Global flag;
+-- this simplifies accounting. If the current device has the Global version,
+-- the LocalDeviceID instance of the file is the one that has the Global
+-- bit.
+--
+-- If the current device does not have that version of the file it gets the
+-- Need bit set. Only Global files announced by another device can have the
+-- Need bit. This allows for very efficient lookup of files needing handling
+-- on this device, which is a common query.
+CREATE TABLE IF NOT EXISTS files (
+    folder_idx INTEGER NOT NULL,
+    device_idx INTEGER NOT NULL, -- actual device ID or LocalDeviceID
+    sequence INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- our local database sequence, for each and every entry
+    remote_sequence INTEGER, -- remote device's sequence number, null for local or synthetic entries
+    name TEXT NOT NULL COLLATE BINARY,
+    type INTEGER NOT NULL, -- protocol.FileInfoType
+    modified INTEGER NOT NULL, -- Unix nanos
+    size INTEGER NOT NULL,
+    version TEXT NOT NULL COLLATE BINARY,
+    deleted INTEGER NOT NULL, -- boolean
+    invalid INTEGER NOT NULL, -- boolean
+    local_flags INTEGER NOT NULL,
+    blocklist_hash BLOB, -- null when there are no blocks
+    FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
+    FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
+) STRICT
+;
+-- FileInfos store the actual protobuf object. We do this separately to keep
+-- the files rows smaller and more efficient.
+CREATE TABLE IF NOT EXISTS fileinfos (
+    sequence INTEGER NOT NULL PRIMARY KEY, -- our local database sequence from the files table
+    fiprotobuf BLOB NOT NULL,
+    FOREIGN KEY(sequence) REFERENCES files(sequence) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+) STRICT
+;
+-- There can be only one file per folder, device, and remote sequence number
+CREATE UNIQUE INDEX IF NOT EXISTS files_remote_sequence ON files (folder_idx, device_idx, remote_sequence)
+    WHERE remote_sequence IS NOT NULL
+;
+-- There can be only one file per folder, device, and name
+CREATE UNIQUE INDEX IF NOT EXISTS files_device_name ON files (folder_idx, device_idx, name)
+;
+-- We want to be able to look up & iterate files based on just folder and name
+CREATE INDEX IF NOT EXISTS files_name_only ON files (folder_idx, name)
+;
+-- We want to be able to look up & iterate files based on blocks hash
+CREATE INDEX IF NOT EXISTS files_blocklist_hash_only ON files (blocklist_hash, device_idx, folder_idx) WHERE blocklist_hash IS NOT NULL
+;
diff --git a/internal/db/sqlite/sql/schema/30-indexids.sql b/internal/db/sqlite/sql/schema/30-indexids.sql
new file mode 100644
index 00000000000..dde9851bc87
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/30-indexids.sql
@@ -0,0 +1,24 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- indexids holds the index ID and maximum sequence for a given device and folder
+CREATE TABLE IF NOT EXISTS indexids (
+    device_idx INTEGER NOT NULL,
+    folder_idx INTEGER NOT NULL,
+    index_id TEXT NOT NULL COLLATE BINARY,
+    sequence INTEGER NOT NULL DEFAULT 0,
+    PRIMARY KEY(device_idx, folder_idx),
+    FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE,
+    FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID
+;
+CREATE TRIGGER IF NOT EXISTS indexids_seq AFTER INSERT ON files
+BEGIN
+    INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
+        VALUES (NEW.folder_idx, NEW.device_idx, "", COALESCE(NEW.remote_sequence, NEW.sequence))
+        ON CONFLICT DO UPDATE SET sequence = COALESCE(NEW.remote_sequence, NEW.sequence);
+END
+;
diff --git a/internal/db/sqlite/sql/schema/40-counts.sql b/internal/db/sqlite/sql/schema/40-counts.sql
new file mode 100644
index 00000000000..cac1851fbcd
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/40-counts.sql
@@ -0,0 +1,53 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- Counts
+--
+-- Counts and sizes are maintained for each device, folder, type, flag bits
+-- combination.
+CREATE TABLE IF NOT EXISTS counts (
+    folder_idx INTEGER NOT NULL,
+    device_idx INTEGER NOT NULL,
+    type INTEGER NOT NULL,
+    local_flags INTEGER NOT NULL,
+    count INTEGER NOT NULL,
+    size INTEGER NOT NULL,
+    deleted INTEGER NOT NULL, -- boolean
+    PRIMARY KEY(folder_idx, device_idx, type, local_flags, deleted),
+    FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
+    FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID
+;
+
+--- Maintain counts when files are added and removed using triggers
+
+CREATE TRIGGER IF NOT EXISTS counts_insert AFTER INSERT ON files
+BEGIN
+    INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
+        VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
+        ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
+END
+;
+CREATE TRIGGER IF NOT EXISTS counts_delete AFTER DELETE ON files
+BEGIN
+    UPDATE counts SET count = count - 1, size = size - OLD.size
+        WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
+END
+;
+CREATE TRIGGER IF NOT EXISTS counts_update AFTER UPDATE OF local_flags ON files
+WHEN NEW.local_flags != OLD.local_flags
+BEGIN
+    INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
+        VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
+        ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
+    UPDATE counts SET count = count - 1, size = size - OLD.size
+        WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
+END
+;
+DROP TRIGGER IF EXISTS counts_update_add -- tmp migration
+;
+DROP TRIGGER IF EXISTS counts_update_del -- tmp migration
+;
diff --git a/internal/db/sqlite/sql/schema/50-blocks.sql b/internal/db/sqlite/sql/schema/50-blocks.sql
new file mode 100644
index 00000000000..17c8e40a0dc
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/50-blocks.sql
@@ -0,0 +1,34 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+-- Block lists
+--
+-- The block lists are extracted from FileInfos and stored separately. This
+-- reduces the database size by reusing the same block list entry for all
+-- devices announcing the same file. Doing it for all block lists instead of
+-- using a size cutoff simplifies queries. Block lists are garbage collected
+-- "manually", not using a trigger as that was too performance impacting.
+CREATE TABLE IF NOT EXISTS blocklists (
+    blocklist_hash BLOB NOT NULL PRIMARY KEY,
+    blprotobuf BLOB NOT NULL
+) STRICT
+;
+
+-- Blocks
+--
+-- For all local files we store the blocks individually for quick lookup. A
+-- given block can exist in multiple blocklists and at multiple offsets in a
+-- blocklist.
+CREATE TABLE IF NOT EXISTS blocks (
+    hash BLOB NOT NULL,
+    blocklist_hash BLOB NOT NULL,
+    idx INTEGER NOT NULL,
+    offset INTEGER NOT NULL,
+    size INTEGER NOT NULL,
+    PRIMARY KEY (hash, blocklist_hash, idx),
+    FOREIGN KEY(blocklist_hash) REFERENCES blocklists(blocklist_hash) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
+) STRICT
+;
diff --git a/internal/db/sqlite/sql/schema/50-mtimes.sql b/internal/db/sqlite/sql/schema/50-mtimes.sql
new file mode 100644
index 00000000000..1d6f6b5bafd
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/50-mtimes.sql
@@ -0,0 +1,16 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+--- Backing for the MtimeFS
+CREATE TABLE IF NOT EXISTS mtimes (
+    folder_idx INTEGER NOT NULL,
+    name TEXT NOT NULL,
+    ondisk INTEGER NOT NULL, -- unix nanos
+    virtual INTEGER NOT NULL, -- unix nanos
+    PRIMARY KEY(folder_idx, name),
+    FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
+) STRICT, WITHOUT ROWID
+;
diff --git a/internal/db/sqlite/sql/schema/70-kv.sql b/internal/db/sqlite/sql/schema/70-kv.sql
new file mode 100644
index 00000000000..e723ef46786
--- /dev/null
+++ b/internal/db/sqlite/sql/schema/70-kv.sql
@@ -0,0 +1,13 @@
+-- Copyright (C) 2025 The Syncthing Authors.
+--
+-- This Source Code Form is subject to the terms of the Mozilla Public
+-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
+-- You can obtain one at https://mozilla.org/MPL/2.0/.
+
+--- Simple KV store. This backs the "miscDB" we use for certain minor pieces
+--  of data.
+CREATE TABLE IF NOT EXISTS kv (
+    key TEXT NOT NULL PRIMARY KEY COLLATE BINARY,
+    value BLOB NOT NULL
+) STRICT
+;
diff --git a/internal/db/sqlite/util.go b/internal/db/sqlite/util.go
new file mode 100644
index 00000000000..3d734d71171
--- /dev/null
+++ b/internal/db/sqlite/util.go
@@ -0,0 +1,117 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"database/sql/driver"
+	"errors"
+	"iter"
+
+	"github.com/jmoiron/sqlx"
+	"github.com/syncthing/syncthing/internal/gen/bep"
+	"github.com/syncthing/syncthing/internal/gen/dbproto"
+	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/protocol"
+	"google.golang.org/protobuf/proto"
+)
+
+// iterStructs returns an iterator over the given struct type by scanning
+// the SQL rows. `rows` is closed when the iterator exits.
+func iterStructs[T any](rows *sqlx.Rows, err error) (iter.Seq[T], func() error) {
+	if err != nil {
+		return func(_ func(T) bool) {}, func() error { return err }
+	}
+
+	var retErr error
+	return func(yield func(T) bool) {
+		defer rows.Close()
+		for rows.Next() {
+			v := new(T)
+			if err := rows.StructScan(v); err != nil {
+				retErr = err
+				break
+			}
+			if cleanuper, ok := any(v).(interface{ cleanup() }); ok {
+				cleanuper.cleanup()
+			}
+			if !yield(*v) {
+				return
+			}
+		}
+		if err := rows.Err(); err != nil && retErr == nil {
+			retErr = err
+		}
+	}, func() error { return retErr }
+}
+
+// dbVector is a wrapper that allows protocol.Vector values to be serialized
+// to and from the database.
+type dbVector struct { //nolint:recvcheck
+	protocol.Vector
+}
+
+func (v dbVector) Value() (driver.Value, error) {
+	return v.String(), nil
+}
+
+func (v *dbVector) Scan(value any) error {
+	str, ok := value.(string)
+	if !ok {
+		return errors.New("not a string")
+	}
+	if str == "" {
+		v.Vector = protocol.Vector{}
+		return nil
+	}
+	vec, err := protocol.VectorFromString(str)
+	if err != nil {
+		return wrap(err)
+	}
+	v.Vector = vec
+
+	return nil
+}
+
+// indirectFI constructs a FileInfo from separate marshalled FileInfo and
+// BlockList bytes.
+type indirectFI struct {
+	Name       string // not used, must be present as dest for Need iterator
+	FiProtobuf []byte
+	BlProtobuf []byte
+	Size       int64 // not used
+	Modified   int64 // not used
+}
+
+func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
+	var fi bep.FileInfo
+	if err := proto.Unmarshal(i.FiProtobuf, &fi); err != nil {
+		return protocol.FileInfo{}, wrap(err, "unmarshal fileinfo")
+	}
+	if len(i.BlProtobuf) > 0 {
+		var bl dbproto.BlockList
+		if err := proto.Unmarshal(i.BlProtobuf, &bl); err != nil {
+			return protocol.FileInfo{}, wrap(err, "unmarshal blocklist")
+		}
+		fi.Blocks = bl.Blocks
+	}
+	fi.Name = osutil.NativeFilename(fi.Name)
+	return protocol.FileInfoFromDB(&fi), nil
+}
+
+func prefixEnd(s string) string {
+	if s == "" {
+		panic("bug: cannot represent end prefix for empty string")
+	}
+	bs := []byte(s)
+	for i := len(bs) - 1; i >= 0; i-- {
+		if bs[i] < 0xff {
+			bs[i]++
+			break
+		}
+	}
+	return string(bs)
+}
diff --git a/internal/db/typed.go b/internal/db/typed.go
new file mode 100644
index 00000000000..75ed88921bd
--- /dev/null
+++ b/internal/db/typed.go
@@ -0,0 +1,140 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db
+
+import (
+	"database/sql"
+	"encoding/binary"
+	"errors"
+	"time"
+)
+
+// Typed is a simple key-value store using a specific namespace within a
+// lower level KV.
+type Typed struct {
+	db     KV
+	prefix string
+}
+
+func NewMiscDB(db KV) *Typed {
+	return NewTyped(db, "misc")
+}
+
+// NewTyped returns a new typed key-value store that lives in the namespace
+// specified by the prefix.
+func NewTyped(db KV, prefix string) *Typed {
+	return &Typed{
+		db:     db,
+		prefix: prefix,
+	}
+}
+
+// PutInt64 stores a new int64. Any existing value (even if of another type)
+// is overwritten.
+func (n *Typed) PutInt64(key string, val int64) error {
+	var valBs [8]byte
+	binary.BigEndian.PutUint64(valBs[:], uint64(val))
+	return n.db.PutKV(n.prefixedKey(key), valBs[:])
+}
+
+// Int64 returns the stored value interpreted as an int64 and a boolean that
+// is false if no value was stored at the key.
+func (n *Typed) Int64(key string) (int64, bool, error) {
+	valBs, err := n.db.GetKV(n.prefixedKey(key))
+	if err != nil {
+		return 0, false, filterNotFound(err)
+	}
+	val := binary.BigEndian.Uint64(valBs)
+	return int64(val), true, nil
+}
+
+// PutTime stores a new time.Time. Any existing value (even if of another
+// type) is overwritten.
+func (n *Typed) PutTime(key string, val time.Time) error {
+	valBs, _ := val.MarshalBinary() // never returns an error
+	return n.db.PutKV(n.prefixedKey(key), valBs)
+}
+
+// Time returns the stored value interpreted as a time.Time and a boolean
+// that is false if no value was stored at the key.
+func (n *Typed) Time(key string) (time.Time, bool, error) {
+	var t time.Time
+	valBs, err := n.db.GetKV(n.prefixedKey(key))
+	if err != nil {
+		return t, false, filterNotFound(err)
+	}
+	err = t.UnmarshalBinary(valBs)
+	return t, err == nil, err
+}
+
+// PutString stores a new string. Any existing value (even if of another type)
+// is overwritten.
+func (n *Typed) PutString(key, val string) error {
+	return n.db.PutKV(n.prefixedKey(key), []byte(val))
+}
+
+// String returns the stored value interpreted as a string and a boolean that
+// is false if no value was stored at the key.
+func (n *Typed) String(key string) (string, bool, error) {
+	valBs, err := n.db.GetKV(n.prefixedKey(key))
+	if err != nil {
+		return "", false, filterNotFound(err)
+	}
+	return string(valBs), true, nil
+}
+
+// PutBytes stores a new byte slice. Any existing value (even if of another type)
+// is overwritten.
+func (n *Typed) PutBytes(key string, val []byte) error {
+	return n.db.PutKV(n.prefixedKey(key), val)
+}
+
+// Bytes returns the stored value as a raw byte slice and a boolean that
+// is false if no value was stored at the key.
+func (n *Typed) Bytes(key string) ([]byte, bool, error) {
+	valBs, err := n.db.GetKV(n.prefixedKey(key))
+	if err != nil {
+		return nil, false, filterNotFound(err)
+	}
+	return valBs, true, nil
+}
+
+// PutBool stores a new boolean. Any existing value (even if of another type)
+// is overwritten.
+func (n *Typed) PutBool(key string, val bool) error {
+	if val {
+		return n.db.PutKV(n.prefixedKey(key), []byte{0x0})
+	}
+	return n.db.PutKV(n.prefixedKey(key), []byte{0x1})
+}
+
+// Bool returns the stored value as a boolean and a boolean that
+// is false if no value was stored at the key.
+func (n *Typed) Bool(key string) (bool, bool, error) {
+	valBs, err := n.db.GetKV(n.prefixedKey(key))
+	if err != nil {
+		return false, false, filterNotFound(err)
+	}
+	return valBs[0] == 0x0, true, nil
+}
+
+// Delete deletes the specified key. It is allowed to delete a nonexistent
+// key.
+func (n *Typed) Delete(key string) error {
+	return n.db.DeleteKV(n.prefixedKey(key))
+}
+
+func (n *Typed) prefixedKey(key string) string {
+	return n.prefix + "/" + key
+}
+
+func filterNotFound(err error) error {
+	if errors.Is(err, sql.ErrNoRows) {
+		return nil
+	}
+	return err
+}
diff --git a/internal/itererr/itererr.go b/internal/itererr/itererr.go
new file mode 100644
index 00000000000..86278a159bb
--- /dev/null
+++ b/internal/itererr/itererr.go
@@ -0,0 +1,83 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package itererr
+
+import "iter"
+
+// Collect returns a slice of the items from the iterator, plus the error if
+// any.
+func Collect[T any](it iter.Seq[T], errFn func() error) ([]T, error) {
+	var s []T
+	for v := range it {
+		s = append(s, v)
+	}
+	return s, errFn()
+}
+
+// Zip interleaves the iterator value with the error. The iteration ends
+// after a non-nil error.
+func Zip[T any](it iter.Seq[T], errFn func() error) iter.Seq2[T, error] {
+	return func(yield func(T, error) bool) {
+		for v := range it {
+			if !yield(v, nil) {
+				break
+			}
+		}
+		if err := errFn(); err != nil {
+			var zero T
+			yield(zero, err)
+		}
+	}
+}
+
+// Map returns a new iterator by applying the map function, while respecting
+// the error function. Additionally, the map function can return an error if
+// its own.
+func Map[A, B any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, error)) (iter.Seq[B], func() error) {
+	var retErr error
+	return func(yield func(B) bool) {
+			for v := range i {
+				mapped, err := mapFn(v)
+				if err != nil {
+					retErr = err
+					return
+				}
+				if !yield(mapped) {
+					return
+				}
+			}
+		}, func() error {
+			if prevErr := errFn(); prevErr != nil {
+				return prevErr
+			}
+			return retErr
+		}
+}
+
+// Map returns a new iterator by applying the map function, while respecting
+// the error function. Additionally, the map function can return an error if
+// its own.
+func Map2[A, B, C any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, C, error)) (iter.Seq2[B, C], func() error) {
+	var retErr error
+	return func(yield func(B, C) bool) {
+			for v := range i {
+				ma, mb, err := mapFn(v)
+				if err != nil {
+					retErr = err
+					return
+				}
+				if !yield(ma, mb) {
+					return
+				}
+			}
+		}, func() error {
+			if prevErr := errFn(); prevErr != nil {
+				return prevErr
+			}
+			return retErr
+		}
+}
diff --git a/internal/protoutil/protoutil.go b/internal/protoutil/protoutil.go
index 93bd769111a..c43057b44e5 100644
--- a/internal/protoutil/protoutil.go
+++ b/internal/protoutil/protoutil.go
@@ -1,3 +1,9 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
 package protoutil
 
 import (
diff --git a/internal/timeutil/timeutil.go b/internal/timeutil/timeutil.go
new file mode 100644
index 00000000000..103de37f32a
--- /dev/null
+++ b/internal/timeutil/timeutil.go
@@ -0,0 +1,27 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package timeutil
+
+import (
+	"sync/atomic"
+	"time"
+)
+
+var prevNanos atomic.Int64
+
+// StrictlyMonotonicNanos returns the current time in Unix nanoseconds.
+// Guaranteed to strictly increase for each call, regardless of the
+// underlying OS timer resolution or clock jumps.
+func StrictlyMonotonicNanos() int64 {
+	for {
+		old := prevNanos.Load()
+		now := max(time.Now().UnixNano(), old+1)
+		if prevNanos.CompareAndSwap(old, now) {
+			return now
+		}
+	}
+}
diff --git a/lib/api/api.go b/lib/api/api.go
index 8d07546b9f1..9a8f7104b58 100644
--- a/lib/api/api.go
+++ b/lib/api/api.go
@@ -40,10 +40,10 @@ import (
 	"golang.org/x/text/transform"
 	"golang.org/x/text/unicode/norm"
 
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/discover"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
@@ -91,7 +91,7 @@ type service struct {
 	startupErr           error
 	listenerAddr         net.Addr
 	exitChan             chan *svcutil.FatalErr
-	miscDB               *db.NamespacedKV
+	miscDB               *db.Typed
 	shutdownTimeout      time.Duration
 
 	guiErrors logger.Recorder
@@ -106,7 +106,7 @@ type Service interface {
 	WaitForStart() error
 }
 
-func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.NamespacedKV) Service {
+func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.Typed) Service {
 	return &service{
 		id:      id,
 		cfg:     cfg,
@@ -984,16 +984,11 @@ func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file)
 
 	sendJSON(w, map[string]interface{}{
 		"global":       jsonFileInfo(gf),
 		"local":        jsonFileInfo(lf),
 		"availability": av,
-		"mtime": map[string]interface{}{
-			"err":   mtimeErr,
-			"value": mtimeMapping,
-		},
 	})
 }
 
@@ -1002,28 +997,14 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) {
 	folder := qs.Get("folder")
 	file := qs.Get("file")
 
-	snap, err := s.model.DBSnapshot(folder)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusNotFound)
-		return
-	}
-
-	mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file)
-
-	lf, _ := snap.Get(protocol.LocalDeviceID, file)
-	gf, _ := snap.GetGlobal(file)
-	av := snap.Availability(file)
-	vl := snap.DebugGlobalVersions(file)
+	lf, _, _ := s.model.CurrentFolderFile(folder, file)
+	gf, _, _ := s.model.CurrentGlobalFile(folder, file)
+	av, _ := s.model.Availability(folder, protocol.FileInfo{Name: file}, protocol.BlockInfo{})
 
 	sendJSON(w, map[string]interface{}{
-		"global":         jsonFileInfo(gf),
-		"local":          jsonFileInfo(lf),
-		"availability":   av,
-		"globalVersions": vl.String(),
-		"mtime": map[string]interface{}{
-			"err":   mtimeErr,
-			"value": mtimeMapping,
-		},
+		"global":       jsonFileInfo(gf),
+		"local":        jsonFileInfo(lf),
+		"availability": av,
 	})
 }
 
diff --git a/lib/api/api_csrf.go b/lib/api/api_csrf.go
index e8f03418d36..d7392a345a7 100644
--- a/lib/api/api_csrf.go
+++ b/lib/api/api_csrf.go
@@ -11,7 +11,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db"
+	"github.com/syncthing/syncthing/internal/db"
 )
 
 const (
@@ -34,7 +34,7 @@ type apiKeyValidator interface {
 // Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
 // the request with 403. For / and /index.html, set a new CSRF cookie if none
 // is currently set.
-func newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, miscDB *db.NamespacedKV) *csrfManager {
+func newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, miscDB *db.Typed) *csrfManager {
 	m := &csrfManager{
 		unique:          unique,
 		prefix:          prefix,
diff --git a/lib/api/tokenmanager.go b/lib/api/tokenmanager.go
index 38cd2441575..5bd674686e0 100644
--- a/lib/api/tokenmanager.go
+++ b/lib/api/tokenmanager.go
@@ -14,9 +14,9 @@ import (
 
 	"google.golang.org/protobuf/proto"
 
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/internal/gen/apiproto"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/sync"
@@ -24,7 +24,7 @@ import (
 
 type tokenManager struct {
 	key      string
-	miscDB   *db.NamespacedKV
+	miscDB   *db.Typed
 	lifetime time.Duration
 	maxItems int
 
@@ -35,7 +35,7 @@ type tokenManager struct {
 	saveTimer *time.Timer
 }
 
-func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager {
+func newTokenManager(key string, miscDB *db.Typed, lifetime time.Duration, maxItems int) *tokenManager {
 	var tokens apiproto.TokenSet
 	if bs, ok, _ := miscDB.Bytes(key); ok {
 		_ = proto.Unmarshal(bs, &tokens) // best effort
@@ -152,7 +152,7 @@ type tokenCookieManager struct {
 	tokens     *tokenManager
 }
 
-func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.NamespacedKV) *tokenCookieManager {
+func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.Typed) *tokenCookieManager {
 	return &tokenCookieManager{
 		cookieName: "sessionid-" + shortID,
 		shortID:    shortID,
diff --git a/lib/build/build.go b/lib/build/build.go
index 098dd7c7c13..b13cb93b772 100644
--- a/lib/build/build.go
+++ b/lib/build/build.go
@@ -18,7 +18,7 @@ import (
 	"time"
 )
 
-const Codename = "Gold Grasshopper"
+const Codename = "Hafnium Hornet"
 
 var (
 	// Injected by build script
@@ -28,6 +28,9 @@ var (
 	Stamp   = "0"
 	Tags    = ""
 
+	// Added to by other packages
+	extraTags []string
+
 	// Set by init()
 	Date        time.Time
 	IsRelease   bool
@@ -43,6 +46,11 @@ var (
 		"STNORESTART",
 		"STNOUPGRADE",
 	}
+	replaceTags = map[string]string{
+		"sqlite_omit_load_extension": "",
+		"osusergo":                   "",
+		"netgo":                      "",
+	}
 )
 
 const versionExtraAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-. "
@@ -108,8 +116,23 @@ func TagsList() []string {
 	if Extra != "" {
 		tags = append(tags, Extra)
 	}
+	tags = append(tags, extraTags...)
+
+	// Replace any tag values we want to have more user friendly versions,
+	// or be removed
+	for i, tag := range tags {
+		if repl, ok := replaceTags[tag]; ok {
+			tags[i] = repl
+		}
+	}
 
 	sort.Strings(tags)
+
+	// Remove any empty tags, which will be at the front of the list now
+	for len(tags) > 0 && tags[0] == "" {
+		tags = tags[1:]
+	}
+
 	return tags
 }
 
@@ -124,3 +147,8 @@ func filterString(s, allowedChars string) string {
 	}
 	return res.String()
 }
+
+func AddTag(tag string) {
+	extraTags = append(extraTags, tag)
+	LongVersion = LongVersionFor("syncthing")
+}
diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go
index 76c3b3e39a9..edcb6a55124 100644
--- a/lib/config/folderconfiguration.go
+++ b/lib/config/folderconfiguration.go
@@ -20,7 +20,6 @@ import (
 	"github.com/shirou/gopsutil/v4/disk"
 
 	"github.com/syncthing/syncthing/lib/build"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -119,26 +118,24 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
 // Filesystem creates a filesystem for the path and options of this folder.
 // The fset parameter may be nil, in which case no mtime handling on top of
 // the filesystem is provided.
-func (f FolderConfiguration) Filesystem(fset *db.FileSet) fs.Filesystem {
+func (f FolderConfiguration) Filesystem(extraOpts ...fs.Option) fs.Filesystem {
 	// This is intentionally not a pointer method, because things like
 	// cfg.Folders["default"].Filesystem(nil) should be valid.
-	opts := make([]fs.Option, 0, 3)
+	var opts []fs.Option
 	if f.FilesystemType == FilesystemTypeBasic && f.JunctionsAsDirs {
 		opts = append(opts, new(fs.OptionJunctionsAsDirs))
 	}
 	if !f.CaseSensitiveFS {
 		opts = append(opts, new(fs.OptionDetectCaseConflicts))
 	}
-	if fset != nil {
-		opts = append(opts, fset.MtimeOption())
-	}
+	opts = append(opts, extraOpts...)
 	return fs.NewFilesystem(f.FilesystemType.ToFS(), f.Path, opts...)
 }
 
 func (f FolderConfiguration) ModTimeWindow() time.Duration {
 	dur := time.Duration(f.RawModTimeWindowS) * time.Second
 	if f.RawModTimeWindowS < 1 && build.IsAndroid {
-		if usage, err := disk.Usage(f.Filesystem(nil).URI()); err != nil {
+		if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
 			dur = 2 * time.Second
 			l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
 		} else if strings.HasPrefix(strings.ToLower(usage.Fstype), "ext2") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext3") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext4") {
@@ -162,7 +159,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 		return nil
 	}
 
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	// Create the marker as a directory
 	err := ffs.Mkdir(DefaultMarkerName, 0o755)
@@ -189,7 +186,7 @@ func (f *FolderConfiguration) CreateMarker() error {
 }
 
 func (f *FolderConfiguration) RemoveMarker() error {
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	_ = ffs.Remove(filepath.Join(DefaultMarkerName, f.markerFilename()))
 	return ffs.Remove(DefaultMarkerName)
 }
@@ -209,7 +206,7 @@ func (f *FolderConfiguration) markerContents() []byte {
 
 // CheckPath returns nil if the folder root exists and contains the marker file
 func (f *FolderConfiguration) CheckPath() error {
-	return f.checkFilesystemPath(f.Filesystem(nil), ".")
+	return f.checkFilesystemPath(f.Filesystem(), ".")
 }
 
 func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string) error {
@@ -252,7 +249,7 @@ func (f *FolderConfiguration) CreateRoot() (err error) {
 		permBits = 0o700
 	}
 
-	filesystem := f.Filesystem(nil)
+	filesystem := f.Filesystem()
 
 	if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
 		err = filesystem.MkdirAll(".", permBits)
@@ -363,7 +360,7 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
 	if val <= 0 {
 		return nil
 	}
-	fs := f.Filesystem(nil)
+	fs := f.Filesystem()
 	usage, err := fs.Usage(".")
 	if err != nil {
 		return nil //nolint: nilerr
diff --git a/lib/config/migrations.go b/lib/config/migrations.go
index 4a4ecdf3774..014d3140426 100644
--- a/lib/config/migrations.go
+++ b/lib/config/migrations.go
@@ -208,7 +208,7 @@ func migrateToConfigV23(cfg *Configuration) {
 	// marker name in later versions.
 
 	for i := range cfg.Folders {
-		fs := cfg.Folders[i].Filesystem(nil)
+		fs := cfg.Folders[i].Filesystem()
 		// Invalid config posted, or tests.
 		if fs == nil {
 			continue
@@ -244,18 +244,18 @@ func migrateToConfigV21(cfg *Configuration) {
 		switch folder.Versioning.Type {
 		case "simple", "trashcan":
 			// Clean out symlinks in the known place
-			cleanSymlinks(folder.Filesystem(nil), ".stversions")
+			cleanSymlinks(folder.Filesystem(), ".stversions")
 		case "staggered":
 			versionDir := folder.Versioning.Params["versionsPath"]
 			if versionDir == "" {
 				// default place
-				cleanSymlinks(folder.Filesystem(nil), ".stversions")
+				cleanSymlinks(folder.Filesystem(), ".stversions")
 			} else if filepath.IsAbs(versionDir) {
 				// absolute
 				cleanSymlinks(fs.NewFilesystem(fs.FilesystemTypeBasic, versionDir), ".")
 			} else {
 				// relative to folder
-				cleanSymlinks(folder.Filesystem(nil), versionDir)
+				cleanSymlinks(folder.Filesystem(), versionDir)
 			}
 		}
 	}
diff --git a/lib/config/optionsconfiguration.go b/lib/config/optionsconfiguration.go
index eecf34dda56..16363b00df5 100644
--- a/lib/config/optionsconfiguration.go
+++ b/lib/config/optionsconfiguration.go
@@ -61,7 +61,6 @@ type OptionsConfiguration struct {
 	StunKeepaliveStartS         int      `json:"stunKeepaliveStartS" xml:"stunKeepaliveStartS" default:"180"`
 	StunKeepaliveMinS           int      `json:"stunKeepaliveMinS" xml:"stunKeepaliveMinS" default:"20"`
 	RawStunServers              []string `json:"stunServers" xml:"stunServer" default:"default"`
-	DatabaseTuning              Tuning   `json:"databaseTuning" xml:"databaseTuning" restart:"true"`
 	RawMaxCIRequestKiB          int      `json:"maxConcurrentIncomingRequestKiB" xml:"maxConcurrentIncomingRequestKiB"`
 	AnnounceLANAddresses        bool     `json:"announceLANAddresses" xml:"announceLANAddresses" default:"true"`
 	SendFullIndexOnUpgrade      bool     `json:"sendFullIndexOnUpgrade" xml:"sendFullIndexOnUpgrade"`
diff --git a/lib/config/tuning.go b/lib/config/tuning.go
deleted file mode 100644
index c2fa9df435e..00000000000
--- a/lib/config/tuning.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2019 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package config
-
-type Tuning int32
-
-const (
-	TuningAuto  Tuning = 0
-	TuningSmall Tuning = 1
-	TuningLarge Tuning = 2
-)
-
-func (t Tuning) String() string {
-	switch t {
-	case TuningAuto:
-		return "auto"
-	case TuningSmall:
-		return "small"
-	case TuningLarge:
-		return "large"
-	default:
-		return "unknown"
-	}
-}
-
-func (t Tuning) MarshalText() ([]byte, error) {
-	return []byte(t.String()), nil
-}
-
-func (t *Tuning) UnmarshalText(bs []byte) error {
-	switch string(bs) {
-	case "auto":
-		*t = TuningAuto
-	case "small":
-		*t = TuningSmall
-	case "large":
-		*t = TuningLarge
-	default:
-		*t = TuningAuto
-	}
-	return nil
-}
diff --git a/lib/db/.gitignore b/lib/db/.gitignore
deleted file mode 100644
index d5316784f41..00000000000
--- a/lib/db/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-!*.zip
-testdata/*.db
diff --git a/lib/db/backend/debug.go b/lib/db/backend/debug.go
deleted file mode 100644
index 9a86fcefa7d..00000000000
--- a/lib/db/backend/debug.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (C) 2019 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package backend
-
-import (
-	"github.com/syncthing/syncthing/lib/logger"
-)
-
-var l = logger.DefaultLogger.NewFacility("backend", "The database backend")
diff --git a/lib/db/backend/leveldb_backend.go b/lib/db/backend/leveldb_backend.go
deleted file mode 100644
index 577ff4b8262..00000000000
--- a/lib/db/backend/leveldb_backend.go
+++ /dev/null
@@ -1,233 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package backend
-
-import (
-	"github.com/syndtr/goleveldb/leveldb"
-	"github.com/syndtr/goleveldb/leveldb/iterator"
-	"github.com/syndtr/goleveldb/leveldb/util"
-)
-
-const (
-	// Never flush transactions smaller than this, even on Checkpoint().
-	// This just needs to be just large enough to avoid flushing
-	// transactions when they are super tiny, thus creating millions of tiny
-	// transactions unnecessarily.
-	dbFlushBatchMin = 64 << KiB
-	// Once a transaction reaches this size, flush it unconditionally. This
-	// should be large enough to avoid forcing a flush between Checkpoint()
-	// calls in loops where we do those, so in principle just large enough
-	// to hold a FileInfo plus corresponding version list and metadata
-	// updates or two.
-	dbFlushBatchMax = 1 << MiB
-)
-
-// leveldbBackend implements Backend on top of a leveldb
-type leveldbBackend struct {
-	ldb      *leveldb.DB
-	closeWG  *closeWaitGroup
-	location string
-}
-
-func newLeveldbBackend(ldb *leveldb.DB, location string) *leveldbBackend {
-	return &leveldbBackend{
-		ldb:      ldb,
-		closeWG:  &closeWaitGroup{},
-		location: location,
-	}
-}
-
-func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
-	return b.newSnapshot()
-}
-
-func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
-	rel, err := newReleaser(b.closeWG)
-	if err != nil {
-		return leveldbSnapshot{}, err
-	}
-	snap, err := b.ldb.GetSnapshot()
-	if err != nil {
-		rel.Release()
-		return leveldbSnapshot{}, wrapLeveldbErr(err)
-	}
-	return leveldbSnapshot{
-		snap: snap,
-		rel:  rel,
-	}, nil
-}
-
-func (b *leveldbBackend) NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error) {
-	rel, err := newReleaser(b.closeWG)
-	if err != nil {
-		return nil, err
-	}
-	snap, err := b.newSnapshot()
-	if err != nil {
-		rel.Release()
-		return nil, err // already wrapped
-	}
-	return &leveldbTransaction{
-		leveldbSnapshot: snap,
-		ldb:             b.ldb,
-		batch:           new(leveldb.Batch),
-		rel:             rel,
-		commitHooks:     hooks,
-		inFlush:         false,
-	}, nil
-}
-
-func (b *leveldbBackend) Close() error {
-	b.closeWG.CloseWait()
-	return wrapLeveldbErr(b.ldb.Close())
-}
-
-func (b *leveldbBackend) Get(key []byte) ([]byte, error) {
-	val, err := b.ldb.Get(key, nil)
-	return val, wrapLeveldbErr(err)
-}
-
-func (b *leveldbBackend) NewPrefixIterator(prefix []byte) (Iterator, error) {
-	return &leveldbIterator{b.ldb.NewIterator(util.BytesPrefix(prefix), nil)}, nil
-}
-
-func (b *leveldbBackend) NewRangeIterator(first, last []byte) (Iterator, error) {
-	return &leveldbIterator{b.ldb.NewIterator(&util.Range{Start: first, Limit: last}, nil)}, nil
-}
-
-func (b *leveldbBackend) Put(key, val []byte) error {
-	return wrapLeveldbErr(b.ldb.Put(key, val, nil))
-}
-
-func (b *leveldbBackend) Delete(key []byte) error {
-	return wrapLeveldbErr(b.ldb.Delete(key, nil))
-}
-
-func (b *leveldbBackend) Compact() error {
-	// Race is detected during testing when db is closed while compaction
-	// is ongoing.
-	err := b.closeWG.Add(1)
-	if err != nil {
-		return err
-	}
-	defer b.closeWG.Done()
-	return wrapLeveldbErr(b.ldb.CompactRange(util.Range{}))
-}
-
-func (b *leveldbBackend) Location() string {
-	return b.location
-}
-
-// leveldbSnapshot implements backend.ReadTransaction
-type leveldbSnapshot struct {
-	snap *leveldb.Snapshot
-	rel  *releaser
-}
-
-func (l leveldbSnapshot) Get(key []byte) ([]byte, error) {
-	val, err := l.snap.Get(key, nil)
-	return val, wrapLeveldbErr(err)
-}
-
-func (l leveldbSnapshot) NewPrefixIterator(prefix []byte) (Iterator, error) {
-	return l.snap.NewIterator(util.BytesPrefix(prefix), nil), nil
-}
-
-func (l leveldbSnapshot) NewRangeIterator(first, last []byte) (Iterator, error) {
-	return l.snap.NewIterator(&util.Range{Start: first, Limit: last}, nil), nil
-}
-
-func (l leveldbSnapshot) Release() {
-	l.snap.Release()
-	l.rel.Release()
-}
-
-// leveldbTransaction implements backend.WriteTransaction using a batch (not
-// an actual leveldb transaction)
-type leveldbTransaction struct {
-	leveldbSnapshot
-	ldb         *leveldb.DB
-	batch       *leveldb.Batch
-	rel         *releaser
-	commitHooks []CommitHook
-	inFlush     bool
-}
-
-func (t *leveldbTransaction) Delete(key []byte) error {
-	t.batch.Delete(key)
-	return t.checkFlush(dbFlushBatchMax)
-}
-
-func (t *leveldbTransaction) Put(key, val []byte) error {
-	t.batch.Put(key, val)
-	return t.checkFlush(dbFlushBatchMax)
-}
-
-func (t *leveldbTransaction) Checkpoint() error {
-	return t.checkFlush(dbFlushBatchMin)
-}
-
-func (t *leveldbTransaction) Commit() error {
-	err := wrapLeveldbErr(t.flush())
-	t.leveldbSnapshot.Release()
-	t.rel.Release()
-	return err
-}
-
-func (t *leveldbTransaction) Release() {
-	t.leveldbSnapshot.Release()
-	t.rel.Release()
-}
-
-// checkFlush flushes and resets the batch if its size exceeds the given size.
-func (t *leveldbTransaction) checkFlush(size int) error {
-	// Hooks might put values in the database, which triggers a checkFlush which might trigger a flush,
-	// which might trigger the hooks.
-	// Don't recurse...
-	if t.inFlush || len(t.batch.Dump()) < size {
-		return nil
-	}
-	return t.flush()
-}
-
-func (t *leveldbTransaction) flush() error {
-	t.inFlush = true
-	defer func() { t.inFlush = false }()
-
-	for _, hook := range t.commitHooks {
-		if err := hook(t); err != nil {
-			return err
-		}
-	}
-	if t.batch.Len() == 0 {
-		return nil
-	}
-	if err := t.ldb.Write(t.batch, nil); err != nil {
-		return wrapLeveldbErr(err)
-	}
-	t.batch.Reset()
-	return nil
-}
-
-type leveldbIterator struct {
-	iterator.Iterator
-}
-
-func (it *leveldbIterator) Error() error {
-	return wrapLeveldbErr(it.Iterator.Error())
-}
-
-// wrapLeveldbErr wraps errors so that the backend package can recognize them
-func wrapLeveldbErr(err error) error {
-	switch err {
-	case leveldb.ErrClosed:
-		return errClosed
-	case leveldb.ErrNotFound:
-		return errNotFound
-	}
-	return err
-}
diff --git a/lib/db/backend/leveldb_open.go b/lib/db/backend/leveldb_open.go
deleted file mode 100644
index 3908d756920..00000000000
--- a/lib/db/backend/leveldb_open.go
+++ /dev/null
@@ -1,231 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package backend
-
-import (
-	"fmt"
-	"os"
-	"strconv"
-	"strings"
-
-	"github.com/syndtr/goleveldb/leveldb"
-	"github.com/syndtr/goleveldb/leveldb/errors"
-	"github.com/syndtr/goleveldb/leveldb/opt"
-	"github.com/syndtr/goleveldb/leveldb/storage"
-	"github.com/syndtr/goleveldb/leveldb/util"
-)
-
-const (
-	dbMaxOpenFiles = 100
-
-	// A large database is > 200 MiB. It's a mostly arbitrary value, but
-	// it's also the case that each file is 2 MiB by default and when we
-	// have dbMaxOpenFiles of them we will need to start thrashing fd:s.
-	// Switching to large database settings causes larger files to be used
-	// when compacting, reducing the number.
-	dbLargeThreshold = dbMaxOpenFiles * (2 << MiB)
-
-	KiB = 10
-	MiB = 20
-)
-
-// OpenLevelDB attempts to open the database at the given location, and runs
-// recovery on it if opening fails. Worst case, if recovery is not possible,
-// the database is erased and created from scratch.
-func OpenLevelDB(location string, tuning Tuning) (Backend, error) {
-	opts := optsFor(location, tuning)
-	ldb, err := open(location, opts)
-	if err != nil {
-		return nil, err
-	}
-	return newLeveldbBackend(ldb, location), nil
-}
-
-// OpenLevelDBAuto is OpenLevelDB with TuningAuto tuning.
-func OpenLevelDBAuto(location string) (Backend, error) {
-	return OpenLevelDB(location, TuningAuto)
-}
-
-// OpenLevelDBRO attempts to open the database at the given location, read
-// only.
-func OpenLevelDBRO(location string) (Backend, error) {
-	opts := &opt.Options{
-		OpenFilesCacheCapacity: dbMaxOpenFiles,
-		ReadOnly:               true,
-	}
-	ldb, err := open(location, opts)
-	if err != nil {
-		return nil, err
-	}
-	return newLeveldbBackend(ldb, location), nil
-}
-
-// OpenLevelDBMemory returns a new Backend referencing an in-memory database.
-func OpenLevelDBMemory() Backend {
-	ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
-	return newLeveldbBackend(ldb, "")
-}
-
-// optsFor returns the database options to use when opening a database with
-// the given location and tuning. Settings can be overridden by debug
-// environment variables.
-func optsFor(location string, tuning Tuning) *opt.Options {
-	large := false
-	switch tuning {
-	case TuningLarge:
-		large = true
-	case TuningAuto:
-		large = dbIsLarge(location)
-	}
-
-	var (
-		// Set defaults used for small databases.
-		defaultBlockCacheCapacity            = 0 // 0 means let leveldb use default
-		defaultBlockSize                     = 0
-		defaultCompactionTableSize           = 0
-		defaultCompactionTableSizeMultiplier = 0
-		defaultWriteBuffer                   = 16 << MiB                      // increased from leveldb default of 4 MiB
-		defaultCompactionL0Trigger           = opt.DefaultCompactionL0Trigger // explicit because we use it as base for other stuff
-	)
-
-	if large {
-		// Change the parameters for better throughput at the price of some
-		// RAM and larger files. This results in larger batches of writes
-		// and compaction at a lower frequency.
-		l.Infoln("Using large-database tuning")
-
-		defaultBlockCacheCapacity = 64 << MiB
-		defaultBlockSize = 64 << KiB
-		defaultCompactionTableSize = 16 << MiB
-		defaultCompactionTableSizeMultiplier = 20 // 2.0 after division by ten
-		defaultWriteBuffer = 64 << MiB
-		defaultCompactionL0Trigger = 8 // number of l0 files
-	}
-
-	opts := &opt.Options{
-		BlockCacheCapacity:            debugEnvValue("BlockCacheCapacity", defaultBlockCacheCapacity),
-		BlockCacheEvictRemoved:        debugEnvValue("BlockCacheEvictRemoved", 0) != 0,
-		BlockRestartInterval:          debugEnvValue("BlockRestartInterval", 0),
-		BlockSize:                     debugEnvValue("BlockSize", defaultBlockSize),
-		CompactionExpandLimitFactor:   debugEnvValue("CompactionExpandLimitFactor", 0),
-		CompactionGPOverlapsFactor:    debugEnvValue("CompactionGPOverlapsFactor", 0),
-		CompactionL0Trigger:           debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger),
-		CompactionSourceLimitFactor:   debugEnvValue("CompactionSourceLimitFactor", 0),
-		CompactionTableSize:           debugEnvValue("CompactionTableSize", defaultCompactionTableSize),
-		CompactionTableSizeMultiplier: float64(debugEnvValue("CompactionTableSizeMultiplier", defaultCompactionTableSizeMultiplier)) / 10.0,
-		CompactionTotalSize:           debugEnvValue("CompactionTotalSize", 0),
-		CompactionTotalSizeMultiplier: float64(debugEnvValue("CompactionTotalSizeMultiplier", 0)) / 10.0,
-		DisableBufferPool:             debugEnvValue("DisableBufferPool", 0) != 0,
-		DisableBlockCache:             debugEnvValue("DisableBlockCache", 0) != 0,
-		DisableCompactionBackoff:      debugEnvValue("DisableCompactionBackoff", 0) != 0,
-		DisableLargeBatchTransaction:  debugEnvValue("DisableLargeBatchTransaction", 0) != 0,
-		NoSync:                        debugEnvValue("NoSync", 0) != 0,
-		NoWriteMerge:                  debugEnvValue("NoWriteMerge", 0) != 0,
-		OpenFilesCacheCapacity:        debugEnvValue("OpenFilesCacheCapacity", dbMaxOpenFiles),
-		WriteBuffer:                   debugEnvValue("WriteBuffer", defaultWriteBuffer),
-		// The write slowdown and pause can be overridden, but even if they
-		// are not and the compaction trigger is overridden we need to
-		// adjust so that we don't pause writes for L0 compaction before we
-		// even *start* L0 compaction...
-		WriteL0SlowdownTrigger: debugEnvValue("WriteL0SlowdownTrigger", 2*debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger)),
-		WriteL0PauseTrigger:    debugEnvValue("WriteL0SlowdownTrigger", 3*debugEnvValue("CompactionL0Trigger", defaultCompactionL0Trigger)),
-	}
-
-	return opts
-}
-
-func open(location string, opts *opt.Options) (*leveldb.DB, error) {
-	db, err := leveldb.OpenFile(location, opts)
-	if leveldbIsCorrupted(err) {
-		db, err = leveldb.RecoverFile(location, opts)
-	}
-	if leveldbIsCorrupted(err) {
-		// The database is corrupted, and we've tried to recover it but it
-		// didn't work. At this point there isn't much to do beyond dropping
-		// the database and reindexing...
-		l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
-		if err := os.RemoveAll(location); err != nil {
-			return nil, &errorSuggestion{err, "failed to delete corrupted database"}
-		}
-		db, err = leveldb.OpenFile(location, opts)
-	}
-	if err != nil {
-		return nil, &errorSuggestion{err, "is another instance of Syncthing running?"}
-	}
-
-	if debugEnvValue("CompactEverything", 0) != 0 {
-		if err := db.CompactRange(util.Range{}); err != nil {
-			l.Warnln("Compacting database:", err)
-		}
-	}
-
-	return db, nil
-}
-
-func debugEnvValue(key string, def int) int {
-	v, err := strconv.ParseInt(os.Getenv("STDEBUG_"+key), 10, 63)
-	if err != nil {
-		return def
-	}
-	return int(v)
-}
-
-// A "better" version of leveldb's errors.IsCorrupted.
-func leveldbIsCorrupted(err error) bool {
-	switch {
-	case err == nil:
-		return false
-
-	case errors.IsCorrupted(err):
-		return true
-
-	case strings.Contains(err.Error(), "corrupted"):
-		return true
-	}
-
-	return false
-}
-
-// dbIsLarge returns whether the estimated size of the database at location
-// is large enough to warrant optimization for large databases.
-func dbIsLarge(location string) bool {
-	if ^uint(0)>>63 == 0 {
-		// We're compiled for a 32 bit architecture. We've seen trouble with
-		// large settings there.
-		// (https://forum.syncthing.net/t/many-small-ldb-files-with-database-tuning/13842)
-		return false
-	}
-
-	entries, err := os.ReadDir(location)
-	if err != nil {
-		return false
-	}
-
-	var size int64
-	for _, entry := range entries {
-		if entry.Name() == "LOG" {
-			// don't count the size
-			continue
-		}
-		fi, err := entry.Info()
-		if err != nil {
-			continue
-		}
-		size += fi.Size()
-	}
-
-	return size > dbLargeThreshold
-}
-
-type errorSuggestion struct {
-	inner      error
-	suggestion string
-}
-
-func (e *errorSuggestion) Error() string {
-	return fmt.Sprintf("%s (%s)", e.inner.Error(), e.suggestion)
-}
diff --git a/lib/db/blockmap.go b/lib/db/blockmap.go
deleted file mode 100644
index 78d71c0b1f0..00000000000
--- a/lib/db/blockmap.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"encoding/binary"
-	"fmt"
-
-	"github.com/syncthing/syncthing/lib/osutil"
-)
-
-type BlockFinder struct {
-	db *Lowlevel
-}
-
-func NewBlockFinder(db *Lowlevel) *BlockFinder {
-	return &BlockFinder{
-		db: db,
-	}
-}
-
-func (f *BlockFinder) String() string {
-	return fmt.Sprintf("BlockFinder@%p", f)
-}
-
-// Iterate takes an iterator function which iterates over all matching blocks
-// for the given hash. The iterator function has to return either true (if
-// they are happy with the block) or false to continue iterating for whatever
-// reason. The iterator finally returns the result, whether or not a
-// satisfying block was eventually found.
-func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string, string, int32) bool) bool {
-	t, err := f.db.newReadOnlyTransaction()
-	if err != nil {
-		return false
-	}
-	defer t.close()
-
-	var key []byte
-	for _, folder := range folders {
-		key, err = f.db.keyer.GenerateBlockMapKey(key, []byte(folder), hash, nil)
-		if err != nil {
-			return false
-		}
-		iter, err := t.NewPrefixIterator(key)
-		if err != nil {
-			return false
-		}
-
-		for iter.Next() && iter.Error() == nil {
-			file := string(f.db.keyer.NameFromBlockMapKey(iter.Key()))
-			index := int32(binary.BigEndian.Uint32(iter.Value()))
-			if iterFn(folder, osutil.NativeFilename(file), index) {
-				iter.Release()
-				return true
-			}
-		}
-		iter.Release()
-	}
-	return false
-}
diff --git a/lib/db/lowlevel.go b/lib/db/lowlevel.go
deleted file mode 100644
index 368f7ff99eb..00000000000
--- a/lib/db/lowlevel.go
+++ /dev/null
@@ -1,1453 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"bytes"
-	"context"
-	"crypto/sha256"
-	"encoding/binary"
-	"errors"
-	"fmt"
-	"hash/maphash"
-	"os"
-	"regexp"
-	"time"
-
-	"github.com/greatroar/blobloom"
-	"github.com/thejerf/suture/v4"
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/fs"
-	"github.com/syncthing/syncthing/lib/protocol"
-	"github.com/syncthing/syncthing/lib/stringutil"
-	"github.com/syncthing/syncthing/lib/svcutil"
-	"github.com/syncthing/syncthing/lib/sync"
-)
-
-const (
-	// We set the bloom filter capacity to handle 100k individual items with
-	// a false positive probability of 1% for the first pass. Once we know
-	// how many items we have we will use that number instead, if it's more
-	// than 100k. For fewer than 100k items we will just get better false
-	// positive rate instead.
-	indirectGCBloomCapacity          = 100000
-	indirectGCBloomFalsePositiveRate = 0.01     // 1%
-	indirectGCBloomMaxBytes          = 32 << 20 // Use at most 32MiB memory, which covers our desired FP rate at 27 M items
-	indirectGCDefaultInterval        = 13 * time.Hour
-	indirectGCTimeKey                = "lastIndirectGCTime"
-
-	// Use indirection for the block list when it exceeds this many entries
-	blocksIndirectionCutoff = 3
-	// Use indirection for the version vector when it exceeds this many entries
-	versionIndirectionCutoff = 10
-
-	recheckDefaultInterval = 30 * 24 * time.Hour
-
-	needsRepairSuffix = ".needsrepair"
-)
-
-// Lowlevel is the lowest level database interface. It has a very simple
-// purpose: hold the actual backend database, and the in-memory state
-// that belong to that database. In the same way that a single on disk
-// database can only be opened once, there should be only one Lowlevel for
-// any given backend.
-type Lowlevel struct {
-	*suture.Supervisor
-	backend.Backend
-	folderIdx          *smallIndex
-	deviceIdx          *smallIndex
-	keyer              keyer
-	gcMut              sync.RWMutex
-	gcKeyCount         int
-	indirectGCInterval time.Duration
-	recheckInterval    time.Duration
-	oneFileSetCreated  chan struct{}
-	evLogger           events.Logger
-
-	blockFilter   *bloomFilter
-	versionFilter *bloomFilter
-}
-
-func NewLowlevel(backend backend.Backend, evLogger events.Logger, opts ...Option) (*Lowlevel, error) {
-	// Only log restarts in debug mode.
-	spec := svcutil.SpecWithDebugLogger(l)
-	db := &Lowlevel{
-		Supervisor:         suture.New("db.Lowlevel", spec),
-		Backend:            backend,
-		folderIdx:          newSmallIndex(backend, []byte{KeyTypeFolderIdx}),
-		deviceIdx:          newSmallIndex(backend, []byte{KeyTypeDeviceIdx}),
-		gcMut:              sync.NewRWMutex(),
-		indirectGCInterval: indirectGCDefaultInterval,
-		recheckInterval:    recheckDefaultInterval,
-		oneFileSetCreated:  make(chan struct{}),
-		evLogger:           evLogger,
-	}
-	for _, opt := range opts {
-		opt(db)
-	}
-	db.keyer = newDefaultKeyer(db.folderIdx, db.deviceIdx)
-	db.Add(svcutil.AsService(db.gcRunner, "db.Lowlevel/gcRunner"))
-	if path := db.needsRepairPath(); path != "" {
-		if _, err := os.Lstat(path); err == nil {
-			l.Infoln("Database was marked for repair - this may take a while")
-			if err := db.checkRepair(); err != nil {
-				db.handleFailure(err)
-				return nil, err
-			}
-			os.Remove(path)
-		}
-	}
-	return db, nil
-}
-
-type Option func(*Lowlevel)
-
-// WithRecheckInterval sets the time interval in between metadata recalculations
-// and consistency checks.
-func WithRecheckInterval(dur time.Duration) Option {
-	return func(db *Lowlevel) {
-		if dur > 0 {
-			db.recheckInterval = dur
-		}
-	}
-}
-
-// WithIndirectGCInterval sets the time interval in between GC runs.
-func WithIndirectGCInterval(dur time.Duration) Option {
-	return func(db *Lowlevel) {
-		if dur > 0 {
-			db.indirectGCInterval = dur
-		}
-	}
-}
-
-// ListFolders returns the list of folders currently in the database
-func (db *Lowlevel) ListFolders() []string {
-	return db.folderIdx.Values()
-}
-
-// updateRemoteFiles adds a list of fileinfos to the database and updates the
-// global versionlist and metadata.
-func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var dk, gk, keyBuf []byte
-	devID, err := protocol.DeviceIDFromBytes(device)
-	if err != nil {
-		return err
-	}
-	for _, f := range fs {
-		name := []byte(f.Name)
-		dk, err = db.keyer.GenerateDeviceFileKey(dk, folder, device, name)
-		if err != nil {
-			return err
-		}
-
-		ef, ok, err := t.getFileTrunc(dk, true)
-		if err != nil {
-			return err
-		}
-
-		if ok {
-			meta.removeFile(devID, ef)
-		}
-		meta.addFile(devID, f)
-
-		l.Debugf("insert (remote); folder=%q device=%v %v", folder, devID, f)
-		if err := t.putFile(dk, f); err != nil {
-			return err
-		}
-
-		gk, err = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
-		if err != nil {
-			return err
-		}
-		keyBuf, err = t.updateGlobal(gk, keyBuf, folder, device, f, meta)
-		if err != nil {
-			return err
-		}
-
-		if err := t.Checkpoint(); err != nil {
-			return err
-		}
-	}
-
-	return t.Commit()
-}
-
-// updateLocalFiles adds fileinfos to the db, and updates the global versionlist,
-// metadata, sequence and blockmap buckets.
-func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta *metadataTracker) error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var dk, gk, keyBuf []byte
-	blockBuf := make([]byte, 4)
-	for _, f := range fs {
-		name := []byte(f.Name)
-		dk, err = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], name)
-		if err != nil {
-			return err
-		}
-
-		ef, ok, err := t.getFileByKey(dk)
-		if err != nil {
-			return err
-		}
-
-		blocksHashSame := ok && bytes.Equal(ef.BlocksHash, f.BlocksHash)
-		if ok {
-			keyBuf, err = db.removeLocalBlockAndSequenceInfo(keyBuf, folder, name, ef, !blocksHashSame, &t)
-			if err != nil {
-				return err
-			}
-		}
-
-		f.Sequence = meta.nextLocalSeq()
-
-		if ok {
-			meta.removeFile(protocol.LocalDeviceID, ef)
-		}
-		meta.addFile(protocol.LocalDeviceID, f)
-
-		l.Debugf("insert (local); folder=%q %v", folder, f)
-		if err := t.putFile(dk, f); err != nil {
-			return err
-		}
-
-		gk, err = db.keyer.GenerateGlobalVersionKey(gk, folder, []byte(f.Name))
-		if err != nil {
-			return err
-		}
-		keyBuf, err = t.updateGlobal(gk, keyBuf, folder, protocol.LocalDeviceID[:], f, meta)
-		if err != nil {
-			return err
-		}
-
-		keyBuf, err = db.keyer.GenerateSequenceKey(keyBuf, folder, f.Sequence)
-		if err != nil {
-			return err
-		}
-		if err := t.Put(keyBuf, dk); err != nil {
-			return err
-		}
-		l.Debugf("adding sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
-
-		if len(f.Blocks) != 0 && !f.IsInvalid() && f.Size > 0 {
-			for i, block := range f.Blocks {
-				binary.BigEndian.PutUint32(blockBuf, uint32(i))
-				keyBuf, err = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
-				if err != nil {
-					return err
-				}
-				if err := t.Put(keyBuf, blockBuf); err != nil {
-					return err
-				}
-			}
-			if !blocksHashSame {
-				keyBuf, err := db.keyer.GenerateBlockListMapKey(keyBuf, folder, f.BlocksHash, name)
-				if err != nil {
-					return err
-				}
-				if err = t.Put(keyBuf, nil); err != nil {
-					return err
-				}
-			}
-		}
-
-		if err := t.Checkpoint(); err != nil {
-			return err
-		}
-	}
-
-	return t.Commit()
-}
-
-func (db *Lowlevel) removeLocalFiles(folder []byte, nameStrs []string, meta *metadataTracker) error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var dk, gk, buf []byte
-	for _, nameStr := range nameStrs {
-		name := []byte(nameStr)
-		dk, err = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], name)
-		if err != nil {
-			return err
-		}
-
-		ef, ok, err := t.getFileByKey(dk)
-		if err != nil {
-			return err
-		}
-		if !ok {
-			l.Debugf("remove (local); folder=%q %v: file doesn't exist", folder, nameStr)
-			continue
-		}
-
-		buf, err = db.removeLocalBlockAndSequenceInfo(buf, folder, name, ef, true, &t)
-		if err != nil {
-			return err
-		}
-
-		meta.removeFile(protocol.LocalDeviceID, ef)
-
-		gk, err = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
-		if err != nil {
-			return err
-		}
-		buf, err = t.removeFromGlobal(gk, buf, folder, protocol.LocalDeviceID[:], name, meta)
-		if err != nil {
-			return err
-		}
-
-		err = t.Delete(dk)
-		if err != nil {
-			return err
-		}
-
-		if err := t.Checkpoint(); err != nil {
-			return err
-		}
-	}
-
-	return t.Commit()
-}
-
-func (db *Lowlevel) removeLocalBlockAndSequenceInfo(keyBuf, folder, name []byte, ef protocol.FileInfo, removeFromBlockListMap bool, t *readWriteTransaction) ([]byte, error) {
-	var err error
-	if len(ef.Blocks) != 0 && !ef.IsInvalid() && ef.Size > 0 {
-		for _, block := range ef.Blocks {
-			keyBuf, err = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
-			if err != nil {
-				return nil, err
-			}
-			if err := t.Delete(keyBuf); err != nil {
-				return nil, err
-			}
-		}
-		if removeFromBlockListMap {
-			keyBuf, err := db.keyer.GenerateBlockListMapKey(keyBuf, folder, ef.BlocksHash, name)
-			if err != nil {
-				return nil, err
-			}
-			if err = t.Delete(keyBuf); err != nil {
-				return nil, err
-			}
-		}
-	}
-
-	keyBuf, err = db.keyer.GenerateSequenceKey(keyBuf, folder, ef.SequenceNo())
-	if err != nil {
-		return nil, err
-	}
-	if err := t.Delete(keyBuf); err != nil {
-		return nil, err
-	}
-	l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, ef.SequenceNo(), ef.FileName())
-	return keyBuf, nil
-}
-
-func (db *Lowlevel) dropFolder(folder []byte) error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	// Remove all items related to the given folder from the device->file bucket
-	k0, err := db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k0.WithoutNameAndDevice()); err != nil {
-		return err
-	}
-
-	// Remove all sequences related to the folder
-	k1, err := db.keyer.GenerateSequenceKey(k0, folder, 0)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k1.WithoutSequence()); err != nil {
-		return err
-	}
-
-	// Remove all items related to the given folder from the global bucket
-	k2, err := db.keyer.GenerateGlobalVersionKey(k1, folder, nil)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k2.WithoutName()); err != nil {
-		return err
-	}
-
-	// Remove all needs related to the folder
-	k3, err := db.keyer.GenerateNeedFileKey(k2, folder, nil)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k3.WithoutName()); err != nil {
-		return err
-	}
-
-	// Remove the blockmap of the folder
-	k4, err := db.keyer.GenerateBlockMapKey(k3, folder, nil, nil)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k4.WithoutHashAndName()); err != nil {
-		return err
-	}
-
-	k5, err := db.keyer.GenerateBlockListMapKey(k4, folder, nil, nil)
-	if err != nil {
-		return err
-	}
-	if err := t.deleteKeyPrefix(k5.WithoutHashAndName()); err != nil {
-		return err
-	}
-
-	return t.Commit()
-}
-
-func (db *Lowlevel) dropDeviceFolder(device, folder []byte, meta *metadataTracker) error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	key, err := db.keyer.GenerateDeviceFileKey(nil, folder, device, nil)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key)
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	var gk, keyBuf []byte
-	for dbi.Next() {
-		name := db.keyer.NameFromDeviceFileKey(dbi.Key())
-		gk, err = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
-		if err != nil {
-			return err
-		}
-		keyBuf, err = t.removeFromGlobal(gk, keyBuf, folder, device, name, meta)
-		if err != nil {
-			return err
-		}
-		if err := t.Delete(dbi.Key()); err != nil {
-			return err
-		}
-		if err := t.Checkpoint(); err != nil {
-			return err
-		}
-	}
-	dbi.Release()
-	if err := dbi.Error(); err != nil {
-		return err
-	}
-
-	if bytes.Equal(device, protocol.LocalDeviceID[:]) {
-		key, err := db.keyer.GenerateBlockMapKey(nil, folder, nil, nil)
-		if err != nil {
-			return err
-		}
-		if err := t.deleteKeyPrefix(key.WithoutHashAndName()); err != nil {
-			return err
-		}
-		key2, err := db.keyer.GenerateBlockListMapKey(key, folder, nil, nil)
-		if err != nil {
-			return err
-		}
-		if err := t.deleteKeyPrefix(key2.WithoutHashAndName()); err != nil {
-			return err
-		}
-	}
-	return t.Commit()
-}
-
-func (db *Lowlevel) checkGlobals(folderStr string) (int, error) {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return 0, err
-	}
-	defer t.close()
-
-	folder := []byte(folderStr)
-	key, err := db.keyer.GenerateGlobalVersionKey(nil, folder, nil)
-	if err != nil {
-		return 0, err
-	}
-	dbi, err := t.NewPrefixIterator(key.WithoutName())
-	if err != nil {
-		return 0, err
-	}
-	defer dbi.Release()
-
-	fixed := 0
-	var dk []byte
-	ro := t.readOnlyTransaction
-	for dbi.Next() {
-		var vl dbproto.VersionList
-		if err := proto.Unmarshal(dbi.Value(), &vl); err != nil || len(vl.Versions) == 0 {
-			if err := t.Delete(dbi.Key()); err != nil && !backend.IsNotFound(err) {
-				return 0, err
-			}
-			continue
-		}
-
-		// Check the global version list for consistency. An issue in previous
-		// versions of goleveldb could result in reordered writes so that
-		// there are global entries pointing to no longer existing files. Here
-		// we find those and clear them out.
-
-		name := db.keyer.NameFromGlobalVersionKey(dbi.Key())
-		newVL := &dbproto.VersionList{}
-		var changed, changedHere bool
-		for _, fv := range vl.Versions {
-			changedHere, err = checkGlobalsFilterDevices(dk, folder, name, fv.Devices, newVL, ro)
-			if err != nil {
-				return 0, err
-			}
-			changed = changed || changedHere
-
-			changedHere, err = checkGlobalsFilterDevices(dk, folder, name, fv.InvalidDevices, newVL, ro)
-			if err != nil {
-				return 0, err
-			}
-			changed = changed || changedHere
-		}
-
-		if len(newVL.Versions) == 0 {
-			if err := t.Delete(dbi.Key()); err != nil && !backend.IsNotFound(err) {
-				return 0, err
-			}
-			fixed++
-		} else if changed {
-			if err := t.Put(dbi.Key(), mustMarshal(newVL)); err != nil {
-				return 0, err
-			}
-			fixed++
-		}
-	}
-	dbi.Release()
-	if err := dbi.Error(); err != nil {
-		return 0, err
-	}
-
-	l.Debugf("global db check completed for %v", folder)
-	return fixed, t.Commit()
-}
-
-func checkGlobalsFilterDevices(dk, folder, name []byte, devices [][]byte, vl *dbproto.VersionList, t readOnlyTransaction) (bool, error) {
-	var changed bool
-	var err error
-	for _, device := range devices {
-		dk, err = t.keyer.GenerateDeviceFileKey(dk, folder, device, name)
-		if err != nil {
-			return false, err
-		}
-		f, ok, err := t.getFileTrunc(dk, false)
-		if err != nil {
-			return false, err
-		}
-		if !ok {
-			changed = true
-			continue
-		}
-		_, _, _, _, _, _, err = vlUpdate(vl, folder, device, f, t)
-		if err != nil {
-			return false, err
-		}
-	}
-	return changed, nil
-}
-
-func (db *Lowlevel) getIndexID(device, folder []byte) (protocol.IndexID, error) {
-	key, err := db.keyer.GenerateIndexIDKey(nil, device, folder)
-	if err != nil {
-		return 0, err
-	}
-	cur, err := db.Get(key)
-	if backend.IsNotFound(err) {
-		return 0, nil
-	} else if err != nil {
-		return 0, err
-	}
-
-	var id protocol.IndexID
-	if err := id.Unmarshal(cur); err != nil {
-		return 0, nil //nolint: nilerr
-	}
-
-	return id, nil
-}
-
-func (db *Lowlevel) setIndexID(device, folder []byte, id protocol.IndexID) error {
-	bs, _ := id.Marshal() // marshalling can't fail
-	key, err := db.keyer.GenerateIndexIDKey(nil, device, folder)
-	if err != nil {
-		return err
-	}
-	return db.Put(key, bs)
-}
-
-func (db *Lowlevel) dropFolderIndexIDs(folder []byte) error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	if err := t.deleteKeyPrefixMatching([]byte{KeyTypeIndexID}, func(key []byte) bool {
-		keyFolder, ok := t.keyer.FolderFromIndexIDKey(key)
-		if !ok {
-			l.Debugf("Deleting IndexID with missing FolderIdx: %v", key)
-			return true
-		}
-		return bytes.Equal(keyFolder, folder)
-	}); err != nil {
-		return err
-	}
-	return t.Commit()
-}
-
-func (db *Lowlevel) dropIndexIDs() error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-	if err := t.deleteKeyPrefix([]byte{KeyTypeIndexID}); err != nil {
-		return err
-	}
-	return t.Commit()
-}
-
-// dropOtherDeviceIndexIDs drops all index IDs for devices other than the
-// local device. This means we will resend our indexes to all other devices,
-// but they don't have to resend to us.
-func (db *Lowlevel) dropOtherDeviceIndexIDs() error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-	if err := t.deleteKeyPrefixMatching([]byte{KeyTypeIndexID}, func(key []byte) bool {
-		dev, _ := t.keyer.DeviceFromIndexIDKey(key)
-		return !bytes.Equal(dev, protocol.LocalDeviceID[:])
-	}); err != nil {
-		return err
-	}
-	return t.Commit()
-}
-
-func (db *Lowlevel) dropMtimes(folder []byte) error {
-	key, err := db.keyer.GenerateMtimesKey(nil, folder)
-	if err != nil {
-		return err
-	}
-	return db.dropPrefix(key)
-}
-
-func (db *Lowlevel) dropFolderMeta(folder []byte) error {
-	key, err := db.keyer.GenerateFolderMetaKey(nil, folder)
-	if err != nil {
-		return err
-	}
-	return db.dropPrefix(key)
-}
-
-func (db *Lowlevel) dropPrefix(prefix []byte) error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	if err := t.deleteKeyPrefix(prefix); err != nil {
-		return err
-	}
-	return t.Commit()
-}
-
-func (db *Lowlevel) gcRunner(ctx context.Context) error {
-	// Calculate the time for the next GC run. Even if we should run GC
-	// directly, give the system a while to get up and running and do other
-	// stuff first. (We might have migrations and stuff which would be
-	// better off running before GC.)
-	next := db.timeUntil(indirectGCTimeKey, db.indirectGCInterval)
-	if next < time.Minute {
-		next = time.Minute
-	}
-
-	t := time.NewTimer(next)
-	defer t.Stop()
-
-	for {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case <-t.C:
-			if err := db.gcIndirect(ctx); err != nil {
-				l.Warnln("Database indirection GC failed:", err)
-			}
-			db.recordTime(indirectGCTimeKey)
-			t.Reset(db.timeUntil(indirectGCTimeKey, db.indirectGCInterval))
-		}
-	}
-}
-
-// recordTime records the current time under the given key, affecting the
-// next call to timeUntil with the same key.
-func (db *Lowlevel) recordTime(key string) {
-	miscDB := NewMiscDataNamespace(db)
-	_ = miscDB.PutInt64(key, time.Now().Unix()) // error wilfully ignored
-}
-
-// timeUntil returns how long we should wait until the next interval, or
-// zero if it should happen directly.
-func (db *Lowlevel) timeUntil(key string, every time.Duration) time.Duration {
-	miscDB := NewMiscDataNamespace(db)
-	lastTime, _, _ := miscDB.Int64(key) // error wilfully ignored
-	nextTime := time.Unix(lastTime, 0).Add(every)
-	sleepTime := time.Until(nextTime)
-	if sleepTime < 0 {
-		sleepTime = 0
-	}
-	return sleepTime
-}
-
-func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
-	// The indirection GC uses bloom filters to track used block lists and
-	// versions. This means iterating over all items, adding their hashes to
-	// the filter, then iterating over the indirected items and removing
-	// those that don't match the filter. The filter will give false
-	// positives so we will keep around one percent of things that we don't
-	// really need (at most).
-	//
-	// Indirection GC needs to run when there are no modifications to the
-	// FileInfos or indirected items.
-
-	l.Debugln("Starting database GC")
-
-	// Create a new set of bloom filters, while holding the gcMut which
-	// guarantees that no other modifications are happening concurrently.
-
-	db.gcMut.Lock()
-	capacity := indirectGCBloomCapacity
-	if db.gcKeyCount > capacity {
-		capacity = db.gcKeyCount
-	}
-	db.blockFilter = newBloomFilter(capacity)
-	db.versionFilter = newBloomFilter(capacity)
-	db.gcMut.Unlock()
-
-	defer func() {
-		// Forget the bloom filters on the way out.
-		db.gcMut.Lock()
-		db.blockFilter = nil
-		db.versionFilter = nil
-		db.gcMut.Unlock()
-	}()
-
-	var discardedBlocks, matchedBlocks, discardedVersions, matchedVersions int
-
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.Release()
-
-	// Set up the bloom filters with the initial capacity and false positive
-	// rate, or higher capacity if we've done this before and seen lots of
-	// items. For simplicity's sake we track just one count, which is the
-	// highest of the various indirected items.
-
-	// Iterate the FileInfos, unmarshal the block and version hashes and
-	// add them to the filter.
-
-	// This happens concurrently with normal database modifications, though
-	// those modifications will now also add their blocks and versions to
-	// the bloom filters.
-
-	it, err := t.NewPrefixIterator([]byte{KeyTypeDevice})
-	if err != nil {
-		return err
-	}
-	defer it.Release()
-	for it.Next() {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		default:
-		}
-
-		var hashes dbproto.IndirectionHashesOnly
-		if err := proto.Unmarshal(it.Value(), &hashes); err != nil {
-			return err
-		}
-		db.recordIndirectionHashes(&hashes)
-	}
-	it.Release()
-	if err := it.Error(); err != nil {
-		return err
-	}
-
-	// For the next phase we grab the GC lock again and hold it for the rest
-	// of the method call. Now there can't be any further modifications to
-	// the database or the bloom filters.
-
-	db.gcMut.Lock()
-	defer db.gcMut.Unlock()
-
-	// Only print something if the process takes more than "a moment".
-	logWait := make(chan struct{})
-	logTimer := time.AfterFunc(10*time.Second, func() {
-		l.Infoln("Database GC in progress - many Syncthing operations will be unresponsive until it's finished")
-		close(logWait)
-	})
-	defer func() {
-		if logTimer.Stop() {
-			return
-		}
-		<-logWait // Make sure messages are sent in order.
-		l.Infof("Database GC complete (discarded/remaining: %v/%v blocks, %v/%v versions)",
-			discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
-	}()
-
-	// Iterate over block lists, removing keys with hashes that don't match
-	// the filter.
-
-	it, err = t.NewPrefixIterator([]byte{KeyTypeBlockList})
-	if err != nil {
-		return err
-	}
-	defer it.Release()
-	for it.Next() {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		default:
-		}
-
-		key := blockListKey(it.Key())
-		if db.blockFilter.has(key.Hash()) {
-			matchedBlocks++
-			continue
-		}
-		if err := t.Delete(key); err != nil {
-			return err
-		}
-		discardedBlocks++
-	}
-	it.Release()
-	if err := it.Error(); err != nil {
-		return err
-	}
-
-	// Iterate over version lists, removing keys with hashes that don't match
-	// the filter.
-
-	it, err = db.NewPrefixIterator([]byte{KeyTypeVersion})
-	if err != nil {
-		return err
-	}
-	for it.Next() {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		default:
-		}
-
-		key := versionKey(it.Key())
-		if db.versionFilter.has(key.Hash()) {
-			matchedVersions++
-			continue
-		}
-		if err := t.Delete(key); err != nil {
-			return err
-		}
-		discardedVersions++
-	}
-	it.Release()
-	if err := it.Error(); err != nil {
-		return err
-	}
-
-	// Remember the number of unique keys we kept until the next pass.
-	db.gcKeyCount = matchedBlocks
-	if matchedVersions > matchedBlocks {
-		db.gcKeyCount = matchedVersions
-	}
-
-	if err := t.Commit(); err != nil {
-		return err
-	}
-
-	l.Debugf("Finished GC (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
-
-	return nil
-}
-
-func (db *Lowlevel) recordIndirectionHashesForFile(f *protocol.FileInfo) {
-	db.recordIndirectionHashes(&dbproto.IndirectionHashesOnly{BlocksHash: f.BlocksHash, VersionHash: f.VersionHash})
-}
-
-func (db *Lowlevel) recordIndirectionHashes(hs *dbproto.IndirectionHashesOnly) {
-	// must be called with gcMut held (at least read-held)
-	if db.blockFilter != nil && len(hs.BlocksHash) > 0 {
-		db.blockFilter.add(hs.BlocksHash)
-	}
-	if db.versionFilter != nil && len(hs.VersionHash) > 0 {
-		db.versionFilter.add(hs.VersionHash)
-	}
-}
-
-func newBloomFilter(capacity int) *bloomFilter {
-	return &bloomFilter{
-		f: blobloom.NewSyncOptimized(blobloom.Config{
-			Capacity: uint64(capacity),
-			FPRate:   indirectGCBloomFalsePositiveRate,
-			MaxBits:  8 * indirectGCBloomMaxBytes,
-		}),
-		seed: maphash.MakeSeed(),
-	}
-}
-
-type bloomFilter struct {
-	f    *blobloom.SyncFilter
-	seed maphash.Seed
-}
-
-func (b *bloomFilter) add(id []byte)      { b.f.Add(b.hash(id)) }
-func (b *bloomFilter) has(id []byte) bool { return b.f.Has(b.hash(id)) }
-
-// Hash function for the bloomfilter: maphash of the SHA-256.
-//
-// The randomization in maphash should ensure that we get different collisions
-// across runs, so colliding keys are not kept indefinitely.
-func (b *bloomFilter) hash(id []byte) uint64 {
-	if len(id) != sha256.Size {
-		panic("bug: bloomFilter.hash passed something not a SHA256 hash")
-	}
-	var h maphash.Hash
-	h.SetSeed(b.seed)
-	_, _ = h.Write(id)
-	return h.Sum64()
-}
-
-// checkRepair checks folder metadata and sequences for miscellaneous errors.
-func (db *Lowlevel) checkRepair() error {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-	for _, folder := range db.ListFolders() {
-		if _, err := db.getMetaAndCheckGCLocked(folder); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (db *Lowlevel) getMetaAndCheck(folder string) (*metadataTracker, error) {
-	db.gcMut.RLock()
-	defer db.gcMut.RUnlock()
-
-	return db.getMetaAndCheckGCLocked(folder)
-}
-
-func (db *Lowlevel) getMetaAndCheckGCLocked(folder string) (*metadataTracker, error) {
-	fixed, err := db.checkLocalNeed([]byte(folder))
-	if err != nil {
-		return nil, fmt.Errorf("checking local need: %w", err)
-	}
-	if fixed != 0 {
-		l.Infof("Repaired %d local need entries for folder %v in database", fixed, folder)
-	}
-
-	fixed, err = db.checkGlobals(folder)
-	if err != nil {
-		return nil, fmt.Errorf("checking globals: %w", err)
-	}
-	if fixed != 0 {
-		l.Infof("Repaired %d global entries for folder %v in database", fixed, folder)
-	}
-
-	oldMeta := newMetadataTracker(db.keyer, db.evLogger)
-	_ = oldMeta.fromDB(db, []byte(folder)) // Ignore error, it leads to index id reset too
-	meta, err := db.recalcMeta(folder)
-	if err != nil {
-		return nil, fmt.Errorf("recalculating metadata: %w", err)
-	}
-
-	fixed, err = db.repairSequenceGCLocked(folder, meta)
-	if err != nil {
-		return nil, fmt.Errorf("repairing sequences: %w", err)
-	}
-	if fixed != 0 {
-		l.Infof("Repaired %d sequence entries for folder %v in database", fixed, folder)
-		meta, err = db.recalcMeta(folder)
-		if err != nil {
-			return nil, fmt.Errorf("recalculating metadata: %w", err)
-		}
-	}
-
-	if err := db.checkSequencesUnchanged(folder, oldMeta, meta); err != nil {
-		return nil, fmt.Errorf("checking for changed sequences: %w", err)
-	}
-
-	return meta, nil
-}
-
-func (db *Lowlevel) loadMetadataTracker(folder string) (*metadataTracker, error) {
-	meta := newMetadataTracker(db.keyer, db.evLogger)
-	if err := meta.fromDB(db, []byte(folder)); err != nil {
-		if errors.Is(err, errMetaInconsistent) {
-			l.Infof("Stored folder metadata for %q is inconsistent; recalculating", folder)
-		} else {
-			l.Infof("No stored folder metadata for %q; recalculating", folder)
-		}
-		return db.getMetaAndCheck(folder)
-	}
-
-	curSeq := meta.Sequence(protocol.LocalDeviceID)
-	if metaOK, err := db.verifyLocalSequence(curSeq, folder); err != nil {
-		return nil, fmt.Errorf("verifying sequences: %w", err)
-	} else if !metaOK {
-		l.Infof("Stored folder metadata for %q is out of date after crash; recalculating", folder)
-		return db.getMetaAndCheck(folder)
-	}
-
-	if age := time.Since(meta.Created()); age > db.recheckInterval {
-		l.Infof("Stored folder metadata for %q is %v old; recalculating", folder, stringutil.NiceDurationString(age))
-		return db.getMetaAndCheck(folder)
-	}
-
-	return meta, nil
-}
-
-func (db *Lowlevel) recalcMeta(folderStr string) (*metadataTracker, error) {
-	folder := []byte(folderStr)
-
-	meta := newMetadataTracker(db.keyer, db.evLogger)
-
-	t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-	if err != nil {
-		return nil, err
-	}
-	defer t.close()
-
-	var deviceID protocol.DeviceID
-	err = t.withAllFolderTruncated(folder, func(device []byte, f protocol.FileInfo) bool {
-		copy(deviceID[:], device)
-		meta.addFile(deviceID, f)
-		return true
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	err = t.withGlobal(folder, nil, true, func(f protocol.FileInfo) bool {
-		meta.addFile(protocol.GlobalDeviceID, f)
-		return true
-	})
-	if err != nil {
-		return nil, err
-	}
-
-	meta.emptyNeeded(protocol.LocalDeviceID)
-	err = t.withNeed(folder, protocol.LocalDeviceID[:], true, func(f protocol.FileInfo) bool {
-		meta.addNeeded(protocol.LocalDeviceID, f)
-		return true
-	})
-	if err != nil {
-		return nil, err
-	}
-	for _, device := range meta.devices() {
-		meta.emptyNeeded(device)
-		err = t.withNeed(folder, device[:], true, func(f protocol.FileInfo) bool {
-			meta.addNeeded(device, f)
-			return true
-		})
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	meta.SetCreated()
-	if err := t.Commit(); err != nil {
-		return nil, err
-	}
-	return meta, nil
-}
-
-// Verify the local sequence number from actual sequence entries. Returns
-// true if it was all good, or false if a fixup was necessary.
-func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) (bool, error) {
-	// Walk the sequence index from the current (supposedly) highest
-	// sequence number and raise the alarm if we get anything. This recovers
-	// from the occasion where we have written sequence entries to disk but
-	// not yet written new metadata to disk.
-	//
-	// Note that we can have the same thing happen for remote devices but
-	// there it's not a problem -- we'll simply advertise a lower sequence
-	// number than we've actually seen and receive some duplicate updates
-	// and then be in sync again.
-
-	t, err := db.newReadOnlyTransaction()
-	if err != nil {
-		return false, err
-	}
-	ok := true
-	if err := t.withHaveSequence([]byte(folder), curSeq+1, func(_ protocol.FileInfo) bool {
-		ok = false // we got something, which we should not have
-		return false
-	}); err != nil {
-		return false, err
-	}
-	t.close()
-
-	return ok, nil
-}
-
-// repairSequenceGCLocked makes sure the sequence numbers in the sequence keys
-// match those in the corresponding file entries. It returns the amount of fixed
-// entries.
-func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTracker) (int, error) {
-	t, err := db.newReadWriteTransaction(meta.CommitHook([]byte(folderStr)))
-	if err != nil {
-		return 0, err
-	}
-	defer t.close()
-
-	fixed := 0
-
-	folder := []byte(folderStr)
-
-	// First check that every file entry has a matching sequence entry
-	// (this was previously db schema upgrade to 9).
-
-	dk, err := t.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], nil)
-	if err != nil {
-		return 0, err
-	}
-	it, err := t.NewPrefixIterator(dk.WithoutName())
-	if err != nil {
-		return 0, err
-	}
-	defer it.Release()
-
-	var sk sequenceKey
-	for it.Next() {
-		intf, err := t.unmarshalTrunc(it.Value(), false)
-		if err != nil {
-			// Delete local items with invalid indirected blocks/versions.
-			// They will be rescanned.
-			var ierr *blocksIndirectionError
-			if ok := errors.As(err, &ierr); ok && backend.IsNotFound(err) {
-				intf, err = t.unmarshalTrunc(it.Value(), true)
-				if err != nil {
-					return 0, err
-				}
-				name := []byte(intf.FileName())
-				gk, err := t.keyer.GenerateGlobalVersionKey(nil, folder, name)
-				if err != nil {
-					return 0, err
-				}
-				_, err = t.removeFromGlobal(gk, nil, folder, protocol.LocalDeviceID[:], name, nil)
-				if err != nil {
-					return 0, err
-				}
-				sk, err = db.keyer.GenerateSequenceKey(sk, folder, intf.SequenceNo())
-				if err != nil {
-					return 0, err
-				}
-				if err := t.Delete(sk); err != nil {
-					return 0, err
-				}
-				if err := t.Delete(it.Key()); err != nil {
-					return 0, err
-				}
-			}
-			return 0, err
-		}
-		if sk, err = t.keyer.GenerateSequenceKey(sk, folder, intf.Sequence); err != nil {
-			return 0, err
-		}
-		switch dk, err = t.Get(sk); {
-		case err != nil:
-			if !backend.IsNotFound(err) {
-				return 0, err
-			}
-			fallthrough
-		case !bytes.Equal(it.Key(), dk):
-			fixed++
-			intf.Sequence = meta.nextLocalSeq()
-			if sk, err = t.keyer.GenerateSequenceKey(sk, folder, intf.Sequence); err != nil {
-				return 0, err
-			}
-			if err := t.Put(sk, it.Key()); err != nil {
-				return 0, err
-			}
-			if err := t.putFile(it.Key(), intf); err != nil {
-				return 0, err
-			}
-		}
-		if err := t.Checkpoint(); err != nil {
-			return 0, err
-		}
-	}
-	if err := it.Error(); err != nil {
-		return 0, err
-	}
-
-	it.Release()
-
-	// Secondly check there's no sequence entries pointing at incorrect things.
-
-	sk, err = t.keyer.GenerateSequenceKey(sk, folder, 0)
-	if err != nil {
-		return 0, err
-	}
-
-	it, err = t.NewPrefixIterator(sk.WithoutSequence())
-	if err != nil {
-		return 0, err
-	}
-	defer it.Release()
-
-	for it.Next() {
-		// Check that the sequence from the key matches the
-		// sequence in the file.
-		fi, ok, err := t.getFileTrunc(it.Value(), true)
-		if err != nil {
-			return 0, err
-		}
-		if ok {
-			if seq := t.keyer.SequenceFromSequenceKey(it.Key()); seq == fi.SequenceNo() {
-				continue
-			}
-		}
-		// Either the file is missing or has a different sequence number
-		fixed++
-		if err := t.Delete(it.Key()); err != nil {
-			return 0, err
-		}
-	}
-	if err := it.Error(); err != nil {
-		return 0, err
-	}
-
-	it.Release()
-
-	return fixed, t.Commit()
-}
-
-// Does not take care of metadata - if anything is repaired, the need count
-// needs to be recalculated.
-func (db *Lowlevel) checkLocalNeed(folder []byte) (int, error) {
-	repaired := 0
-
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return 0, err
-	}
-	defer t.close()
-
-	key, err := t.keyer.GenerateNeedFileKey(nil, folder, nil)
-	if err != nil {
-		return 0, err
-	}
-	dbi, err := t.NewPrefixIterator(key.WithoutName())
-	if err != nil {
-		return 0, err
-	}
-	defer dbi.Release()
-
-	var needName string
-	var needDone bool
-	next := func() {
-		needDone = !dbi.Next()
-		if !needDone {
-			needName = string(t.keyer.NameFromGlobalVersionKey(dbi.Key()))
-		}
-	}
-	next()
-	itErr := t.withNeedIteratingGlobal(folder, protocol.LocalDeviceID[:], true, func(fi protocol.FileInfo) bool {
-		for !needDone && needName < fi.Name {
-			repaired++
-			if err = t.Delete(dbi.Key()); err != nil && !backend.IsNotFound(err) {
-				return false
-			}
-			l.Debugln("check local need: removing", needName)
-			next()
-		}
-		if needName == fi.Name {
-			next()
-		} else {
-			repaired++
-			key, err = t.keyer.GenerateNeedFileKey(key, folder, []byte(fi.Name))
-			if err != nil {
-				return false
-			}
-			if err = t.Put(key, nil); err != nil {
-				return false
-			}
-			l.Debugln("check local need: adding", fi.Name)
-		}
-		return true
-	})
-	if err != nil {
-		return 0, err
-	}
-	if itErr != nil {
-		return 0, itErr
-	}
-
-	for !needDone {
-		repaired++
-		if err := t.Delete(dbi.Key()); err != nil && !backend.IsNotFound(err) {
-			return 0, err
-		}
-		l.Debugln("check local need: removing", needName)
-		next()
-	}
-
-	if err := dbi.Error(); err != nil {
-		return 0, err
-	}
-	dbi.Release()
-
-	if err = t.Commit(); err != nil {
-		return 0, err
-	}
-
-	return repaired, nil
-}
-
-// checkSequencesUnchanged resets delta indexes for any device where the
-// sequence changed.
-func (db *Lowlevel) checkSequencesUnchanged(folder string, oldMeta, meta *metadataTracker) error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var key []byte
-	deleteIndexID := func(devID protocol.DeviceID) error {
-		key, err = db.keyer.GenerateIndexIDKey(key, devID[:], []byte(folder))
-		if err != nil {
-			return err
-		}
-		return t.Delete(key)
-	}
-
-	if oldMeta.Sequence(protocol.LocalDeviceID) != meta.Sequence(protocol.LocalDeviceID) {
-		if err := deleteIndexID(protocol.LocalDeviceID); err != nil {
-			return err
-		}
-		l.Infof("Local sequence for folder %v changed while repairing - dropping delta indexes", folder)
-	}
-
-	oldDevices := oldMeta.devices()
-	oldSequences := make(map[protocol.DeviceID]int64, len(oldDevices))
-	for _, devID := range oldDevices {
-		oldSequences[devID] = oldMeta.Sequence(devID)
-	}
-	for _, devID := range meta.devices() {
-		oldSeq := oldSequences[devID]
-		delete(oldSequences, devID)
-		// A lower sequence number just means we will receive some indexes again.
-		if oldSeq >= meta.Sequence(devID) {
-			if oldSeq > meta.Sequence(devID) {
-				db.evLogger.Log(events.Failure, "lower remote sequence after recalculating metadata")
-			}
-			continue
-		}
-		db.evLogger.Log(events.Failure, "higher remote sequence after recalculating metadata")
-		if err := deleteIndexID(devID); err != nil {
-			return err
-		}
-		l.Infof("Sequence of device %v for folder %v changed while repairing - dropping delta indexes", devID.Short(), folder)
-	}
-	for devID := range oldSequences {
-		if err := deleteIndexID(devID); err != nil {
-			return err
-		}
-		l.Debugf("Removed indexID of device %v for folder %v which isn't present anymore", devID.Short(), folder)
-	}
-
-	return t.Commit()
-}
-
-func (db *Lowlevel) needsRepairPath() string {
-	path := db.Location()
-	if path == "" {
-		return ""
-	}
-	if path[len(path)-1] == fs.PathSeparator {
-		path = path[:len(path)-1]
-	}
-	return path + needsRepairSuffix
-}
-
-func (db *Lowlevel) checkErrorForRepair(err error) {
-	if errors.Is(err, errEntryFromGlobalMissing) || errors.Is(err, errEmptyGlobal) {
-		// Inconsistency error, mark db for repair on next start.
-		if path := db.needsRepairPath(); path != "" {
-			if fd, err := os.Create(path); err == nil {
-				fd.Close()
-			}
-		}
-	}
-}
-
-func (db *Lowlevel) handleFailure(err error) {
-	db.checkErrorForRepair(err)
-	if shouldReportFailure(err) {
-		db.evLogger.Log(events.Failure, err.Error())
-	}
-}
-
-var ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
-
-func shouldReportFailure(err error) bool {
-	return !ldbPathRe.MatchString(err.Error())
-}
diff --git a/lib/db/meta.go b/lib/db/meta.go
deleted file mode 100644
index fd7ec1fce77..00000000000
--- a/lib/db/meta.go
+++ /dev/null
@@ -1,472 +0,0 @@
-// Copyright (C) 2017 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"errors"
-	"fmt"
-	"math/bits"
-	"time"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/protocol"
-	"github.com/syncthing/syncthing/lib/sync"
-)
-
-var errMetaInconsistent = errors.New("inconsistent counts detected")
-
-type countsMap struct {
-	counts  CountsSet
-	indexes map[metaKey]int // device ID + local flags -> index in counts
-}
-
-// metadataTracker keeps metadata on a per device, per local flag basis.
-type metadataTracker struct {
-	keyer keyer
-	countsMap
-	mut      sync.RWMutex
-	dirty    bool
-	evLogger events.Logger
-}
-
-type metaKey struct {
-	dev  protocol.DeviceID
-	flag uint32
-}
-
-const needFlag uint32 = 1 << 31 // Last bit, as early ones are local flags
-
-func newMetadataTracker(keyer keyer, evLogger events.Logger) *metadataTracker {
-	return &metadataTracker{
-		keyer: keyer,
-		mut:   sync.NewRWMutex(),
-		countsMap: countsMap{
-			indexes: make(map[metaKey]int),
-		},
-		evLogger: evLogger,
-	}
-}
-
-// Unmarshal loads a metadataTracker from the corresponding protobuf
-// representation
-func (m *metadataTracker) Unmarshal(bs []byte) error {
-	var dbc dbproto.CountsSet
-	if err := proto.Unmarshal(bs, &dbc); err != nil {
-		return err
-	}
-	m.counts.Created = dbc.Created
-	m.counts.Counts = make([]Counts, len(dbc.Counts))
-	for i, c := range dbc.Counts {
-		m.counts.Counts[i] = countsFromWire(c)
-	}
-
-	// Initialize the index map
-	m.indexes = make(map[metaKey]int)
-	for i, c := range m.counts.Counts {
-		m.indexes[metaKey{c.DeviceID, c.LocalFlags}] = i
-	}
-	return nil
-}
-
-// protoMarshal returns the protobuf representation of the metadataTracker.
-// Must be called with the read lock held.
-func (m *metadataTracker) protoMarshal() ([]byte, error) {
-	dbc := &dbproto.CountsSet{
-		Counts:  make([]*dbproto.Counts, len(m.counts.Counts)),
-		Created: m.counts.Created,
-	}
-	for i, c := range m.counts.Counts {
-		dbc.Counts[i] = c.toWire()
-	}
-	return proto.Marshal(dbc)
-}
-
-func (m *metadataTracker) CommitHook(folder []byte) backend.CommitHook {
-	return func(t backend.WriteTransaction) error {
-		return m.toDB(t, folder)
-	}
-}
-
-// toDB saves the marshalled metadataTracker to the given db, under the key
-// corresponding to the given folder
-func (m *metadataTracker) toDB(t backend.WriteTransaction, folder []byte) error {
-	key, err := m.keyer.GenerateFolderMetaKey(nil, folder)
-	if err != nil {
-		return err
-	}
-
-	m.mut.RLock()
-	defer m.mut.RUnlock()
-
-	if !m.dirty {
-		return nil
-	}
-
-	bs, err := m.protoMarshal()
-	if err != nil {
-		return err
-	}
-	err = t.Put(key, bs)
-	if err == nil {
-		m.dirty = false
-	}
-
-	return err
-}
-
-// fromDB initializes the metadataTracker from the marshalled data found in
-// the database under the key corresponding to the given folder
-func (m *metadataTracker) fromDB(db *Lowlevel, folder []byte) error {
-	key, err := db.keyer.GenerateFolderMetaKey(nil, folder)
-	if err != nil {
-		return err
-	}
-	bs, err := db.Get(key)
-	if err != nil {
-		return err
-	}
-	if err = m.Unmarshal(bs); err != nil {
-		return err
-	}
-	if m.counts.Created == 0 {
-		return errMetaInconsistent
-	}
-	return nil
-}
-
-// countsPtr returns a pointer to the corresponding Counts struct, if
-// necessary allocating one in the process
-func (m *metadataTracker) countsPtr(dev protocol.DeviceID, flag uint32) *Counts {
-	// must be called with the mutex held
-
-	if bits.OnesCount32(flag) > 1 {
-		panic("incorrect usage: set at most one bit in flag")
-	}
-
-	key := metaKey{dev, flag}
-	idx, ok := m.indexes[key]
-	if !ok {
-		idx = len(m.counts.Counts)
-		m.counts.Counts = append(m.counts.Counts, Counts{DeviceID: dev, LocalFlags: flag})
-		m.indexes[key] = idx
-		// Need bucket must be initialized when a device first occurs in
-		// the metadatatracker, even if there's no change to the need
-		// bucket itself.
-		nkey := metaKey{dev, needFlag}
-		if _, ok := m.indexes[nkey]; !ok {
-			// Initially a new device needs everything, except deletes
-			nidx := len(m.counts.Counts)
-			m.counts.Counts = append(m.counts.Counts, m.allNeededCounts(dev))
-			m.indexes[nkey] = nidx
-		}
-	}
-	return &m.counts.Counts[idx]
-}
-
-// allNeeded makes sure there is a counts in case the device needs everything.
-func (m *countsMap) allNeededCounts(dev protocol.DeviceID) Counts {
-	var counts Counts
-	if idx, ok := m.indexes[metaKey{protocol.GlobalDeviceID, 0}]; ok {
-		counts = m.counts.Counts[idx]
-		counts.Deleted = 0 // Don't need deletes if having nothing
-	}
-	counts.DeviceID = dev
-	counts.LocalFlags = needFlag
-	return counts
-}
-
-// addFile adds a file to the counts, adjusting the sequence number as
-// appropriate
-func (m *metadataTracker) addFile(dev protocol.DeviceID, f protocol.FileInfo) {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	m.updateSeqLocked(dev, f)
-
-	m.updateFileLocked(dev, f, m.addFileLocked)
-}
-
-func (m *metadataTracker) updateFileLocked(dev protocol.DeviceID, f protocol.FileInfo, fn func(protocol.DeviceID, uint32, protocol.FileInfo)) {
-	m.dirty = true
-
-	if f.IsInvalid() && (f.FileLocalFlags() == 0 || dev == protocol.GlobalDeviceID) {
-		// This is a remote invalid file or concern the global state.
-		// In either case invalid files are not accounted.
-		return
-	}
-
-	if flags := f.FileLocalFlags(); flags == 0 {
-		// Account regular files in the zero-flags bucket.
-		fn(dev, 0, f)
-	} else {
-		// Account in flag specific buckets.
-		eachFlagBit(flags, func(flag uint32) {
-			fn(dev, flag, f)
-		})
-	}
-}
-
-// emptyNeeded ensures that there is a need count for the given device and that it is empty.
-func (m *metadataTracker) emptyNeeded(dev protocol.DeviceID) {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	m.dirty = true
-
-	empty := Counts{
-		DeviceID:   dev,
-		LocalFlags: needFlag,
-	}
-	key := metaKey{dev, needFlag}
-	if idx, ok := m.indexes[key]; ok {
-		m.counts.Counts[idx] = empty
-		return
-	}
-	m.indexes[key] = len(m.counts.Counts)
-	m.counts.Counts = append(m.counts.Counts, empty)
-}
-
-// addNeeded adds a file to the needed counts
-func (m *metadataTracker) addNeeded(dev protocol.DeviceID, f protocol.FileInfo) {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	m.dirty = true
-
-	m.addFileLocked(dev, needFlag, f)
-}
-
-func (m *metadataTracker) Sequence(dev protocol.DeviceID) int64 {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-	return m.countsPtr(dev, 0).Sequence
-}
-
-func (m *metadataTracker) updateSeqLocked(dev protocol.DeviceID, f protocol.FileInfo) {
-	if dev == protocol.GlobalDeviceID {
-		return
-	}
-	if cp := m.countsPtr(dev, 0); f.SequenceNo() > cp.Sequence {
-		cp.Sequence = f.SequenceNo()
-	}
-}
-
-func (m *metadataTracker) addFileLocked(dev protocol.DeviceID, flag uint32, f protocol.FileInfo) {
-	cp := m.countsPtr(dev, flag)
-
-	switch {
-	case f.IsDeleted():
-		cp.Deleted++
-	case f.IsDirectory() && !f.IsSymlink():
-		cp.Directories++
-	case f.IsSymlink():
-		cp.Symlinks++
-	default:
-		cp.Files++
-	}
-	cp.Bytes += f.FileSize()
-}
-
-// removeFile removes a file from the counts
-func (m *metadataTracker) removeFile(dev protocol.DeviceID, f protocol.FileInfo) {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	m.updateFileLocked(dev, f, m.removeFileLocked)
-}
-
-// removeNeeded removes a file from the needed counts
-func (m *metadataTracker) removeNeeded(dev protocol.DeviceID, f protocol.FileInfo) {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	m.dirty = true
-
-	m.removeFileLocked(dev, needFlag, f)
-}
-
-func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flag uint32, f protocol.FileInfo) {
-	cp := m.countsPtr(dev, flag)
-
-	switch {
-	case f.IsDeleted():
-		cp.Deleted--
-	case f.IsDirectory() && !f.IsSymlink():
-		cp.Directories--
-	case f.IsSymlink():
-		cp.Symlinks--
-	default:
-		cp.Files--
-	}
-	cp.Bytes -= f.FileSize()
-
-	// If we've run into an impossible situation, correct it for now and set
-	// the created timestamp to zero. Next time we start up the metadata
-	// will be seen as infinitely old and recalculated from scratch.
-	if cp.Deleted < 0 {
-		m.evLogger.Log(events.Failure, fmt.Sprintf("meta deleted count for flag 0x%x dropped below zero", flag))
-		cp.Deleted = 0
-		m.counts.Created = 0
-	}
-	if cp.Files < 0 {
-		m.evLogger.Log(events.Failure, fmt.Sprintf("meta files count for flag 0x%x dropped below zero", flag))
-		cp.Files = 0
-		m.counts.Created = 0
-	}
-	if cp.Directories < 0 {
-		m.evLogger.Log(events.Failure, fmt.Sprintf("meta directories count for flag 0x%x dropped below zero", flag))
-		cp.Directories = 0
-		m.counts.Created = 0
-	}
-	if cp.Symlinks < 0 {
-		m.evLogger.Log(events.Failure, fmt.Sprintf("meta deleted count for flag 0x%x dropped below zero", flag))
-		cp.Symlinks = 0
-		m.counts.Created = 0
-	}
-}
-
-// resetAll resets all metadata for the given device
-func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
-	m.mut.Lock()
-	m.dirty = true
-	for i, c := range m.counts.Counts {
-		if c.DeviceID == dev {
-			if c.LocalFlags != needFlag {
-				m.counts.Counts[i] = Counts{
-					DeviceID:   c.DeviceID,
-					LocalFlags: c.LocalFlags,
-				}
-			} else {
-				m.counts.Counts[i] = m.allNeededCounts(dev)
-			}
-		}
-	}
-	m.mut.Unlock()
-}
-
-// resetCounts resets the file, dir, etc. counters, while retaining the
-// sequence number
-func (m *metadataTracker) resetCounts(dev protocol.DeviceID) {
-	m.mut.Lock()
-	m.dirty = true
-
-	for i, c := range m.counts.Counts {
-		if c.DeviceID == dev {
-			m.counts.Counts[i] = Counts{
-				DeviceID:   c.DeviceID,
-				Sequence:   c.Sequence,
-				LocalFlags: c.LocalFlags,
-			}
-		}
-	}
-
-	m.mut.Unlock()
-}
-
-func (m *countsMap) Counts(dev protocol.DeviceID, flag uint32) Counts {
-	if bits.OnesCount32(flag) > 1 {
-		panic("incorrect usage: set at most one bit in flag")
-	}
-
-	idx, ok := m.indexes[metaKey{dev, flag}]
-	if !ok {
-		if flag == needFlag {
-			// If there's nothing about a device in the index yet,
-			// it needs everything.
-			return m.allNeededCounts(dev)
-		}
-		return Counts{}
-	}
-
-	return m.counts.Counts[idx]
-}
-
-// Snapshot returns a copy of the metadata for reading.
-func (m *metadataTracker) Snapshot() *countsMap {
-	m.mut.RLock()
-	defer m.mut.RUnlock()
-
-	c := &countsMap{
-		counts: CountsSet{
-			Counts:  make([]Counts, len(m.counts.Counts)),
-			Created: m.counts.Created,
-		},
-		indexes: make(map[metaKey]int, len(m.indexes)),
-	}
-	for k, v := range m.indexes {
-		c.indexes[k] = v
-	}
-	copy(c.counts.Counts, m.counts.Counts)
-
-	return c
-}
-
-// nextLocalSeq allocates a new local sequence number
-func (m *metadataTracker) nextLocalSeq() int64 {
-	m.mut.Lock()
-	defer m.mut.Unlock()
-
-	c := m.countsPtr(protocol.LocalDeviceID, 0)
-	c.Sequence++
-	return c.Sequence
-}
-
-// devices returns the list of devices tracked, excluding the local device
-// (which we don't know the ID of)
-func (m *metadataTracker) devices() []protocol.DeviceID {
-	m.mut.RLock()
-	defer m.mut.RUnlock()
-	return m.countsMap.devices()
-}
-
-func (m *countsMap) devices() []protocol.DeviceID {
-	devs := make([]protocol.DeviceID, 0, len(m.counts.Counts))
-
-	for _, dev := range m.counts.Counts {
-		if dev.Sequence > 0 {
-			if dev.DeviceID == protocol.GlobalDeviceID || dev.DeviceID == protocol.LocalDeviceID {
-				continue
-			}
-			devs = append(devs, dev.DeviceID)
-		}
-	}
-
-	return devs
-}
-
-func (m *metadataTracker) Created() time.Time {
-	m.mut.RLock()
-	defer m.mut.RUnlock()
-	return time.Unix(0, m.counts.Created)
-}
-
-func (m *metadataTracker) SetCreated() {
-	m.mut.Lock()
-	m.counts.Created = time.Now().UnixNano()
-	m.dirty = true
-	m.mut.Unlock()
-}
-
-// eachFlagBit calls the function once for every bit that is set in flags
-func eachFlagBit(flags uint32, fn func(flag uint32)) {
-	// Test each bit from the right, as long as there are bits left in the
-	// flag set. Clear any bits found and stop testing as soon as there are
-	// no more bits set.
-
-	currentBit := uint32(1 << 0)
-	for flags != 0 {
-		if flags&currentBit != 0 {
-			fn(currentBit)
-			flags &^= currentBit
-		}
-		currentBit <<= 1
-	}
-}
diff --git a/lib/db/namespaced.go b/lib/db/namespaced.go
deleted file mode 100644
index ec7f3298c81..00000000000
--- a/lib/db/namespaced.go
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"encoding/binary"
-	"time"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
-)
-
-// NamespacedKV is a simple key-value store using a specific namespace within
-// a leveldb.
-type NamespacedKV struct {
-	db     backend.Backend
-	prefix string
-}
-
-// NewNamespacedKV returns a new NamespacedKV that lives in the namespace
-// specified by the prefix.
-func NewNamespacedKV(db backend.Backend, prefix string) *NamespacedKV {
-	return &NamespacedKV{
-		db:     db,
-		prefix: prefix,
-	}
-}
-
-// PutInt64 stores a new int64. Any existing value (even if of another type)
-// is overwritten.
-func (n *NamespacedKV) PutInt64(key string, val int64) error {
-	var valBs [8]byte
-	binary.BigEndian.PutUint64(valBs[:], uint64(val))
-	return n.db.Put(n.prefixedKey(key), valBs[:])
-}
-
-// Int64 returns the stored value interpreted as an int64 and a boolean that
-// is false if no value was stored at the key.
-func (n *NamespacedKV) Int64(key string) (int64, bool, error) {
-	valBs, err := n.db.Get(n.prefixedKey(key))
-	if err != nil {
-		return 0, false, filterNotFound(err)
-	}
-	val := binary.BigEndian.Uint64(valBs)
-	return int64(val), true, nil
-}
-
-// PutTime stores a new time.Time. Any existing value (even if of another
-// type) is overwritten.
-func (n *NamespacedKV) PutTime(key string, val time.Time) error {
-	valBs, _ := val.MarshalBinary() // never returns an error
-	return n.db.Put(n.prefixedKey(key), valBs)
-}
-
-// Time returns the stored value interpreted as a time.Time and a boolean
-// that is false if no value was stored at the key.
-func (n NamespacedKV) Time(key string) (time.Time, bool, error) {
-	var t time.Time
-	valBs, err := n.db.Get(n.prefixedKey(key))
-	if err != nil {
-		return t, false, filterNotFound(err)
-	}
-	err = t.UnmarshalBinary(valBs)
-	return t, err == nil, err
-}
-
-// PutString stores a new string. Any existing value (even if of another type)
-// is overwritten.
-func (n *NamespacedKV) PutString(key, val string) error {
-	return n.db.Put(n.prefixedKey(key), []byte(val))
-}
-
-// String returns the stored value interpreted as a string and a boolean that
-// is false if no value was stored at the key.
-func (n NamespacedKV) String(key string) (string, bool, error) {
-	valBs, err := n.db.Get(n.prefixedKey(key))
-	if err != nil {
-		return "", false, filterNotFound(err)
-	}
-	return string(valBs), true, nil
-}
-
-// PutBytes stores a new byte slice. Any existing value (even if of another type)
-// is overwritten.
-func (n *NamespacedKV) PutBytes(key string, val []byte) error {
-	return n.db.Put(n.prefixedKey(key), val)
-}
-
-// Bytes returns the stored value as a raw byte slice and a boolean that
-// is false if no value was stored at the key.
-func (n NamespacedKV) Bytes(key string) ([]byte, bool, error) {
-	valBs, err := n.db.Get(n.prefixedKey(key))
-	if err != nil {
-		return nil, false, filterNotFound(err)
-	}
-	return valBs, true, nil
-}
-
-// PutBool stores a new boolean. Any existing value (even if of another type)
-// is overwritten.
-func (n *NamespacedKV) PutBool(key string, val bool) error {
-	if val {
-		return n.db.Put(n.prefixedKey(key), []byte{0x0})
-	}
-	return n.db.Put(n.prefixedKey(key), []byte{0x1})
-}
-
-// Bool returns the stored value as a boolean and a boolean that
-// is false if no value was stored at the key.
-func (n NamespacedKV) Bool(key string) (bool, bool, error) {
-	valBs, err := n.db.Get(n.prefixedKey(key))
-	if err != nil {
-		return false, false, filterNotFound(err)
-	}
-	return valBs[0] == 0x0, true, nil
-}
-
-// Delete deletes the specified key. It is allowed to delete a nonexistent
-// key.
-func (n NamespacedKV) Delete(key string) error {
-	return n.db.Delete(n.prefixedKey(key))
-}
-
-func (n NamespacedKV) prefixedKey(key string) []byte {
-	return []byte(n.prefix + key)
-}
-
-// Well known namespaces that can be instantiated without knowing the key
-// details.
-
-// NewDeviceStatisticsNamespace creates a KV namespace for device statistics
-// for the given device.
-func NewDeviceStatisticsNamespace(db backend.Backend, device string) *NamespacedKV {
-	return NewNamespacedKV(db, string(KeyTypeDeviceStatistic)+device)
-}
-
-// NewFolderStatisticsNamespace creates a KV namespace for folder statistics
-// for the given folder.
-func NewFolderStatisticsNamespace(db backend.Backend, folder string) *NamespacedKV {
-	return NewNamespacedKV(db, string(KeyTypeFolderStatistic)+folder)
-}
-
-// NewMiscDataNamespace creates a KV namespace for miscellaneous metadata.
-func NewMiscDataNamespace(db backend.Backend) *NamespacedKV {
-	return NewNamespacedKV(db, string(KeyTypeMiscData))
-}
-
-func filterNotFound(err error) error {
-	if backend.IsNotFound(err) {
-		return nil
-	}
-	return err
-}
diff --git a/lib/db/schemaupdater.go b/lib/db/schemaupdater.go
deleted file mode 100644
index 3a9c4abb288..00000000000
--- a/lib/db/schemaupdater.go
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"fmt"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/bep"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-// dbMigrationVersion is for migrations that do not change the schema and thus
-// do not put restrictions on downgrades (e.g. for repairs after a bugfix).
-const (
-	dbVersion             = 14
-	dbMigrationVersion    = 20
-	dbMinSyncthingVersion = "v1.9.0"
-)
-
-type migration struct {
-	schemaVersion       int64
-	migrationVersion    int64
-	minSyncthingVersion string
-	migration           func(prevSchema int) error
-}
-
-type databaseDowngradeError struct {
-	minSyncthingVersion string
-}
-
-func (e *databaseDowngradeError) Error() string {
-	if e.minSyncthingVersion == "" {
-		return "newer Syncthing required"
-	}
-	return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion)
-}
-
-// UpdateSchema updates a possibly outdated database to the current schema and
-// also does repairs where necessary.
-func UpdateSchema(db *Lowlevel) error {
-	updater := &schemaUpdater{db}
-	return updater.updateSchema()
-}
-
-type schemaUpdater struct {
-	*Lowlevel
-}
-
-func (db *schemaUpdater) updateSchema() error {
-	// Updating the schema can touch any and all parts of the database. Make
-	// sure we do not run GC concurrently with schema migrations.
-	db.gcMut.Lock()
-	defer db.gcMut.Unlock()
-
-	miscDB := NewMiscDataNamespace(db.Lowlevel)
-	prevVersion, _, err := miscDB.Int64("dbVersion")
-	if err != nil {
-		return err
-	}
-
-	if prevVersion > 0 && prevVersion < 14 {
-		// This is a database version that is too old to be upgraded directly.
-		// The user will have to upgrade to an older version first.
-		return fmt.Errorf("database version %d is too old to be upgraded directly; step via Syncthing v1.27.0 to upgrade", prevVersion)
-	}
-
-	if prevVersion > dbVersion {
-		err := &databaseDowngradeError{}
-		if minSyncthingVersion, ok, dbErr := miscDB.String("dbMinSyncthingVersion"); dbErr != nil {
-			return dbErr
-		} else if ok {
-			err.minSyncthingVersion = minSyncthingVersion
-		}
-		return err
-	}
-
-	prevMigration, _, err := miscDB.Int64("dbMigrationVersion")
-	if err != nil {
-		return err
-	}
-	// Cover versions before adding `dbMigrationVersion` (== 0) and possible future weirdness.
-	if prevMigration < prevVersion {
-		prevMigration = prevVersion
-	}
-
-	if prevVersion == dbVersion && prevMigration >= dbMigrationVersion {
-		return nil
-	}
-
-	migrations := []migration{
-		{14, 14, "v1.9.0", db.updateSchemaTo14},
-		{14, 16, "v1.9.0", db.checkRepairMigration},
-		{14, 17, "v1.9.0", db.migration17},
-		{14, 19, "v1.9.0", db.dropAllIndexIDsMigration},
-		{14, 20, "v1.9.0", db.dropOutgoingIndexIDsMigration},
-	}
-
-	for _, m := range migrations {
-		if prevMigration < m.migrationVersion {
-			l.Infof("Running database migration %d...", m.migrationVersion)
-			if err := m.migration(int(prevVersion)); err != nil {
-				return fmt.Errorf("failed to do migration %v: %w", m.migrationVersion, err)
-			}
-			if err := db.writeVersions(m, miscDB); err != nil {
-				return fmt.Errorf("failed to write versions after migration %v: %w", m.migrationVersion, err)
-			}
-		}
-	}
-
-	if err := db.writeVersions(migration{
-		schemaVersion:       dbVersion,
-		migrationVersion:    dbMigrationVersion,
-		minSyncthingVersion: dbMinSyncthingVersion,
-	}, miscDB); err != nil {
-		return fmt.Errorf("failed to write versions after migrations: %w", err)
-	}
-
-	l.Infoln("Compacting database after migration...")
-	return db.Compact()
-}
-
-func (*schemaUpdater) writeVersions(m migration, miscDB *NamespacedKV) error {
-	if err := miscDB.PutInt64("dbVersion", m.schemaVersion); err != nil {
-		return err
-	}
-	if err := miscDB.PutString("dbMinSyncthingVersion", m.minSyncthingVersion); err != nil {
-		return err
-	}
-	if err := miscDB.PutInt64("dbMigrationVersion", m.migrationVersion); err != nil {
-		return err
-	}
-	return nil
-}
-
-func (db *schemaUpdater) updateSchemaTo14(_ int) error {
-	// Checks for missing blocks and marks those entries as requiring a
-	// rehash/being invalid. The db is checked/repaired afterwards, i.e.
-	// no care is taken to get metadata and sequences right.
-	// If the corresponding files changed on disk compared to the global
-	// version, this will cause a conflict.
-
-	var key, gk []byte
-	for _, folderStr := range db.ListFolders() {
-		folder := []byte(folderStr)
-		meta := newMetadataTracker(db.keyer, db.evLogger)
-		meta.counts.Created = 0 // Recalculate metadata afterwards
-
-		t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
-		if err != nil {
-			return err
-		}
-		defer t.close()
-
-		key, err = t.keyer.GenerateDeviceFileKey(key, folder, protocol.LocalDeviceID[:], nil)
-		if err != nil {
-			return err
-		}
-		it, err := t.NewPrefixIterator(key)
-		if err != nil {
-			return err
-		}
-		defer it.Release()
-		for it.Next() {
-			var bepf bep.FileInfo
-			if err := proto.Unmarshal(it.Value(), &bepf); err != nil {
-				return err
-			}
-			fi := protocol.FileInfoFromDB(&bepf)
-			if len(fi.Blocks) > 0 || len(fi.BlocksHash) == 0 {
-				continue
-			}
-			key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
-			_, err := t.Get(key)
-			if err == nil {
-				continue
-			}
-
-			fi.SetMustRescan()
-			if err = t.putFile(it.Key(), fi); err != nil {
-				return err
-			}
-
-			gk, err = t.keyer.GenerateGlobalVersionKey(gk, folder, []byte(fi.Name))
-			if err != nil {
-				return err
-			}
-			key, err = t.updateGlobal(gk, key, folder, protocol.LocalDeviceID[:], fi, meta)
-			if err != nil {
-				return err
-			}
-		}
-		it.Release()
-
-		if err = t.Commit(); err != nil {
-			return err
-		}
-		t.close()
-	}
-
-	return nil
-}
-
-func (db *schemaUpdater) checkRepairMigration(_ int) error {
-	for _, folder := range db.ListFolders() {
-		_, err := db.getMetaAndCheckGCLocked(folder)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// migration17 finds all files that were pulled as invalid from an invalid
-// global and make sure they get scanned/pulled again.
-func (db *schemaUpdater) migration17(prev int) error {
-	if prev < 16 {
-		// Issue was introduced in migration to 16
-		return nil
-	}
-	t, err := db.newReadOnlyTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	for _, folderStr := range db.ListFolders() {
-		folder := []byte(folderStr)
-		meta, err := db.loadMetadataTracker(folderStr)
-		if err != nil {
-			return err
-		}
-		batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
-			return db.updateLocalFiles(folder, fs, meta)
-		})
-		var innerErr error
-		err = t.withHave(folder, protocol.LocalDeviceID[:], nil, false, func(fi protocol.FileInfo) bool {
-			if fi.IsInvalid() && fi.FileLocalFlags() == 0 {
-				fi.SetMustRescan()
-				fi.Version = protocol.Vector{}
-				batch.Append(fi)
-				innerErr = batch.FlushIfFull()
-				return innerErr == nil
-			}
-			return true
-		})
-		if innerErr != nil {
-			return innerErr
-		}
-		if err != nil {
-			return err
-		}
-		if err := batch.Flush(); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func (db *schemaUpdater) dropAllIndexIDsMigration(_ int) error {
-	return db.dropIndexIDs()
-}
-
-func (db *schemaUpdater) dropOutgoingIndexIDsMigration(_ int) error {
-	return db.dropOtherDeviceIndexIDs()
-}
diff --git a/lib/db/set.go b/lib/db/set.go
deleted file mode 100644
index 06e91e14b63..00000000000
--- a/lib/db/set.go
+++ /dev/null
@@ -1,553 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-// Package db provides a set type to track local/remote files with newness
-// checks. We must do a certain amount of normalization in here. We will get
-// fed paths with either native or wire-format separators and encodings
-// depending on who calls us. We transform paths to wire-format (NFC and
-// slashes) on the way to the database, and transform to native format
-// (varying separator and encoding) on the way back out.
-package db
-
-import (
-	"bytes"
-	"fmt"
-
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/fs"
-	"github.com/syncthing/syncthing/lib/osutil"
-	"github.com/syncthing/syncthing/lib/protocol"
-	"github.com/syncthing/syncthing/lib/sync"
-)
-
-type FileSet struct {
-	folder string
-	db     *Lowlevel
-	meta   *metadataTracker
-
-	updateMutex sync.Mutex // protects database updates and the corresponding metadata changes
-}
-
-// The Iterator is called with either a protocol.FileInfo or a
-// FileInfoTruncated (depending on the method) and returns true to
-// continue iteration, false to stop.
-type Iterator func(f protocol.FileInfo) bool
-
-func NewFileSet(folder string, db *Lowlevel) (*FileSet, error) {
-	select {
-	case <-db.oneFileSetCreated:
-	default:
-		close(db.oneFileSetCreated)
-	}
-	meta, err := db.loadMetadataTracker(folder)
-	if err != nil {
-		db.handleFailure(err)
-		return nil, err
-	}
-	s := &FileSet{
-		folder:      folder,
-		db:          db,
-		meta:        meta,
-		updateMutex: sync.NewMutex(),
-	}
-	if id := s.IndexID(protocol.LocalDeviceID); id == 0 {
-		// No index ID set yet. We create one now.
-		id = protocol.NewIndexID()
-		err := s.db.setIndexID(protocol.LocalDeviceID[:], []byte(s.folder), id)
-		if err != nil && !backend.IsClosed(err) {
-			fatalError(err, fmt.Sprintf("%s Creating new IndexID", s.folder), s.db)
-		}
-	}
-	return s, nil
-}
-
-func (s *FileSet) Drop(device protocol.DeviceID) {
-	opStr := fmt.Sprintf("%s Drop(%v)", s.folder, device)
-	l.Debugf(opStr)
-
-	s.updateMutex.Lock()
-	defer s.updateMutex.Unlock()
-
-	if err := s.db.dropDeviceFolder(device[:], []byte(s.folder), s.meta); backend.IsClosed(err) {
-		return
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-
-	if device == protocol.LocalDeviceID {
-		s.meta.resetCounts(device)
-		// We deliberately do not reset the sequence number here. Dropping
-		// all files for the local device ID only happens in testing - which
-		// expects the sequence to be retained, like an old Replace() of all
-		// files would do. However, if we ever did it "in production" we
-		// would anyway want to retain the sequence for delta indexes to be
-		// happy.
-	} else {
-		// Here, on the other hand, we want to make sure that any file
-		// announced from the remote is newer than our current sequence
-		// number.
-		s.meta.resetAll(device)
-	}
-
-	t, err := s.db.newReadWriteTransaction()
-	if backend.IsClosed(err) {
-		return
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-	defer t.close()
-
-	if err := s.meta.toDB(t, []byte(s.folder)); backend.IsClosed(err) {
-		return
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-	if err := t.Commit(); backend.IsClosed(err) {
-		return
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-}
-
-func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
-	opStr := fmt.Sprintf("%s Update(%v, [%d])", s.folder, device, len(fs))
-	l.Debugf(opStr)
-
-	// do not modify fs in place, it is still used in outer scope
-	fs = append([]protocol.FileInfo(nil), fs...)
-
-	// If one file info is present multiple times, only keep the last.
-	// Updating the same file multiple times is problematic, because the
-	// previous updates won't yet be represented in the db when we update it
-	// again. Additionally even if that problem was taken care of, it would
-	// be pointless because we remove the previously added file info again
-	// right away.
-	fs = normalizeFilenamesAndDropDuplicates(fs)
-
-	s.updateMutex.Lock()
-	defer s.updateMutex.Unlock()
-
-	if device == protocol.LocalDeviceID {
-		// For the local device we have a bunch of metadata to track.
-		if err := s.db.updateLocalFiles([]byte(s.folder), fs, s.meta); err != nil && !backend.IsClosed(err) {
-			fatalError(err, opStr, s.db)
-		}
-		return
-	}
-	// Easy case, just update the files and we're done.
-	if err := s.db.updateRemoteFiles([]byte(s.folder), device[:], fs, s.meta); err != nil && !backend.IsClosed(err) {
-		fatalError(err, opStr, s.db)
-	}
-}
-
-func (s *FileSet) RemoveLocalItems(items []string) {
-	opStr := fmt.Sprintf("%s RemoveLocalItems([%d])", s.folder, len(items))
-	l.Debugf(opStr)
-
-	s.updateMutex.Lock()
-	defer s.updateMutex.Unlock()
-
-	for i := range items {
-		items[i] = osutil.NormalizedFilename(items[i])
-	}
-
-	if err := s.db.removeLocalFiles([]byte(s.folder), items, s.meta); err != nil && !backend.IsClosed(err) {
-		fatalError(err, opStr, s.db)
-	}
-}
-
-type Snapshot struct {
-	folder     string
-	t          readOnlyTransaction
-	meta       *countsMap
-	fatalError func(error, string)
-}
-
-func (s *FileSet) Snapshot() (*Snapshot, error) {
-	opStr := fmt.Sprintf("%s Snapshot()", s.folder)
-	l.Debugf(opStr)
-
-	s.updateMutex.Lock()
-	defer s.updateMutex.Unlock()
-
-	t, err := s.db.newReadOnlyTransaction()
-	if err != nil {
-		s.db.handleFailure(err)
-		return nil, err
-	}
-	return &Snapshot{
-		folder: s.folder,
-		t:      t,
-		meta:   s.meta.Snapshot(),
-		fatalError: func(err error, opStr string) {
-			fatalError(err, opStr, s.db)
-		},
-	}, nil
-}
-
-func (s *Snapshot) Release() {
-	s.t.close()
-}
-
-func (s *Snapshot) WithNeed(device protocol.DeviceID, fn Iterator) {
-	opStr := fmt.Sprintf("%s WithNeed(%v)", s.folder, device)
-	l.Debugf(opStr)
-	if err := s.t.withNeed([]byte(s.folder), device[:], false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
-	opStr := fmt.Sprintf("%s WithNeedTruncated(%v)", s.folder, device)
-	l.Debugf(opStr)
-	if err := s.t.withNeed([]byte(s.folder), device[:], true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithHave(device protocol.DeviceID, fn Iterator) {
-	opStr := fmt.Sprintf("%s WithHave(%v)", s.folder, device)
-	l.Debugf(opStr)
-	if err := s.t.withHave([]byte(s.folder), device[:], nil, false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithHaveTruncated(device protocol.DeviceID, fn Iterator) {
-	opStr := fmt.Sprintf("%s WithHaveTruncated(%v)", s.folder, device)
-	l.Debugf(opStr)
-	if err := s.t.withHave([]byte(s.folder), device[:], nil, true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithHaveSequence(startSeq int64, fn Iterator) {
-	opStr := fmt.Sprintf("%s WithHaveSequence(%v)", s.folder, startSeq)
-	l.Debugf(opStr)
-	if err := s.t.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-// Except for an item with a path equal to prefix, only children of prefix are iterated.
-// E.g. for prefix "dir", "dir/file" is iterated, but "dir.file" is not.
-func (s *Snapshot) WithPrefixedHaveTruncated(device protocol.DeviceID, prefix string, fn Iterator) {
-	opStr := fmt.Sprintf(`%s WithPrefixedHaveTruncated(%v, "%v")`, s.folder, device, prefix)
-	l.Debugf(opStr)
-	if err := s.t.withHave([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithGlobal(fn Iterator) {
-	opStr := fmt.Sprintf("%s WithGlobal()", s.folder)
-	l.Debugf(opStr)
-	if err := s.t.withGlobal([]byte(s.folder), nil, false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) WithGlobalTruncated(fn Iterator) {
-	opStr := fmt.Sprintf("%s WithGlobalTruncated()", s.folder)
-	l.Debugf(opStr)
-	if err := s.t.withGlobal([]byte(s.folder), nil, true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-// Except for an item with a path equal to prefix, only children of prefix are iterated.
-// E.g. for prefix "dir", "dir/file" is iterated, but "dir.file" is not.
-func (s *Snapshot) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
-	opStr := fmt.Sprintf(`%s WithPrefixedGlobalTruncated("%v")`, s.folder, prefix)
-	l.Debugf(opStr)
-	if err := s.t.withGlobal([]byte(s.folder), []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *Snapshot) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
-	opStr := fmt.Sprintf("%s Get(%v)", s.folder, file)
-	l.Debugf(opStr)
-	f, ok, err := s.t.getFile([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
-	if backend.IsClosed(err) {
-		return protocol.FileInfo{}, false
-	} else if err != nil {
-		s.fatalError(err, opStr)
-	}
-	f.Name = osutil.NativeFilename(f.Name)
-	return f, ok
-}
-
-func (s *Snapshot) GetGlobal(file string) (protocol.FileInfo, bool) {
-	opStr := fmt.Sprintf("%s GetGlobal(%v)", s.folder, file)
-	l.Debugf(opStr)
-	_, fi, ok, err := s.t.getGlobal(nil, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), false)
-	if backend.IsClosed(err) {
-		return protocol.FileInfo{}, false
-	} else if err != nil {
-		s.fatalError(err, opStr)
-	}
-	if !ok {
-		return protocol.FileInfo{}, false
-	}
-	fi.Name = osutil.NativeFilename(fi.Name)
-	return fi, true
-}
-
-func (s *Snapshot) GetGlobalTruncated(file string) (protocol.FileInfo, bool) {
-	opStr := fmt.Sprintf("%s GetGlobalTruncated(%v)", s.folder, file)
-	l.Debugf(opStr)
-	_, fi, ok, err := s.t.getGlobal(nil, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), true)
-	if backend.IsClosed(err) {
-		return protocol.FileInfo{}, false
-	} else if err != nil {
-		s.fatalError(err, opStr)
-	}
-	if !ok {
-		return protocol.FileInfo{}, false
-	}
-	fi.Name = osutil.NativeFilename(fi.Name)
-	return fi, true
-}
-
-func (s *Snapshot) Availability(file string) []protocol.DeviceID {
-	opStr := fmt.Sprintf("%s Availability(%v)", s.folder, file)
-	l.Debugf(opStr)
-	av, err := s.t.availability([]byte(s.folder), []byte(osutil.NormalizedFilename(file)))
-	if backend.IsClosed(err) {
-		return nil
-	} else if err != nil {
-		s.fatalError(err, opStr)
-	}
-	return av
-}
-
-func (s *Snapshot) DebugGlobalVersions(file string) *DebugVersionList {
-	opStr := fmt.Sprintf("%s DebugGlobalVersions(%v)", s.folder, file)
-	l.Debugf(opStr)
-	vl, err := s.t.getGlobalVersions(nil, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
-	if backend.IsClosed(err) || backend.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		s.fatalError(err, opStr)
-	}
-	return &DebugVersionList{vl}
-}
-
-func (s *Snapshot) Sequence(device protocol.DeviceID) int64 {
-	return s.meta.Counts(device, 0).Sequence
-}
-
-// RemoteSequences returns a map of the sequence numbers seen for each
-// remote device sharing this folder.
-func (s *Snapshot) RemoteSequences() map[protocol.DeviceID]int64 {
-	res := make(map[protocol.DeviceID]int64)
-	for _, device := range s.meta.devices() {
-		switch device {
-		case protocol.EmptyDeviceID, protocol.LocalDeviceID, protocol.GlobalDeviceID:
-			continue
-		default:
-			if seq := s.Sequence(device); seq > 0 {
-				res[device] = seq
-			}
-		}
-	}
-
-	return res
-}
-
-func (s *Snapshot) LocalSize() Counts {
-	local := s.meta.Counts(protocol.LocalDeviceID, 0)
-	return local.Add(s.ReceiveOnlyChangedSize())
-}
-
-func (s *Snapshot) ReceiveOnlyChangedSize() Counts {
-	return s.meta.Counts(protocol.LocalDeviceID, protocol.FlagLocalReceiveOnly)
-}
-
-func (s *Snapshot) GlobalSize() Counts {
-	return s.meta.Counts(protocol.GlobalDeviceID, 0)
-}
-
-func (s *Snapshot) NeedSize(device protocol.DeviceID) Counts {
-	return s.meta.Counts(device, needFlag)
-}
-
-func (s *Snapshot) WithBlocksHash(hash []byte, fn Iterator) {
-	opStr := fmt.Sprintf(`%s WithBlocksHash("%x")`, s.folder, hash)
-	l.Debugf(opStr)
-	if err := s.t.withBlocksHash([]byte(s.folder), hash, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
-		s.fatalError(err, opStr)
-	}
-}
-
-func (s *FileSet) Sequence(device protocol.DeviceID) int64 {
-	return s.meta.Sequence(device)
-}
-
-func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
-	opStr := fmt.Sprintf("%s IndexID(%v)", s.folder, device)
-	l.Debugf(opStr)
-	id, err := s.db.getIndexID(device[:], []byte(s.folder))
-	if backend.IsClosed(err) {
-		return 0
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-	return id
-}
-
-func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
-	if device == protocol.LocalDeviceID {
-		panic("do not explicitly set index ID for local device")
-	}
-	opStr := fmt.Sprintf("%s SetIndexID(%v, %v)", s.folder, device, id)
-	l.Debugf(opStr)
-	if err := s.db.setIndexID(device[:], []byte(s.folder), id); err != nil && !backend.IsClosed(err) {
-		fatalError(err, opStr, s.db)
-	}
-}
-
-func (s *FileSet) MtimeOption() fs.Option {
-	opStr := fmt.Sprintf("%s MtimeOption()", s.folder)
-	l.Debugf(opStr)
-	prefix, err := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
-	if backend.IsClosed(err) {
-		return nil
-	} else if err != nil {
-		fatalError(err, opStr, s.db)
-	}
-	kv := NewNamespacedKV(s.db, string(prefix))
-	return fs.NewMtimeOption(kv)
-}
-
-func (s *FileSet) ListDevices() []protocol.DeviceID {
-	return s.meta.devices()
-}
-
-func (s *FileSet) RepairSequence() (int, error) {
-	s.updateAndGCMutexLock() // Ensures consistent locking order
-	defer s.updateMutex.Unlock()
-	defer s.db.gcMut.RUnlock()
-	return s.db.repairSequenceGCLocked(s.folder, s.meta)
-}
-
-func (s *FileSet) updateAndGCMutexLock() {
-	s.updateMutex.Lock()
-	s.db.gcMut.RLock()
-}
-
-// DropFolder clears out all information related to the given folder from the
-// database.
-func DropFolder(db *Lowlevel, folder string) {
-	opStr := fmt.Sprintf("DropFolder(%v)", folder)
-	l.Debugf(opStr)
-	droppers := []func([]byte) error{
-		db.dropFolder,
-		db.dropMtimes,
-		db.dropFolderMeta,
-		db.dropFolderIndexIDs,
-		db.folderIdx.Delete,
-	}
-	for _, drop := range droppers {
-		if err := drop([]byte(folder)); backend.IsClosed(err) {
-			return
-		} else if err != nil {
-			fatalError(err, opStr, db)
-		}
-	}
-}
-
-// DropDeltaIndexIDs removes all delta index IDs from the database.
-// This will cause a full index transmission on the next connection.
-// Must be called before using FileSets, i.e. before NewFileSet is called for
-// the first time.
-func DropDeltaIndexIDs(db *Lowlevel) {
-	select {
-	case <-db.oneFileSetCreated:
-		panic("DropDeltaIndexIDs must not be called after NewFileSet for the same Lowlevel")
-	default:
-	}
-	opStr := "DropDeltaIndexIDs"
-	l.Debugf(opStr)
-	err := db.dropIndexIDs()
-	if backend.IsClosed(err) {
-		return
-	} else if err != nil {
-		fatalError(err, opStr, db)
-	}
-}
-
-func normalizeFilenamesAndDropDuplicates(fs []protocol.FileInfo) []protocol.FileInfo {
-	positions := make(map[string]int, len(fs))
-	for i, f := range fs {
-		norm := osutil.NormalizedFilename(f.Name)
-		if pos, ok := positions[norm]; ok {
-			fs[pos] = protocol.FileInfo{}
-		}
-		positions[norm] = i
-		fs[i].Name = norm
-	}
-	for i := 0; i < len(fs); {
-		if fs[i].Name == "" {
-			fs = append(fs[:i], fs[i+1:]...)
-			continue
-		}
-		i++
-	}
-	return fs
-}
-
-func nativeFileIterator(fn Iterator) Iterator {
-	return func(fi protocol.FileInfo) bool {
-		fi.Name = osutil.NativeFilename(fi.Name)
-		return fn(fi)
-	}
-}
-
-func fatalError(err error, opStr string, db *Lowlevel) {
-	db.checkErrorForRepair(err)
-	l.Warnf("Fatal error: %v: %v", opStr, err)
-	panic(ldbPathRe.ReplaceAllString(err.Error(), "$1 x: "))
-}
-
-// DebugFileVersion is the database-internal representation of a file
-// version, with a nicer string representation, used only by API debug
-// methods.
-type DebugVersionList struct {
-	*dbproto.VersionList
-}
-
-func (vl DebugVersionList) String() string {
-	var b bytes.Buffer
-	var id protocol.DeviceID
-	b.WriteString("[")
-	for i, v := range vl.Versions {
-		if i > 0 {
-			b.WriteString(", ")
-		}
-		fmt.Fprintf(&b, "{Version:%v, Deleted:%v, Devices:[", protocol.VectorFromWire(v.Version), v.Deleted)
-		for j, dev := range v.Devices {
-			if j > 0 {
-				b.WriteString(", ")
-			}
-			copy(id[:], dev)
-			fmt.Fprint(&b, id.Short())
-		}
-		b.WriteString("], Invalid:[")
-		for j, dev := range v.InvalidDevices {
-			if j > 0 {
-				b.WriteString(", ")
-			}
-			copy(id[:], dev)
-			fmt.Fprint(&b, id.Short())
-		}
-		fmt.Fprint(&b, "]}")
-	}
-	b.WriteString("]")
-	return b.String()
-}
diff --git a/lib/db/structs.go b/lib/db/structs.go
deleted file mode 100644
index 69d602112cf..00000000000
--- a/lib/db/structs.go
+++ /dev/null
@@ -1,363 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"bytes"
-	"fmt"
-	"strings"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-type CountsSet struct {
-	Counts  []Counts
-	Created int64 // unix nanos
-}
-
-type Counts struct {
-	Files       int
-	Directories int
-	Symlinks    int
-	Deleted     int
-	Bytes       int64
-	Sequence    int64             // zero for the global state
-	DeviceID    protocol.DeviceID // device ID for remote devices, or special values for local/global
-	LocalFlags  uint32            // the local flag for this count bucket
-}
-
-func (c Counts) toWire() *dbproto.Counts {
-	return &dbproto.Counts{
-		Files:       int32(c.Files),
-		Directories: int32(c.Directories),
-		Symlinks:    int32(c.Symlinks),
-		Deleted:     int32(c.Deleted),
-		Bytes:       c.Bytes,
-		Sequence:    c.Sequence,
-		DeviceId:    c.DeviceID[:],
-		LocalFlags:  c.LocalFlags,
-	}
-}
-
-func countsFromWire(w *dbproto.Counts) Counts {
-	return Counts{
-		Files:       int(w.Files),
-		Directories: int(w.Directories),
-		Symlinks:    int(w.Symlinks),
-		Deleted:     int(w.Deleted),
-		Bytes:       w.Bytes,
-		Sequence:    w.Sequence,
-		DeviceID:    protocol.DeviceID(w.DeviceId),
-		LocalFlags:  w.LocalFlags,
-	}
-}
-
-func (c Counts) Add(other Counts) Counts {
-	return Counts{
-		Files:       c.Files + other.Files,
-		Directories: c.Directories + other.Directories,
-		Symlinks:    c.Symlinks + other.Symlinks,
-		Deleted:     c.Deleted + other.Deleted,
-		Bytes:       c.Bytes + other.Bytes,
-		Sequence:    c.Sequence + other.Sequence,
-		DeviceID:    protocol.EmptyDeviceID,
-		LocalFlags:  c.LocalFlags | other.LocalFlags,
-	}
-}
-
-func (c Counts) TotalItems() int {
-	return c.Files + c.Directories + c.Symlinks + c.Deleted
-}
-
-func (c Counts) String() string {
-	var flags strings.Builder
-	if c.LocalFlags&needFlag != 0 {
-		flags.WriteString("Need")
-	}
-	if c.LocalFlags&protocol.FlagLocalIgnored != 0 {
-		flags.WriteString("Ignored")
-	}
-	if c.LocalFlags&protocol.FlagLocalMustRescan != 0 {
-		flags.WriteString("Rescan")
-	}
-	if c.LocalFlags&protocol.FlagLocalReceiveOnly != 0 {
-		flags.WriteString("Recvonly")
-	}
-	if c.LocalFlags&protocol.FlagLocalUnsupported != 0 {
-		flags.WriteString("Unsupported")
-	}
-	if c.LocalFlags != 0 {
-		flags.WriteString(fmt.Sprintf("(%x)", c.LocalFlags))
-	}
-	if flags.Len() == 0 {
-		flags.WriteString("---")
-	}
-	return fmt.Sprintf("{Device:%v, Files:%d, Dirs:%d, Symlinks:%d, Del:%d, Bytes:%d, Seq:%d, Flags:%s}", c.DeviceID, c.Files, c.Directories, c.Symlinks, c.Deleted, c.Bytes, c.Sequence, flags.String())
-}
-
-// Equal compares the numbers only, not sequence/dev/flags.
-func (c Counts) Equal(o Counts) bool {
-	return c.Files == o.Files && c.Directories == o.Directories && c.Symlinks == o.Symlinks && c.Deleted == o.Deleted && c.Bytes == o.Bytes
-}
-
-// update brings the VersionList up to date with file. It returns the updated
-// VersionList, a device that has the global/newest version, a device that previously
-// had the global/newest version, a boolean indicating if the global version has
-// changed and if any error occurred (only possible in db interaction).
-func vlUpdate(vl *dbproto.VersionList, folder, device []byte, file protocol.FileInfo, t readOnlyTransaction) (*dbproto.FileVersion, *dbproto.FileVersion, *dbproto.FileVersion, bool, bool, bool, error) {
-	if len(vl.Versions) == 0 {
-		nv := newFileVersion(device, file.FileVersion(), file.IsInvalid(), file.IsDeleted())
-		vl.Versions = append(vl.Versions, nv)
-		return nv, nil, nil, false, false, true, nil
-	}
-
-	// Get the current global (before updating)
-	oldFV, haveOldGlobal := vlGetGlobal(vl)
-	oldFV = fvCopy(oldFV)
-
-	// Remove ourselves first
-	removedFV, haveRemoved, _ := vlPop(vl, device)
-	// Find position and insert the file
-	err := vlInsert(vl, folder, device, file, t)
-	if err != nil {
-		return nil, nil, nil, false, false, false, err
-	}
-
-	newFV, _ := vlGetGlobal(vl) // We just inserted something above, can't be empty
-
-	if !haveOldGlobal {
-		return newFV, nil, removedFV, false, haveRemoved, true, nil
-	}
-
-	globalChanged := true
-	if fvIsInvalid(oldFV) == fvIsInvalid(newFV) && protocol.VectorFromWire(oldFV.Version).Equal(protocol.VectorFromWire(newFV.Version)) {
-		globalChanged = false
-	}
-
-	return newFV, oldFV, removedFV, true, haveRemoved, globalChanged, nil
-}
-
-func vlInsert(vl *dbproto.VersionList, folder, device []byte, file protocol.FileInfo, t readOnlyTransaction) error {
-	var added bool
-	var err error
-	i := 0
-	for ; i < len(vl.Versions); i++ {
-		// Insert our new version
-		added, err = vlCheckInsertAt(vl, i, folder, device, file, t)
-		if err != nil {
-			return err
-		}
-		if added {
-			break
-		}
-	}
-	if i == len(vl.Versions) {
-		// Append to the end
-		vl.Versions = append(vl.Versions, newFileVersion(device, file.FileVersion(), file.IsInvalid(), file.IsDeleted()))
-	}
-	return nil
-}
-
-func vlInsertAt(vl *dbproto.VersionList, i int, v *dbproto.FileVersion) {
-	vl.Versions = append(vl.Versions, &dbproto.FileVersion{})
-	copy(vl.Versions[i+1:], vl.Versions[i:])
-	vl.Versions[i] = v
-}
-
-// pop removes the given device from the VersionList and returns the FileVersion
-// before removing the device, whether it was found/removed at all and whether
-// the global changed in the process.
-func vlPop(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool, bool) {
-	invDevice, i, j, ok := vlFindDevice(vl, device)
-	if !ok {
-		return nil, false, false
-	}
-	globalPos := vlFindGlobal(vl)
-
-	fv := vl.Versions[i]
-	if fvDeviceCount(fv) == 1 {
-		vlPopVersionAt(vl, i)
-		return fv, true, globalPos == i
-	}
-
-	oldFV := fvCopy(fv)
-	if invDevice {
-		vl.Versions[i].InvalidDevices = popDeviceAt(vl.Versions[i].InvalidDevices, j)
-		return oldFV, true, false
-	}
-	vl.Versions[i].Devices = popDeviceAt(vl.Versions[i].Devices, j)
-	// If the last valid device of the previous global was removed above,
-	// the global changed.
-	return oldFV, true, len(vl.Versions[i].Devices) == 0 && globalPos == i
-}
-
-// Get returns a FileVersion that contains the given device and whether it has
-// been found at all.
-func vlGet(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool) {
-	_, i, _, ok := vlFindDevice(vl, device)
-	if !ok {
-		return &dbproto.FileVersion{}, false
-	}
-	return vl.Versions[i], true
-}
-
-// GetGlobal returns the current global FileVersion. The returned FileVersion
-// may be invalid, if all FileVersions are invalid. Returns false only if
-// VersionList is empty.
-func vlGetGlobal(vl *dbproto.VersionList) (*dbproto.FileVersion, bool) {
-	i := vlFindGlobal(vl)
-	if i == -1 {
-		return nil, false
-	}
-	return vl.Versions[i], true
-}
-
-// findGlobal returns the first version that isn't invalid, or if all versions are
-// invalid just the first version (i.e. 0) or -1, if there's no versions at all.
-func vlFindGlobal(vl *dbproto.VersionList) int {
-	for i := range vl.Versions {
-		if !fvIsInvalid(vl.Versions[i]) {
-			return i
-		}
-	}
-	if len(vl.Versions) == 0 {
-		return -1
-	}
-	return 0
-}
-
-// findDevice returns whether the device is in InvalidVersions or Versions and
-// in InvalidDevices or Devices (true for invalid), the positions in the version
-// and device slices and whether it has been found at all.
-func vlFindDevice(vl *dbproto.VersionList, device []byte) (bool, int, int, bool) {
-	for i, v := range vl.Versions {
-		if j := deviceIndex(v.Devices, device); j != -1 {
-			return false, i, j, true
-		}
-		if j := deviceIndex(v.InvalidDevices, device); j != -1 {
-			return true, i, j, true
-		}
-	}
-	return false, -1, -1, false
-}
-
-func vlPopVersionAt(vl *dbproto.VersionList, i int) {
-	vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
-}
-
-// checkInsertAt determines if the given device and associated file should be
-// inserted into the FileVersion at position i or into a new FileVersion at
-// position i.
-func vlCheckInsertAt(vl *dbproto.VersionList, i int, folder, device []byte, file protocol.FileInfo, t readOnlyTransaction) (bool, error) {
-	fv := vl.Versions[i]
-	ordering := protocol.VectorFromWire(fv.Version).Compare(file.FileVersion())
-	if ordering == protocol.Equal {
-		if !file.IsInvalid() {
-			fv.Devices = append(fv.Devices, device)
-		} else {
-			fv.InvalidDevices = append(fv.InvalidDevices, device)
-		}
-		return true, nil
-	}
-	existingDevice, _ := fvFirstDevice(fv)
-	insert, err := shouldInsertBefore(ordering, folder, existingDevice, fvIsInvalid(fv), file, t)
-	if err != nil {
-		return false, err
-	}
-	if insert {
-		vlInsertAt(vl, i, newFileVersion(device, file.FileVersion(), file.IsInvalid(), file.IsDeleted()))
-		return true, nil
-	}
-	return false, nil
-}
-
-// shouldInsertBefore determines whether the file comes before an existing
-// entry, given the version ordering (existing compared to new one), existing
-// device and if the existing version is invalid.
-func shouldInsertBefore(ordering protocol.Ordering, folder, existingDevice []byte, existingInvalid bool, file protocol.FileInfo, t readOnlyTransaction) (bool, error) {
-	switch ordering {
-	case protocol.Lesser:
-		// The version at this point in the list is lesser
-		// ("older") than us. We insert ourselves in front of it.
-		return true, nil
-
-	case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
-		// The version in conflict with us.
-		// Check if we can shortcut due to one being invalid.
-		if existingInvalid != file.IsInvalid() {
-			return existingInvalid, nil
-		}
-		// We must pull the actual file metadata to determine who wins.
-		// If we win, we insert ourselves in front of the loser here.
-		// (The "Lesser" and "Greater" in the condition above is just
-		// based on the device IDs in the version vector, which is not
-		// the only thing we use to determine the winner.)
-		of, ok, err := t.getFile(folder, existingDevice, []byte(file.FileName()))
-		if err != nil {
-			return false, err
-		}
-		// A surprise missing file entry here is counted as a win for us.
-		if !ok {
-			return true, nil
-		}
-		if file.WinsConflict(of) {
-			return true, nil
-		}
-	}
-	return false, nil
-}
-
-func deviceIndex(devices [][]byte, device []byte) int {
-	for i, dev := range devices {
-		if bytes.Equal(device, dev) {
-			return i
-		}
-	}
-	return -1
-}
-
-func popDeviceAt(devices [][]byte, i int) [][]byte {
-	return append(devices[:i], devices[i+1:]...)
-}
-
-func newFileVersion(device []byte, version protocol.Vector, invalid, deleted bool) *dbproto.FileVersion {
-	fv := &dbproto.FileVersion{
-		Version: version.ToWire(),
-		Deleted: deleted,
-	}
-	if invalid {
-		fv.InvalidDevices = [][]byte{device}
-	} else {
-		fv.Devices = [][]byte{device}
-	}
-	return fv
-}
-
-func fvFirstDevice(fv *dbproto.FileVersion) ([]byte, bool) {
-	if len(fv.Devices) != 0 {
-		return fv.Devices[0], true
-	}
-	if len(fv.InvalidDevices) != 0 {
-		return fv.InvalidDevices[0], true
-	}
-	return nil, false
-}
-
-func fvIsInvalid(fv *dbproto.FileVersion) bool {
-	return fv == nil || len(fv.Devices) == 0
-}
-
-func fvDeviceCount(fv *dbproto.FileVersion) int {
-	return len(fv.Devices) + len(fv.InvalidDevices)
-}
-
-func fvCopy(fv *dbproto.FileVersion) *dbproto.FileVersion {
-	return proto.Clone(fv).(*dbproto.FileVersion)
-}
diff --git a/lib/db/transactions.go b/lib/db/transactions.go
deleted file mode 100644
index 9efeff21efb..00000000000
--- a/lib/db/transactions.go
+++ /dev/null
@@ -1,1008 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"bytes"
-	"errors"
-	"fmt"
-
-	"google.golang.org/protobuf/proto"
-
-	"github.com/syncthing/syncthing/internal/gen/bep"
-	"github.com/syncthing/syncthing/internal/gen/dbproto"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/osutil"
-	"github.com/syncthing/syncthing/lib/protocol"
-	"github.com/syncthing/syncthing/lib/sliceutil"
-)
-
-var (
-	errEntryFromGlobalMissing = errors.New("device present in global list but missing as device/fileinfo entry")
-	errEmptyGlobal            = errors.New("no versions in global list")
-	errEmptyFileVersion       = errors.New("no devices in global file version")
-)
-
-// A readOnlyTransaction represents a database snapshot.
-type readOnlyTransaction struct {
-	backend.ReadTransaction
-	keyer    keyer
-	evLogger events.Logger
-}
-
-func (db *Lowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
-	tran, err := db.NewReadTransaction()
-	if err != nil {
-		return readOnlyTransaction{}, err
-	}
-	return db.readOnlyTransactionFromBackendTransaction(tran), nil
-}
-
-func (db *Lowlevel) readOnlyTransactionFromBackendTransaction(tran backend.ReadTransaction) readOnlyTransaction {
-	return readOnlyTransaction{
-		ReadTransaction: tran,
-		keyer:           db.keyer,
-		evLogger:        db.evLogger,
-	}
-}
-
-func (t readOnlyTransaction) close() {
-	t.Release()
-}
-
-func (t readOnlyTransaction) getFile(folder, device, file []byte) (protocol.FileInfo, bool, error) {
-	key, err := t.keyer.GenerateDeviceFileKey(nil, folder, device, file)
-	if err != nil {
-		return protocol.FileInfo{}, false, err
-	}
-	return t.getFileByKey(key)
-}
-
-func (t readOnlyTransaction) getFileByKey(key []byte) (protocol.FileInfo, bool, error) {
-	f, ok, err := t.getFileTrunc(key, false)
-	if err != nil || !ok {
-		return protocol.FileInfo{}, false, err
-	}
-	return f, true, nil
-}
-
-func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (protocol.FileInfo, bool, error) {
-	bs, err := t.Get(key)
-	if backend.IsNotFound(err) {
-		return protocol.FileInfo{}, false, nil
-	}
-	if err != nil {
-		return protocol.FileInfo{}, false, err
-	}
-	f, err := t.unmarshalTrunc(bs, trunc)
-	if backend.IsNotFound(err) {
-		return protocol.FileInfo{}, false, nil
-	}
-	if err != nil {
-		return protocol.FileInfo{}, false, err
-	}
-	return f, true, nil
-}
-
-func (t readOnlyTransaction) unmarshalTrunc(bs []byte, trunc bool) (protocol.FileInfo, error) {
-	if trunc {
-		var bfi dbproto.FileInfoTruncated
-		err := proto.Unmarshal(bs, &bfi)
-		if err != nil {
-			return protocol.FileInfo{}, err
-		}
-		if err := t.fillTruncated(&bfi); err != nil {
-			return protocol.FileInfo{}, err
-		}
-		return protocol.FileInfoFromDBTruncated(&bfi), nil
-	}
-
-	var bfi bep.FileInfo
-	err := proto.Unmarshal(bs, &bfi)
-	if err != nil {
-		return protocol.FileInfo{}, err
-	}
-	if err := t.fillFileInfo(&bfi); err != nil {
-		return protocol.FileInfo{}, err
-	}
-	return protocol.FileInfoFromDB(&bfi), nil
-}
-
-type blocksIndirectionError struct {
-	err error
-}
-
-func (e *blocksIndirectionError) Error() string {
-	return fmt.Sprintf("filling Blocks: %v", e.err)
-}
-
-func (e *blocksIndirectionError) Unwrap() error {
-	return e.err
-}
-
-// fillFileInfo follows the (possible) indirection of blocks and version
-// vector and fills it out.
-func (t readOnlyTransaction) fillFileInfo(fi *bep.FileInfo) error {
-	var key []byte
-
-	if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
-		// The blocks list is indirected and we need to load it.
-		key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
-		bs, err := t.Get(key)
-		if err != nil {
-			return &blocksIndirectionError{err}
-		}
-		var bl dbproto.BlockList
-		if err := proto.Unmarshal(bs, &bl); err != nil {
-			return err
-		}
-		fi.Blocks = bl.Blocks
-	}
-
-	if len(fi.VersionHash) != 0 {
-		key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
-		bs, err := t.Get(key)
-		if err != nil {
-			return fmt.Errorf("filling Version: %w", err)
-		}
-		var v bep.Vector
-		if err := proto.Unmarshal(bs, &v); err != nil {
-			return err
-		}
-		fi.Version = &v
-	}
-
-	return nil
-}
-
-// fillTruncated follows the (possible) indirection of version vector and
-// fills it.
-func (t readOnlyTransaction) fillTruncated(fi *dbproto.FileInfoTruncated) error {
-	var key []byte
-
-	if len(fi.VersionHash) == 0 {
-		return nil
-	}
-
-	key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
-	bs, err := t.Get(key)
-	if err != nil {
-		return err
-	}
-	var v bep.Vector
-	if err := proto.Unmarshal(bs, &v); err != nil {
-		return err
-	}
-	fi.Version = &v
-	return nil
-}
-
-func (t readOnlyTransaction) getGlobalVersions(keyBuf, folder, file []byte) (*dbproto.VersionList, error) {
-	var err error
-	keyBuf, err = t.keyer.GenerateGlobalVersionKey(keyBuf, folder, file)
-	if err != nil {
-		return nil, err
-	}
-	return t.getGlobalVersionsByKey(keyBuf)
-}
-
-func (t readOnlyTransaction) getGlobalVersionsByKey(key []byte) (*dbproto.VersionList, error) {
-	bs, err := t.Get(key)
-	if err != nil {
-		return nil, err
-	}
-
-	var vl dbproto.VersionList
-	if err := proto.Unmarshal(bs, &vl); err != nil {
-		return nil, err
-	}
-
-	return &vl, nil
-}
-
-func (t readOnlyTransaction) getGlobal(keyBuf, folder, file []byte, truncate bool) ([]byte, protocol.FileInfo, bool, error) {
-	vl, err := t.getGlobalVersions(keyBuf, folder, file)
-	if backend.IsNotFound(err) {
-		return keyBuf, protocol.FileInfo{}, false, nil
-	} else if err != nil {
-		return nil, protocol.FileInfo{}, false, err
-	}
-	keyBuf, fi, err := t.getGlobalFromVersionList(keyBuf, folder, file, truncate, vl)
-	return keyBuf, fi, true, err
-}
-
-func (t readOnlyTransaction) getGlobalFromVersionList(keyBuf, folder, file []byte, truncate bool, vl *dbproto.VersionList) ([]byte, protocol.FileInfo, error) {
-	fv, ok := vlGetGlobal(vl)
-	if !ok {
-		return keyBuf, protocol.FileInfo{}, errEmptyGlobal
-	}
-	keyBuf, fi, err := t.getGlobalFromFileVersion(keyBuf, folder, file, truncate, fv)
-	return keyBuf, fi, err
-}
-
-func (t readOnlyTransaction) getGlobalFromFileVersion(keyBuf, folder, file []byte, truncate bool, fv *dbproto.FileVersion) ([]byte, protocol.FileInfo, error) {
-	dev, ok := fvFirstDevice(fv)
-	if !ok {
-		return keyBuf, protocol.FileInfo{}, errEmptyFileVersion
-	}
-	keyBuf, err := t.keyer.GenerateDeviceFileKey(keyBuf, folder, dev, file)
-	if err != nil {
-		return keyBuf, protocol.FileInfo{}, err
-	}
-	fi, ok, err := t.getFileTrunc(keyBuf, truncate)
-	if err != nil {
-		return keyBuf, protocol.FileInfo{}, err
-	}
-	if !ok {
-		return keyBuf, protocol.FileInfo{}, errEntryFromGlobalMissing
-	}
-	return keyBuf, fi, nil
-}
-
-func (t *readOnlyTransaction) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) error {
-	if len(prefix) > 0 {
-		unslashedPrefix := prefix
-		if bytes.HasSuffix(prefix, []byte{'/'}) {
-			unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
-		} else {
-			prefix = append(prefix, '/')
-		}
-
-		key, err := t.keyer.GenerateDeviceFileKey(nil, folder, device, unslashedPrefix)
-		if err != nil {
-			return err
-		}
-		if f, ok, err := t.getFileTrunc(key, truncate); err != nil {
-			return err
-		} else if ok && !fn(f) {
-			return nil
-		}
-	}
-
-	key, err := t.keyer.GenerateDeviceFileKey(nil, folder, device, prefix)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key)
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	for dbi.Next() {
-		name := t.keyer.NameFromDeviceFileKey(dbi.Key())
-		if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
-			return nil
-		}
-
-		f, err := t.unmarshalTrunc(dbi.Value(), truncate)
-		if err != nil {
-			l.Debugln("unmarshal error:", err)
-			continue
-		}
-		if !fn(f) {
-			return nil
-		}
-	}
-	return dbi.Error()
-}
-
-func (t *readOnlyTransaction) withHaveSequence(folder []byte, startSeq int64, fn Iterator) error {
-	first, err := t.keyer.GenerateSequenceKey(nil, folder, startSeq)
-	if err != nil {
-		return err
-	}
-	last, err := t.keyer.GenerateSequenceKey(nil, folder, maxInt64)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewRangeIterator(first, last)
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	for dbi.Next() {
-		f, ok, err := t.getFileByKey(dbi.Value())
-		if err != nil {
-			return err
-		}
-		if !ok {
-			l.Debugln("missing file for sequence number", t.keyer.SequenceFromSequenceKey(dbi.Key()))
-			continue
-		}
-
-		if shouldDebug() {
-			if seq := t.keyer.SequenceFromSequenceKey(dbi.Key()); f.Sequence != seq {
-				l.Debugf("Sequence index corruption (folder %v, file %v): sequence %d != expected %d", string(folder), f.Name, f.Sequence, seq)
-			}
-		}
-		if !fn(f) {
-			return nil
-		}
-	}
-	return dbi.Error()
-}
-
-func (t *readOnlyTransaction) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) error {
-	if len(prefix) > 0 {
-		unslashedPrefix := prefix
-		if bytes.HasSuffix(prefix, []byte{'/'}) {
-			unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
-		} else {
-			prefix = append(prefix, '/')
-		}
-
-		if _, f, ok, err := t.getGlobal(nil, folder, unslashedPrefix, truncate); err != nil {
-			return err
-		} else if ok && !fn(f) {
-			return nil
-		}
-	}
-
-	key, err := t.keyer.GenerateGlobalVersionKey(nil, folder, prefix)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key)
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	var dk []byte
-	for dbi.Next() {
-		name := t.keyer.NameFromGlobalVersionKey(dbi.Key())
-		if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
-			return nil
-		}
-
-		var vl dbproto.VersionList
-		if err := proto.Unmarshal(dbi.Value(), &vl); err != nil {
-			return err
-		}
-
-		var f protocol.FileInfo
-		dk, f, err = t.getGlobalFromVersionList(dk, folder, name, truncate, &vl)
-		if err != nil {
-			return err
-		}
-
-		if !fn(f) {
-			return nil
-		}
-	}
-	if err != nil {
-		return err
-	}
-	return dbi.Error()
-}
-
-func (t *readOnlyTransaction) withBlocksHash(folder, hash []byte, iterator Iterator) error {
-	key, err := t.keyer.GenerateBlockListMapKey(nil, folder, hash, nil)
-	if err != nil {
-		return err
-	}
-
-	iter, err := t.NewPrefixIterator(key)
-	if err != nil {
-		return err
-	}
-	defer iter.Release()
-
-	for iter.Next() {
-		file := string(t.keyer.NameFromBlockListMapKey(iter.Key()))
-		f, ok, err := t.getFile(folder, protocol.LocalDeviceID[:], []byte(osutil.NormalizedFilename(file)))
-		if err != nil {
-			return err
-		}
-		if !ok {
-			continue
-		}
-		f.Name = osutil.NativeFilename(f.Name)
-
-		if !bytes.Equal(f.BlocksHash, hash) {
-			msg := "Mismatching block map list hashes"
-			t.evLogger.Log(events.Failure, fmt.Sprintln(msg, "in withBlocksHash"))
-			l.Warnf("%v: got %x expected %x", msg, f.BlocksHash, hash)
-			continue
-		}
-
-		if f.IsDeleted() || f.IsInvalid() || f.IsDirectory() || f.IsSymlink() {
-			msg := "Found something of unexpected type in block list map"
-			t.evLogger.Log(events.Failure, fmt.Sprintln(msg, "in withBlocksHash"))
-			l.Warnf("%v: %s", msg, f)
-			continue
-		}
-
-		if !iterator(f) {
-			break
-		}
-	}
-
-	return iter.Error()
-}
-
-func (t *readOnlyTransaction) availability(folder, file []byte) ([]protocol.DeviceID, error) {
-	vl, err := t.getGlobalVersions(nil, folder, file)
-	if backend.IsNotFound(err) {
-		return nil, nil
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	fv, ok := vlGetGlobal(vl)
-	if !ok {
-		return nil, nil
-	}
-	devices := make([]protocol.DeviceID, len(fv.Devices))
-	for i, dev := range fv.Devices {
-		n, err := protocol.DeviceIDFromBytes(dev)
-		if err != nil {
-			return nil, err
-		}
-		devices[i] = n
-	}
-
-	return devices, nil
-}
-
-func (t *readOnlyTransaction) withNeed(folder, device []byte, truncate bool, fn Iterator) error {
-	if bytes.Equal(device, protocol.LocalDeviceID[:]) {
-		return t.withNeedLocal(folder, truncate, fn)
-	}
-	return t.withNeedIteratingGlobal(folder, device, truncate, fn)
-}
-
-func (t *readOnlyTransaction) withNeedIteratingGlobal(folder, device []byte, truncate bool, fn Iterator) error {
-	key, err := t.keyer.GenerateGlobalVersionKey(nil, folder, nil)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key.WithoutName())
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	var dk []byte
-	devID, err := protocol.DeviceIDFromBytes(device)
-	if err != nil {
-		return err
-	}
-	for dbi.Next() {
-		var vl dbproto.VersionList
-		if err := proto.Unmarshal(dbi.Value(), &vl); err != nil {
-			return err
-		}
-
-		globalFV, ok := vlGetGlobal(&vl)
-		if !ok {
-			return errEmptyGlobal
-		}
-		haveFV, have := vlGet(&vl, device)
-
-		if !Need(globalFV, have, protocol.VectorFromWire(haveFV.Version)) {
-			continue
-		}
-
-		name := t.keyer.NameFromGlobalVersionKey(dbi.Key())
-		var gf protocol.FileInfo
-		dk, gf, err = t.getGlobalFromFileVersion(dk, folder, name, truncate, globalFV)
-		if err != nil {
-			return err
-		}
-
-		if shouldDebug() {
-			if globalDev, ok := fvFirstDevice(globalFV); ok {
-				globalID, _ := protocol.DeviceIDFromBytes(globalDev)
-				l.Debugf("need folder=%q device=%v name=%q have=%v invalid=%v haveV=%v haveDeleted=%v globalV=%v globalDeleted=%v globalDev=%v", folder, devID, name, have, fvIsInvalid(haveFV), haveFV.Version, haveFV.Deleted, gf.FileVersion(), globalFV.Deleted, globalID)
-			}
-		}
-		if !fn(gf) {
-			return dbi.Error()
-		}
-	}
-	return dbi.Error()
-}
-
-func (t *readOnlyTransaction) withNeedLocal(folder []byte, truncate bool, fn Iterator) error {
-	key, err := t.keyer.GenerateNeedFileKey(nil, folder, nil)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key.WithoutName())
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	var keyBuf []byte
-	var f protocol.FileInfo
-	var ok bool
-	for dbi.Next() {
-		keyBuf, f, ok, err = t.getGlobal(keyBuf, folder, t.keyer.NameFromGlobalVersionKey(dbi.Key()), truncate)
-		if err != nil {
-			return err
-		}
-		if !ok {
-			continue
-		}
-		if !fn(f) {
-			return nil
-		}
-	}
-	return dbi.Error()
-}
-
-// A readWriteTransaction is a readOnlyTransaction plus a batch for writes.
-// The batch will be committed on close() or by checkFlush() if it exceeds the
-// batch size.
-type readWriteTransaction struct {
-	backend.WriteTransaction
-	readOnlyTransaction
-	indirectionTracker
-}
-
-type indirectionTracker interface {
-	recordIndirectionHashesForFile(f *protocol.FileInfo)
-}
-
-func (db *Lowlevel) newReadWriteTransaction(hooks ...backend.CommitHook) (readWriteTransaction, error) {
-	tran, err := db.NewWriteTransaction(hooks...)
-	if err != nil {
-		return readWriteTransaction{}, err
-	}
-	return readWriteTransaction{
-		WriteTransaction:    tran,
-		readOnlyTransaction: db.readOnlyTransactionFromBackendTransaction(tran),
-		indirectionTracker:  db,
-	}, nil
-}
-
-func (t readWriteTransaction) Commit() error {
-	// The readOnlyTransaction must close after commit, because they may be
-	// backed by the same actual lower level transaction.
-	defer t.readOnlyTransaction.close()
-	return t.WriteTransaction.Commit()
-}
-
-func (t readWriteTransaction) close() {
-	t.readOnlyTransaction.close()
-	t.WriteTransaction.Release()
-}
-
-// putFile stores a file in the database, taking care of indirected fields.
-func (t readWriteTransaction) putFile(fkey []byte, fi protocol.FileInfo) error {
-	var bkey []byte
-
-	// Always set the blocks hash when there are blocks.
-	if len(fi.Blocks) > 0 {
-		fi.BlocksHash = protocol.BlocksHash(fi.Blocks)
-	} else {
-		fi.BlocksHash = nil
-	}
-
-	// Indirect the blocks if the block list is large enough.
-	if len(fi.Blocks) > blocksIndirectionCutoff {
-		bkey = t.keyer.GenerateBlockListKey(bkey, fi.BlocksHash)
-		if _, err := t.Get(bkey); backend.IsNotFound(err) {
-			// Marshal the block list and save it
-			blocks := sliceutil.Map(fi.Blocks, protocol.BlockInfo.ToWire)
-			blocksBs := mustMarshal(&dbproto.BlockList{Blocks: blocks})
-			if err := t.Put(bkey, blocksBs); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-		fi.Blocks = nil
-	}
-
-	// Indirect the version vector if it's large enough.
-	if len(fi.Version.Counters) > versionIndirectionCutoff {
-		fi.VersionHash = protocol.VectorHash(fi.Version)
-		bkey = t.keyer.GenerateVersionKey(bkey, fi.VersionHash)
-		if _, err := t.Get(bkey); backend.IsNotFound(err) {
-			// Marshal the version vector and save it
-			versionBs := mustMarshal(fi.Version.ToWire())
-			if err := t.Put(bkey, versionBs); err != nil {
-				return err
-			}
-		} else if err != nil {
-			return err
-		}
-		fi.Version = protocol.Vector{}
-	} else {
-		fi.VersionHash = nil
-	}
-
-	t.indirectionTracker.recordIndirectionHashesForFile(&fi)
-
-	fiBs := mustMarshal(fi.ToWire(true))
-	return t.Put(fkey, fiBs)
-}
-
-// updateGlobal adds this device+version to the version list for the given
-// file. If the device is already present in the list, the version is updated.
-// If the file does not have an entry in the global list, it is created.
-func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, file protocol.FileInfo, meta *metadataTracker) ([]byte, error) {
-	deviceID, err := protocol.DeviceIDFromBytes(device)
-	if err != nil {
-		return nil, err
-	}
-
-	l.Debugf("update global; folder=%q device=%v file=%q version=%v invalid=%v", folder, deviceID, file.Name, file.Version, file.IsInvalid())
-
-	fl, err := t.getGlobalVersionsByKey(gk)
-	if err != nil && !backend.IsNotFound(err) {
-		return nil, err
-	}
-	if fl == nil {
-		fl = &dbproto.VersionList{}
-	}
-
-	globalFV, oldGlobalFV, removedFV, haveOldGlobal, haveRemoved, globalChanged, err := vlUpdate(fl, folder, device, file, t.readOnlyTransaction)
-	if err != nil {
-		return nil, err
-	}
-
-	name := []byte(file.Name)
-
-	l.Debugf(`new global for "%v" after update: %v`, file.Name, fl)
-	if err := t.Put(gk, mustMarshal(fl)); err != nil {
-		return nil, err
-	}
-
-	// Only load those from db if actually needed
-
-	var gotGlobal, gotOldGlobal bool
-	var global, oldGlobal protocol.FileInfo
-
-	// Check the need of the device that was updated
-	// Must happen before updating global meta: If this is the first
-	// item from this device, it will be initialized with the global state.
-
-	needBefore := haveOldGlobal && Need(oldGlobalFV, haveRemoved, protocol.VectorFromWire(removedFV.GetVersion()))
-	needNow := Need(globalFV, true, file.Version)
-	if needBefore {
-		if keyBuf, oldGlobal, err = t.getGlobalFromFileVersion(keyBuf, folder, name, true, oldGlobalFV); err != nil {
-			return nil, err
-		}
-		gotOldGlobal = true
-		meta.removeNeeded(deviceID, oldGlobal)
-		if !needNow && bytes.Equal(device, protocol.LocalDeviceID[:]) {
-			if keyBuf, err = t.updateLocalNeed(keyBuf, folder, name, false); err != nil {
-				return nil, err
-			}
-		}
-	}
-	if needNow {
-		keyBuf, global, err = t.getGlobalFromFileVersion(keyBuf, folder, name, true, globalFV)
-		if err != nil {
-			return nil, err
-		}
-		gotGlobal = true
-		meta.addNeeded(deviceID, global)
-		if !needBefore && bytes.Equal(device, protocol.LocalDeviceID[:]) {
-			if keyBuf, err = t.updateLocalNeed(keyBuf, folder, name, true); err != nil {
-				return nil, err
-			}
-		}
-	}
-
-	// Update global size counter if necessary
-
-	if !globalChanged {
-		// Neither the global state nor the needs of any devices, except
-		// the one updated, changed.
-		return keyBuf, nil
-	}
-
-	// Remove the old global from the global size counter
-	if haveOldGlobal {
-		if !gotOldGlobal {
-			if keyBuf, oldGlobal, err = t.getGlobalFromFileVersion(keyBuf, folder, name, true, oldGlobalFV); err != nil {
-				return nil, err
-			}
-		}
-		// Remove the old global from the global size counter
-		meta.removeFile(protocol.GlobalDeviceID, oldGlobal)
-	}
-
-	// Add the new global to the global size counter
-	if !gotGlobal {
-		if protocol.VectorFromWire(globalFV.Version).Equal(file.Version) {
-			// The inserted file is the global file
-			global = file
-		} else {
-			keyBuf, global, err = t.getGlobalFromFileVersion(keyBuf, folder, name, true, globalFV)
-			if err != nil {
-				return nil, err
-			}
-		}
-	}
-	meta.addFile(protocol.GlobalDeviceID, global)
-
-	// check for local (if not already done before)
-	if !bytes.Equal(device, protocol.LocalDeviceID[:]) {
-		localFV, haveLocal := vlGet(fl, protocol.LocalDeviceID[:])
-		localVersion := protocol.VectorFromWire(localFV.Version)
-		needBefore := haveOldGlobal && Need(oldGlobalFV, haveLocal, localVersion)
-		needNow := Need(globalFV, haveLocal, localVersion)
-		if needBefore {
-			meta.removeNeeded(protocol.LocalDeviceID, oldGlobal)
-			if !needNow {
-				if keyBuf, err = t.updateLocalNeed(keyBuf, folder, name, false); err != nil {
-					return nil, err
-				}
-			}
-		}
-		if needNow {
-			meta.addNeeded(protocol.LocalDeviceID, global)
-			if !needBefore {
-				if keyBuf, err = t.updateLocalNeed(keyBuf, folder, name, true); err != nil {
-					return nil, err
-				}
-			}
-		}
-	}
-
-	for _, dev := range meta.devices() {
-		if bytes.Equal(dev[:], device) {
-			// Already handled above
-			continue
-		}
-		fv, have := vlGet(fl, dev[:])
-		fvVersion := protocol.VectorFromWire(fv.Version)
-		if haveOldGlobal && Need(oldGlobalFV, have, fvVersion) {
-			meta.removeNeeded(dev, oldGlobal)
-		}
-		if Need(globalFV, have, fvVersion) {
-			meta.addNeeded(dev, global)
-		}
-	}
-
-	return keyBuf, nil
-}
-
-func (t readWriteTransaction) updateLocalNeed(keyBuf, folder, name []byte, add bool) ([]byte, error) {
-	var err error
-	keyBuf, err = t.keyer.GenerateNeedFileKey(keyBuf, folder, name)
-	if err != nil {
-		return nil, err
-	}
-	if add {
-		l.Debugf("local need insert; folder=%q, name=%q", folder, name)
-		err = t.Put(keyBuf, nil)
-	} else {
-		l.Debugf("local need delete; folder=%q, name=%q", folder, name)
-		err = t.Delete(keyBuf)
-	}
-	return keyBuf, err
-}
-
-func Need(global *dbproto.FileVersion, haveLocal bool, localVersion protocol.Vector) bool {
-	// We never need an invalid file or a file without a valid version (just
-	// another way of expressing "invalid", really, until we fix that
-	// part...).
-	globalVersion := protocol.VectorFromWire(global.Version)
-	if fvIsInvalid(global) || globalVersion.IsEmpty() {
-		return false
-	}
-	// We don't need a deleted file if we don't have it.
-	if global.Deleted && !haveLocal {
-		return false
-	}
-	// We don't need the global file if we already have the same version.
-	if haveLocal && localVersion.GreaterEqual(globalVersion) {
-		return false
-	}
-	return true
-}
-
-// removeFromGlobal removes the device from the global version list for the
-// given file. If the version list is empty after this, the file entry is
-// removed entirely.
-func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device, file []byte, meta *metadataTracker) ([]byte, error) {
-	deviceID, err := protocol.DeviceIDFromBytes(device)
-	if err != nil {
-		return nil, err
-	}
-
-	l.Debugf("remove from global; folder=%q device=%v file=%q", folder, deviceID, file)
-
-	fl, err := t.getGlobalVersionsByKey(gk)
-	if backend.IsNotFound(err) {
-		// We might be called to "remove" a global version that doesn't exist
-		// if the first update for the file is already marked invalid.
-		return keyBuf, nil
-	} else if err != nil {
-		return nil, err
-	}
-
-	oldGlobalFV, haveOldGlobal := vlGetGlobal(fl)
-	oldGlobalFV = fvCopy(oldGlobalFV)
-
-	if !haveOldGlobal {
-		// Shouldn't ever happen, but doesn't hurt to handle.
-		t.evLogger.Log(events.Failure, "encountered empty global while removing item")
-		return keyBuf, t.Delete(gk)
-	}
-
-	removedFV, haveRemoved, globalChanged := vlPop(fl, device)
-	if !haveRemoved {
-		// There is no version for the given device
-		return keyBuf, nil
-	}
-
-	var global protocol.FileInfo
-	var gotGlobal bool
-
-	globalFV, haveGlobal := vlGetGlobal(fl)
-	// Add potential needs of the removed device
-	if haveGlobal && !fvIsInvalid(globalFV) && Need(globalFV, false, protocol.Vector{}) && !Need(oldGlobalFV, haveRemoved, protocol.VectorFromWire(removedFV.Version)) {
-		keyBuf, global, err = t.getGlobalFromVersionList(keyBuf, folder, file, true, fl)
-		if err != nil {
-			return nil, err
-		}
-		gotGlobal = true
-		meta.addNeeded(deviceID, global)
-		if bytes.Equal(protocol.LocalDeviceID[:], device) {
-			if keyBuf, err = t.updateLocalNeed(keyBuf, folder, file, true); err != nil {
-				return nil, err
-			}
-		}
-	}
-
-	// Global hasn't changed, abort early
-	if !globalChanged {
-		l.Debugf("new global after remove: %v", fl)
-		if err := t.Put(gk, mustMarshal(fl)); err != nil {
-			return nil, err
-		}
-		return keyBuf, nil
-	}
-
-	var oldGlobal protocol.FileInfo
-	keyBuf, oldGlobal, err = t.getGlobalFromFileVersion(keyBuf, folder, file, true, oldGlobalFV)
-	if err != nil {
-		return nil, err
-	}
-	meta.removeFile(protocol.GlobalDeviceID, oldGlobal)
-
-	// Remove potential device needs
-	shouldRemoveNeed := func(dev protocol.DeviceID) bool {
-		fv, have := vlGet(fl, dev[:])
-		fvVersion := protocol.VectorFromWire(fv.Version)
-		if !Need(oldGlobalFV, have, fvVersion) {
-			return false // Didn't need it before
-		}
-		return !haveGlobal || !Need(globalFV, have, fvVersion)
-	}
-	if shouldRemoveNeed(protocol.LocalDeviceID) {
-		meta.removeNeeded(protocol.LocalDeviceID, oldGlobal)
-		if keyBuf, err = t.updateLocalNeed(keyBuf, folder, file, false); err != nil {
-			return nil, err
-		}
-	}
-	for _, dev := range meta.devices() {
-		if bytes.Equal(dev[:], device) { // Was the previous global
-			continue
-		}
-		if shouldRemoveNeed(dev) {
-			meta.removeNeeded(dev, oldGlobal)
-		}
-	}
-
-	// Nothing left, i.e. nothing to add to the global counter below.
-	if len(fl.Versions) == 0 {
-		if err := t.Delete(gk); err != nil {
-			return nil, err
-		}
-		return keyBuf, nil
-	}
-
-	// Add to global
-	if !gotGlobal {
-		keyBuf, global, err = t.getGlobalFromVersionList(keyBuf, folder, file, true, fl)
-		if err != nil {
-			return nil, err
-		}
-	}
-	meta.addFile(protocol.GlobalDeviceID, global)
-
-	l.Debugf(`new global for "%s" after remove: %v`, file, fl)
-	if err := t.Put(gk, mustMarshal(fl)); err != nil {
-		return nil, err
-	}
-
-	return keyBuf, nil
-}
-
-func (t readWriteTransaction) deleteKeyPrefix(prefix []byte) error {
-	return t.deleteKeyPrefixMatching(prefix, func([]byte) bool { return true })
-}
-
-func (t readWriteTransaction) deleteKeyPrefixMatching(prefix []byte, match func(key []byte) bool) error {
-	dbi, err := t.NewPrefixIterator(prefix)
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-	for dbi.Next() {
-		if !match(dbi.Key()) {
-			continue
-		}
-		if err := t.Delete(dbi.Key()); err != nil {
-			return err
-		}
-	}
-	return dbi.Error()
-}
-
-func (t *readWriteTransaction) withAllFolderTruncated(folder []byte, fn func(device []byte, f protocol.FileInfo) bool) error {
-	key, err := t.keyer.GenerateDeviceFileKey(nil, folder, nil, nil)
-	if err != nil {
-		return err
-	}
-	dbi, err := t.NewPrefixIterator(key.WithoutNameAndDevice())
-	if err != nil {
-		return err
-	}
-	defer dbi.Release()
-
-	var gk, keyBuf []byte
-	for dbi.Next() {
-		device, ok := t.keyer.DeviceFromDeviceFileKey(dbi.Key())
-		if !ok {
-			// Not having the device in the index is bad. Clear it.
-			if err := t.Delete(dbi.Key()); err != nil {
-				return err
-			}
-			continue
-		}
-
-		f, err := t.unmarshalTrunc(dbi.Value(), true)
-		if err != nil {
-			return err
-		}
-
-		switch f.Name {
-		case "", ".", "..", "/": // A few obviously invalid filenames
-			l.Infof("Dropping invalid filename %q from database", f.Name)
-			name := []byte(f.Name)
-			gk, err = t.keyer.GenerateGlobalVersionKey(gk, folder, name)
-			if err != nil {
-				return err
-			}
-			keyBuf, err = t.removeFromGlobal(gk, keyBuf, folder, device, name, nil)
-			if err != nil {
-				return err
-			}
-			if err := t.Delete(dbi.Key()); err != nil {
-				return err
-			}
-			continue
-		}
-
-		if !fn(device, f) {
-			return nil
-		}
-	}
-	return dbi.Error()
-}
-
-func mustMarshal(f proto.Message) []byte {
-	bs, err := proto.Marshal(f)
-	if err != nil {
-		panic(err)
-	}
-	return bs
-}
diff --git a/lib/fs/mtimefs.go b/lib/fs/mtimefs.go
index ba1d1ecc5bf..3f4e0721130 100644
--- a/lib/fs/mtimefs.go
+++ b/lib/fs/mtimefs.go
@@ -7,21 +7,21 @@
 package fs
 
 import (
-	"errors"
 	"time"
 )
 
 // The database is where we store the virtual mtimes
 type database interface {
-	Bytes(key string) (data []byte, ok bool, err error)
-	PutBytes(key string, data []byte) error
-	Delete(key string) error
+	GetMtime(folder, name string) (ondisk, virtual time.Time)
+	PutMtime(folder, name string, ondisk, virtual time.Time) error
+	DeleteMtime(folder, name string) error
 }
 
 type mtimeFS struct {
 	Filesystem
 	chtimes         func(string, time.Time, time.Time) error
 	db              database
+	folderID        string
 	caseInsensitive bool
 }
 
@@ -34,16 +34,18 @@ func WithCaseInsensitivity(v bool) MtimeFSOption {
 }
 
 type optionMtime struct {
-	db      database
-	options []MtimeFSOption
+	db       database
+	folderID string
+	options  []MtimeFSOption
 }
 
 // NewMtimeOption makes any filesystem provide nanosecond mtime precision,
 // regardless of what shenanigans the underlying filesystem gets up to.
-func NewMtimeOption(db database, options ...MtimeFSOption) Option {
+func NewMtimeOption(db database, folderID string, options ...MtimeFSOption) Option {
 	return &optionMtime{
-		db:      db,
-		options: options,
+		db:       db,
+		folderID: folderID,
+		options:  options,
 	}
 }
 
@@ -52,6 +54,7 @@ func (o *optionMtime) apply(fs Filesystem) Filesystem {
 		Filesystem: fs,
 		chtimes:    fs.Chtimes, // for mocking it out in the tests
 		db:         o.db,
+		folderID:   o.folderID,
 	}
 	for _, opt := range o.options {
 		opt(f)
@@ -84,14 +87,11 @@ func (f *mtimeFS) Stat(name string) (FileInfo, error) {
 		return nil, err
 	}
 
-	mtimeMapping, err := f.load(name)
-	if err != nil {
-		return nil, err
-	}
-	if mtimeMapping.Real.Equal(info.ModTime()) {
+	ondisk, virtual := f.load(name)
+	if ondisk.Equal(info.ModTime()) {
 		info = mtimeFileInfo{
 			FileInfo: info,
-			mtime:    mtimeMapping.Virtual,
+			mtime:    virtual,
 		}
 	}
 
@@ -104,14 +104,11 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
 		return nil, err
 	}
 
-	mtimeMapping, err := f.load(name)
-	if err != nil {
-		return nil, err
-	}
-	if mtimeMapping.Real.Equal(info.ModTime()) {
+	ondisk, virtual := f.load(name)
+	if ondisk.Equal(info.ModTime()) {
 		info = mtimeFileInfo{
 			FileInfo: info,
-			mtime:    mtimeMapping.Virtual,
+			mtime:    virtual,
 		}
 	}
 
@@ -150,43 +147,27 @@ func (*mtimeFS) wrapperType() filesystemWrapperType {
 	return filesystemWrapperTypeMtime
 }
 
-func (f *mtimeFS) save(name string, real, virtual time.Time) {
+func (f *mtimeFS) save(name string, ondisk, virtual time.Time) {
 	if f.caseInsensitive {
 		name = UnicodeLowercaseNormalized(name)
 	}
 
-	if real.Equal(virtual) {
+	if ondisk.Equal(virtual) {
 		// If the virtual time and the real on disk time are equal we don't
 		// need to store anything.
-		f.db.Delete(name)
+		_ = f.db.DeleteMtime(f.folderID, name)
 		return
 	}
 
-	mtime := MtimeMapping{
-		Real:    real,
-		Virtual: virtual,
-	}
-	bs, _ := mtime.Marshal() // Can't fail
-	f.db.PutBytes(name, bs)
+	_ = f.db.PutMtime(f.folderID, name, ondisk, virtual)
 }
 
-func (f *mtimeFS) load(name string) (MtimeMapping, error) {
+func (f *mtimeFS) load(name string) (ondisk, virtual time.Time) {
 	if f.caseInsensitive {
 		name = UnicodeLowercaseNormalized(name)
 	}
 
-	data, exists, err := f.db.Bytes(name)
-	if err != nil {
-		return MtimeMapping{}, err
-	} else if !exists {
-		return MtimeMapping{}, nil
-	}
-
-	var mtime MtimeMapping
-	if err := mtime.Unmarshal(data); err != nil {
-		return MtimeMapping{}, err
-	}
-	return mtime, nil
+	return f.db.GetMtime(f.folderID, name)
 }
 
 // The mtimeFileInfo is an os.FileInfo that lies about the ModTime().
@@ -211,14 +192,11 @@ func (f mtimeFile) Stat() (FileInfo, error) {
 		return nil, err
 	}
 
-	mtimeMapping, err := f.fs.load(f.Name())
-	if err != nil {
-		return nil, err
-	}
-	if mtimeMapping.Real.Equal(info.ModTime()) {
+	ondisk, virtual := f.fs.load(f.Name())
+	if ondisk.Equal(info.ModTime()) {
 		info = mtimeFileInfo{
 			FileInfo: info,
-			mtime:    mtimeMapping.Virtual,
+			mtime:    virtual,
 		}
 	}
 
@@ -230,38 +208,14 @@ func (f mtimeFile) unwrap() File {
 	return f.File
 }
 
-// MtimeMapping represents the mapping as stored in the database
-type MtimeMapping struct {
-	// "Real" is the on disk timestamp
-	Real time.Time `json:"real"`
-	// "Virtual" is what want the timestamp to be
-	Virtual time.Time `json:"virtual"`
-}
-
-func (t *MtimeMapping) Marshal() ([]byte, error) {
-	bs0, _ := t.Real.MarshalBinary()
-	bs1, _ := t.Virtual.MarshalBinary()
-	return append(bs0, bs1...), nil
-}
-
-func (t *MtimeMapping) Unmarshal(bs []byte) error {
-	if err := t.Real.UnmarshalBinary(bs[:len(bs)/2]); err != nil {
-		return err
-	}
-	if err := t.Virtual.UnmarshalBinary(bs[len(bs)/2:]); err != nil {
-		return err
-	}
-	return nil
-}
-
-func GetMtimeMapping(fs Filesystem, file string) (MtimeMapping, error) {
+func GetMtimeMapping(fs Filesystem, file string) (ondisk, virtual time.Time) {
 	fs, ok := unwrapFilesystem(fs, filesystemWrapperTypeMtime)
 	if !ok {
-		return MtimeMapping{}, errors.New("failed to unwrap")
+		return time.Time{}, time.Time{}
 	}
 	mtimeFs, ok := fs.(*mtimeFS)
 	if !ok {
-		return MtimeMapping{}, errors.New("unwrapping failed")
+		return time.Time{}, time.Time{}
 	}
 	return mtimeFs.load(file)
 }
diff --git a/lib/locations/locations.go b/lib/locations/locations.go
index 197c686eb67..3c38c751147 100644
--- a/lib/locations/locations.go
+++ b/lib/locations/locations.go
@@ -22,17 +22,18 @@ type LocationEnum string
 // Use strings as keys to make printout and serialization of the locations map
 // more meaningful.
 const (
-	ConfigFile    LocationEnum = "config"
-	CertFile      LocationEnum = "certFile"
-	KeyFile       LocationEnum = "keyFile"
-	HTTPSCertFile LocationEnum = "httpsCertFile"
-	HTTPSKeyFile  LocationEnum = "httpsKeyFile"
-	Database      LocationEnum = "database"
-	LogFile       LocationEnum = "logFile"
-	PanicLog      LocationEnum = "panicLog"
-	AuditLog      LocationEnum = "auditLog"
-	GUIAssets     LocationEnum = "guiAssets"
-	DefFolder     LocationEnum = "defFolder"
+	ConfigFile     LocationEnum = "config"
+	CertFile       LocationEnum = "certFile"
+	KeyFile        LocationEnum = "keyFile"
+	HTTPSCertFile  LocationEnum = "httpsCertFile"
+	HTTPSKeyFile   LocationEnum = "httpsKeyFile"
+	LegacyDatabase LocationEnum = "legacyDatabase"
+	Database       LocationEnum = "database"
+	LogFile        LocationEnum = "logFile"
+	PanicLog       LocationEnum = "panicLog"
+	AuditLog       LocationEnum = "auditLog"
+	GUIAssets      LocationEnum = "guiAssets"
+	DefFolder      LocationEnum = "defFolder"
 )
 
 type BaseDirEnum string
@@ -46,14 +47,15 @@ const (
 	// User's home directory, *not* --home flag
 	UserHomeBaseDir BaseDirEnum = "userHome"
 
-	LevelDBDir          = "index-v0.14.0.db"
+	levelDBDir          = "index-v0.14.0.db"
+	databaseName        = "index-v2.db"
 	configFileName      = "config.xml"
 	defaultStateDir     = ".local/state/syncthing"
 	oldDefaultConfigDir = ".config/syncthing"
 )
 
 // Platform dependent directories
-var baseDirs = make(map[BaseDirEnum]string, 3)
+var baseDirs = make(map[BaseDirEnum]string)
 
 func init() {
 	userHome := userHomeDir()
@@ -113,17 +115,18 @@ func GetBaseDir(baseDir BaseDirEnum) string {
 
 // Use the variables from baseDirs here
 var locationTemplates = map[LocationEnum]string{
-	ConfigFile:    "${config}/config.xml",
-	CertFile:      "${config}/cert.pem",
-	KeyFile:       "${config}/key.pem",
-	HTTPSCertFile: "${config}/https-cert.pem",
-	HTTPSKeyFile:  "${config}/https-key.pem",
-	Database:      "${data}/" + LevelDBDir,
-	LogFile:       "${data}/syncthing.log", // --logfile on Windows
-	PanicLog:      "${data}/panic-%{timestamp}.log",
-	AuditLog:      "${data}/audit-%{timestamp}.log",
-	GUIAssets:     "${config}/gui",
-	DefFolder:     "${userHome}/Sync",
+	ConfigFile:     "${config}/config.xml",
+	CertFile:       "${config}/cert.pem",
+	KeyFile:        "${config}/key.pem",
+	HTTPSCertFile:  "${config}/https-cert.pem",
+	HTTPSKeyFile:   "${config}/https-key.pem",
+	LegacyDatabase: "${data}/" + levelDBDir,
+	Database:       "${data}/" + databaseName,
+	LogFile:        "${data}/syncthing.log", // --logfile on Windows
+	PanicLog:       "${data}/panic-%{timestamp}.log",
+	AuditLog:       "${data}/audit-%{timestamp}.log",
+	GUIAssets:      "${config}/gui",
+	DefFolder:      "${userHome}/Sync",
 }
 
 var locations = make(map[LocationEnum]string)
@@ -242,7 +245,8 @@ func unixDataDir(userHome, configDir, xdgDataHome, xdgStateHome string, fileExis
 	// If a database exists at the config location, use that. This is the
 	// most common case for both legacy (~/.config/syncthing) and current
 	// (~/.local/state/syncthing) setups.
-	if fileExists(filepath.Join(configDir, LevelDBDir)) {
+	if fileExists(filepath.Join(configDir, databaseName)) ||
+		fileExists(filepath.Join(configDir, levelDBDir)) {
 		return configDir
 	}
 
@@ -251,14 +255,16 @@ func unixDataDir(userHome, configDir, xdgDataHome, xdgStateHome string, fileExis
 	// but that's not what we did previously, so we retain the old behavior.
 	if xdgDataHome != "" {
 		candidate := filepath.Join(xdgDataHome, "syncthing")
-		if fileExists(filepath.Join(candidate, LevelDBDir)) {
+		if fileExists(filepath.Join(candidate, databaseName)) ||
+			fileExists(filepath.Join(candidate, levelDBDir)) {
 			return candidate
 		}
 	}
 
 	// Legacy: if a database exists under ~/.config/syncthing, use that
 	candidate := filepath.Join(userHome, oldDefaultConfigDir)
-	if fileExists(filepath.Join(candidate, LevelDBDir)) {
+	if fileExists(filepath.Join(candidate, databaseName)) ||
+		fileExists(filepath.Join(candidate, levelDBDir)) {
 		return candidate
 	}
 
diff --git a/lib/db/util.go b/lib/model/fileinfobatch.go
similarity index 99%
rename from lib/db/util.go
rename to lib/model/fileinfobatch.go
index 9251f3fdce0..92398848989 100644
--- a/lib/db/util.go
+++ b/lib/model/fileinfobatch.go
@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package db
+package model
 
 import (
 	"github.com/syncthing/syncthing/lib/protocol"
diff --git a/lib/model/folder.go b/lib/model/folder.go
index f8b8c501f06..05a5b103201 100644
--- a/lib/model/folder.go
+++ b/lib/model/folder.go
@@ -15,8 +15,9 @@ import (
 	"sort"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -46,7 +47,7 @@ type folder struct {
 
 	model         *model
 	shortID       protocol.ShortID
-	fset          *db.FileSet
+	db            db.DB
 	ignores       *ignore.Matcher
 	mtimefs       fs.Filesystem
 	modTimeWindow time.Duration
@@ -96,18 +97,18 @@ type puller interface {
 	pull() (bool, error) // true when successful and should not be retried
 }
 
-func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, evLogger events.Logger, ioLimiter *semaphore.Semaphore, ver versioner.Versioner) folder {
+func newFolder(model *model, ignores *ignore.Matcher, cfg config.FolderConfiguration, evLogger events.Logger, ioLimiter *semaphore.Semaphore, ver versioner.Versioner) folder {
 	f := folder{
 		stateTracker:              newStateTracker(cfg.ID, evLogger),
 		FolderConfiguration:       cfg,
-		FolderStatisticsReference: stats.NewFolderStatisticsReference(model.db, cfg.ID),
+		FolderStatisticsReference: stats.NewFolderStatisticsReference(db.NewTyped(model.sdb, "folderstats/"+cfg.ID)),
 		ioLimiter:                 ioLimiter,
 
 		model:         model,
 		shortID:       model.shortID,
-		fset:          fset,
+		db:            model.sdb,
 		ignores:       ignores,
-		mtimefs:       cfg.Filesystem(fset),
+		mtimefs:       cfg.Filesystem(fs.NewMtimeOption(model.sdb, cfg.ID)),
 		modTimeWindow: cfg.ModTimeWindow(),
 		done:          make(chan struct{}),
 
@@ -367,17 +368,11 @@ func (f *folder) pull() (success bool, err error) {
 	}()
 
 	// If there is nothing to do, don't even enter sync-waiting state.
-	abort := true
-	snap, err := f.dbSnapshot()
+	needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID)
 	if err != nil {
 		return false, err
 	}
-	snap.WithNeed(protocol.LocalDeviceID, func(intf protocol.FileInfo) bool {
-		abort = false
-		return false
-	})
-	snap.Release()
-	if abort {
+	if needCount.TotalItems() == 0 {
 		// Clears pull failures on items that were needed before, but aren't anymore.
 		f.errorsMut.Lock()
 		f.pullErrors = nil
@@ -484,15 +479,10 @@ func (f *folder) scanSubdirs(subDirs []string) error {
 	// Clean the list of subitems to ensure that we start at a known
 	// directory, and don't scan subdirectories of things we've already
 	// scanned.
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return err
-	}
 	subDirs = unifySubs(subDirs, func(file string) bool {
-		_, ok := snap.Get(protocol.LocalDeviceID, file)
-		return ok
+		_, ok, err := f.db.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file)
+		return err == nil && ok
 	})
-	snap.Release()
 
 	f.setState(FolderScanning)
 	f.clearScanErrors(subDirs)
@@ -546,7 +536,7 @@ const maxToRemove = 1000
 
 type scanBatch struct {
 	f           *folder
-	updateBatch *db.FileInfoBatch
+	updateBatch *FileInfoBatch
 	toRemove    []string
 }
 
@@ -555,7 +545,7 @@ func (f *folder) newScanBatch() *scanBatch {
 		f:        f,
 		toRemove: make([]string, 0, maxToRemove),
 	}
-	b.updateBatch = db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
+	b.updateBatch = NewFileInfoBatch(func(fs []protocol.FileInfo) error {
 		if err := b.f.getHealthErrorWithoutIgnores(); err != nil {
 			l.Debugf("Stopping scan of folder %s due to: %s", b.f.Description(), err)
 			return err
@@ -570,46 +560,56 @@ func (b *scanBatch) Remove(item string) {
 	b.toRemove = append(b.toRemove, item)
 }
 
-func (b *scanBatch) flushToRemove() {
+func (b *scanBatch) flushToRemove() error {
 	if len(b.toRemove) > 0 {
-		b.f.fset.RemoveLocalItems(b.toRemove)
+		if err := b.f.db.DropFilesNamed(b.f.folderID, protocol.LocalDeviceID, b.toRemove); err != nil {
+			return err
+		}
 		b.toRemove = b.toRemove[:0]
 	}
+	return nil
 }
 
 func (b *scanBatch) Flush() error {
-	b.flushToRemove()
+	if err := b.flushToRemove(); err != nil {
+		return err
+	}
 	return b.updateBatch.Flush()
 }
 
 func (b *scanBatch) FlushIfFull() error {
 	if len(b.toRemove) >= maxToRemove {
-		b.flushToRemove()
+		if err := b.flushToRemove(); err != nil {
+			return err
+		}
 	}
 	return b.updateBatch.FlushIfFull()
 }
 
 // Update adds the fileinfo to the batch for updating, and does a few checks.
 // It returns false if the checks result in the file not going to be updated or removed.
-func (b *scanBatch) Update(fi protocol.FileInfo, snap *db.Snapshot) bool {
+func (b *scanBatch) Update(fi protocol.FileInfo) (bool, error) {
 	// Check for a "virtual" parent directory of encrypted files. We don't track
 	// it, but check if anything still exists within and delete it otherwise.
 	if b.f.Type == config.FolderTypeReceiveEncrypted && fi.IsDirectory() && protocol.IsEncryptedParent(fs.PathComponents(fi.Name)) {
 		if names, err := b.f.mtimefs.DirNames(fi.Name); err == nil && len(names) == 0 {
 			b.f.mtimefs.Remove(fi.Name)
 		}
-		return false
+		return false, nil
 	}
 	// Resolve receive-only items which are identical with the global state or
 	// the global item is our own receive-only item.
-	switch gf, ok := snap.GetGlobal(fi.Name); {
+	switch gf, ok, err := b.f.db.GetGlobalFile(b.f.folderID, fi.Name); {
+	case err != nil:
+		return false, err
 	case !ok:
 	case gf.IsReceiveOnlyChanged():
 		if fi.IsDeleted() {
 			// Our item is deleted and the global item is our own receive only
 			// file. No point in keeping track of that.
 			b.Remove(fi.Name)
-			return true
+			l.Debugf("%v scanning: deleting deleted receive-only local-changed file: %v", b.f, fi)
+			return true, nil
 		}
 	case (b.f.Type == config.FolderTypeReceiveOnly || b.f.Type == config.FolderTypeReceiveEncrypted) &&
 		gf.IsEquivalentOptional(fi, protocol.FileInfoComparison{
@@ -621,20 +621,15 @@ func (b *scanBatch) Update(fi protocol.FileInfo, snap *db.Snapshot) bool {
 			IgnoreXattrs:    !b.f.SyncXattrs && !b.f.SendXattrs,
 		}):
 		// What we have locally is equivalent to the global file.
-		l.Debugf("%v scanning: Merging identical locally changed item with global", b.f, fi)
+		l.Debugf("%v scanning: Merging identical locally changed item with global: %v", b.f, fi)
 		fi = gf
 	}
 	b.updateBatch.Append(fi)
-	return true
+	return true, nil
 }
 
 func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (int, error) {
 	changes := 0
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return changes, err
-	}
-	defer snap.Release()
 
 	// If we return early e.g. due to a folder health error, the scan needs
 	// to be cancelled.
@@ -646,7 +641,7 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
 		Subs:                  subDirs,
 		Matcher:               f.ignores,
 		TempLifetime:          time.Duration(f.model.cfg.Options().KeepTemporariesH) * time.Hour,
-		CurrentFiler:          cFiler{snap},
+		CurrentFiler:          cFiler{db: f.db, folder: f.folderID},
 		Filesystem:            f.mtimefs,
 		IgnorePerms:           f.IgnorePerms,
 		AutoNormalize:         f.AutoNormalize,
@@ -683,15 +678,19 @@ func (f *folder) scanSubdirsChangedAndNew(subDirs []string, batch *scanBatch) (i
 			return changes, err
 		}
 
-		if batch.Update(res.File, snap) {
+		if ok, err := batch.Update(res.File); err != nil {
+			return 0, err
+		} else if ok {
 			changes++
 		}
 
 		switch f.Type {
 		case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
 		default:
-			if nf, ok := f.findRename(snap, res.File, alreadyUsedOrExisting); ok {
-				if batch.Update(nf, snap) {
+			if nf, ok := f.findRename(res.File, alreadyUsedOrExisting); ok {
+				if ok, err := batch.Update(nf); err != nil {
+					return 0, err
+				} else if ok {
 					changes++
 				}
 			}
@@ -705,25 +704,22 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 	var toIgnore []protocol.FileInfo
 	ignoredParent := ""
 	changes := 0
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return 0, err
-	}
-	defer snap.Release()
 
+outer:
 	for _, sub := range subDirs {
-		var iterError error
+		for fi, err := range itererr.Zip(f.db.AllLocalFilesWithPrefix(f.folderID, protocol.LocalDeviceID, sub)) {
+			if err != nil {
+				return changes, err
+			}
 
-		snap.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi protocol.FileInfo) bool {
 			select {
 			case <-f.ctx.Done():
-				return false
+				break outer
 			default:
 			}
 
 			if err := batch.FlushIfFull(); err != nil {
-				iterError = err
-				return false
+				return 0, err
 			}
 
 			if ignoredParent != "" && !fs.IsParent(fi.Name, ignoredParent) {
@@ -731,12 +727,13 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 					l.Debugln("marking file as ignored", file)
 					nf := file
 					nf.SetIgnored()
-					if batch.Update(nf, snap) {
+					if ok, err := batch.Update(nf); err != nil {
+						return 0, err
+					} else if ok {
 						changes++
 					}
 					if err := batch.FlushIfFull(); err != nil {
-						iterError = err
-						return false
+						return 0, err
 					}
 				}
 				toIgnore = toIgnore[:0]
@@ -745,7 +742,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 
 			switch ignored := f.ignores.Match(fi.Name).IsIgnored(); {
 			case fi.IsIgnored() && ignored:
-				return true
+				continue
 			case !fi.IsIgnored() && ignored:
 				// File was not ignored at last pass but has been ignored.
 				if fi.IsDirectory() {
@@ -756,13 +753,15 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 						// this path as the "highest" ignored parent
 						ignoredParent = fi.Name
 					}
-					return true
+					continue
 				}
 
 				l.Debugln("marking file as ignored", fi)
 				nf := fi
 				nf.SetIgnored()
-				if batch.Update(nf, snap) {
+				if ok, err := batch.Update(nf); err != nil {
+					return 0, err
+				} else if ok {
 					changes++
 				}
 
@@ -781,7 +780,7 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 						toIgnore = toIgnore[:0]
 						ignoredParent = ""
 					}
-					return true
+					continue
 				}
 				nf := fi
 				nf.SetDeleted(f.shortID)
@@ -793,13 +792,17 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 					nf.Version = protocol.Vector{}
 				}
 				l.Debugln("marking file as deleted", nf)
-				if batch.Update(nf, snap) {
+				if ok, err := batch.Update(nf); err != nil {
+					return 0, err
+				} else if ok {
 					changes++
 				}
 			case fi.IsDeleted() && fi.IsReceiveOnlyChanged():
 				switch f.Type {
 				case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
-					switch gf, ok := snap.GetGlobal(fi.Name); {
+					switch gf, ok, err := f.db.GetGlobalFile(f.folderID, fi.Name); {
+					case err != nil:
+						return 0, err
 					case !ok:
 					case gf.IsReceiveOnlyChanged():
 						l.Debugln("removing deleted, receive-only item that is globally receive-only from db", fi)
@@ -810,7 +813,9 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 						// pretend it is a normal deleted file (nobody cares about that).
 						l.Debugf("%v scanning: Marking globally deleted item as not locally changed: %v", f, fi.Name)
 						fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
-						if batch.Update(fi, snap) {
+						if ok, err := batch.Update(fi); err != nil {
+							return 0, err
+						} else if ok {
 							changes++
 						}
 					}
@@ -819,14 +824,14 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 					// deleted and just the folder type/local flags changed.
 					fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
 					l.Debugln("removing receive-only flag on deleted item", fi)
-					if batch.Update(fi, snap) {
+					if ok, err := batch.Update(fi); err != nil {
+						return 0, err
+					} else if ok {
 						changes++
 					}
 				}
 			}
-
-			return true
-		})
+		}
 
 		select {
 		case <-f.ctx.Done():
@@ -834,30 +839,28 @@ func (f *folder) scanSubdirsDeletedAndIgnored(subDirs []string, batch *scanBatch
 		default:
 		}
 
-		if iterError == nil && len(toIgnore) > 0 {
+		if len(toIgnore) > 0 {
 			for _, file := range toIgnore {
 				l.Debugln("marking file as ignored", file)
 				nf := file
 				nf.SetIgnored()
-				if batch.Update(nf, snap) {
+				if ok, err := batch.Update(nf); err != nil {
+					return 0, err
+				} else if ok {
 					changes++
 				}
-				if iterError = batch.FlushIfFull(); iterError != nil {
-					break
+				if err := batch.FlushIfFull(); err != nil {
+					return 0, err
 				}
 			}
 			toIgnore = toIgnore[:0]
 		}
-
-		if iterError != nil {
-			return changes, iterError
-		}
 	}
 
 	return changes, nil
 }
 
-func (f *folder) findRename(snap *db.Snapshot, file protocol.FileInfo, alreadyUsedOrExisting map[string]struct{}) (protocol.FileInfo, bool) {
+func (f *folder) findRename(file protocol.FileInfo, alreadyUsedOrExisting map[string]struct{}) (protocol.FileInfo, bool) {
 	if len(file.Blocks) == 0 || file.Size == 0 {
 		return protocol.FileInfo{}, false
 	}
@@ -865,49 +868,58 @@ func (f *folder) findRename(snap *db.Snapshot, file protocol.FileInfo, alreadyUs
 	found := false
 	nf := protocol.FileInfo{}
 
-	snap.WithBlocksHash(file.BlocksHash, func(fi protocol.FileInfo) bool {
+loop:
+	for fi, err := range itererr.Zip(f.db.AllLocalFilesWithBlocksHash(f.folderID, file.BlocksHash)) {
+		if err != nil {
+			return protocol.FileInfo{}, false
+		}
+
 		select {
 		case <-f.ctx.Done():
-			return false
+			break loop
 		default:
 		}
 
 		if fi.Name == file.Name {
 			alreadyUsedOrExisting[fi.Name] = struct{}{}
-			return true
+			continue
 		}
 
 		if _, ok := alreadyUsedOrExisting[fi.Name]; ok {
-			return true
+			continue
 		}
 
 		if fi.ShouldConflict() {
-			return true
+			continue
 		}
 
 		if f.ignores.Match(fi.Name).IsIgnored() {
-			return true
+			continue
 		}
 
 		// Only check the size.
 		// No point checking block equality, as that uses BlocksHash comparison if that is set (which it will be).
 		// No point checking BlocksHash comparison as WithBlocksHash already does that.
 		if file.Size != fi.Size {
-			return true
+			continue
 		}
 
 		alreadyUsedOrExisting[fi.Name] = struct{}{}
 
 		if !osutil.IsDeleted(f.mtimefs, fi.Name) {
-			return true
+			continue
 		}
 
-		nf = fi
+		var ok bool
+		nf, ok, err = f.db.GetDeviceFile(f.folderID, protocol.LocalDeviceID, fi.Name)
+		if err != nil || !ok || nf.Sequence != fi.Sequence {
+			continue
+		}
 		nf.SetDeleted(f.shortID)
 		nf.LocalFlags = f.localFlags
 		found = true
-		return false
-	})
+		break
+	}
 
 	return nf, found
 }
@@ -1216,20 +1228,26 @@ func (f *folder) ScheduleForceRescan(path string) {
 	}
 }
 
-func (f *folder) updateLocalsFromScanning(fs []protocol.FileInfo) {
-	f.updateLocals(fs)
-
+func (f *folder) updateLocalsFromScanning(fs []protocol.FileInfo) error {
+	if err := f.updateLocals(fs); err != nil {
+		return err
+	}
 	f.emitDiskChangeEvents(fs, events.LocalChangeDetected)
+	return nil
 }
 
-func (f *folder) updateLocalsFromPulling(fs []protocol.FileInfo) {
-	f.updateLocals(fs)
-
+func (f *folder) updateLocalsFromPulling(fs []protocol.FileInfo) error {
+	if err := f.updateLocals(fs); err != nil {
+		return err
+	}
 	f.emitDiskChangeEvents(fs, events.RemoteChangeDetected)
+	return nil
 }
 
-func (f *folder) updateLocals(fs []protocol.FileInfo) {
-	f.fset.Update(protocol.LocalDeviceID, fs)
+func (f *folder) updateLocals(fs []protocol.FileInfo) error {
+	if err := f.db.Update(f.folderID, protocol.LocalDeviceID, fs); err != nil {
+		return err
+	}
 
 	filenames := make([]string, len(fs))
 	f.forcedRescanPathsMut.Lock()
@@ -1240,7 +1258,10 @@ func (f *folder) updateLocals(fs []protocol.FileInfo) {
 	}
 	f.forcedRescanPathsMut.Unlock()
 
-	seq := f.fset.Sequence(protocol.LocalDeviceID)
+	seq, err := f.db.GetDeviceSequence(f.folderID, protocol.LocalDeviceID)
+	if err != nil {
+		return err
+	}
 	f.evLogger.Log(events.LocalIndexUpdated, map[string]interface{}{
 		"folder":    f.ID,
 		"items":     len(fs),
@@ -1248,6 +1269,7 @@ func (f *folder) updateLocals(fs []protocol.FileInfo) {
 		"sequence":  seq,
 		"version":   seq, // legacy for sequence
 	})
+	return nil
 }
 
 func (f *folder) emitDiskChangeEvents(fs []protocol.FileInfo, typeOfEvent events.EventType) {
@@ -1294,23 +1316,19 @@ func (f *folder) handleForcedRescans() error {
 		return nil
 	}
 
-	batch := db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
-		f.fset.Update(protocol.LocalDeviceID, fs)
-		return nil
+	batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
+		return f.db.Update(f.folderID, protocol.LocalDeviceID, fs)
 	})
 
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return err
-	}
-	defer snap.Release()
-
 	for _, path := range paths {
 		if err := batch.FlushIfFull(); err != nil {
 			return err
 		}
 
-		fi, ok := snap.Get(protocol.LocalDeviceID, path)
+		fi, ok, err := f.db.GetDeviceFile(f.folderID, protocol.LocalDeviceID, path)
+		if err != nil {
+			return err
+		}
 		if !ok {
 			continue
 		}
@@ -1318,23 +1336,13 @@ func (f *folder) handleForcedRescans() error {
 		batch.Append(fi)
 	}
 
-	if err = batch.Flush(); err != nil {
+	if err := batch.Flush(); err != nil {
 		return err
 	}
 
 	return f.scanSubdirs(paths)
 }
 
-// dbSnapshots gets a snapshot from the fileset, and wraps any error
-// in a svcutil.FatalErr.
-func (f *folder) dbSnapshot() (*db.Snapshot, error) {
-	snap, err := f.fset.Snapshot()
-	if err != nil {
-		return nil, svcutil.AsFatalErr(err, svcutil.ExitError)
-	}
-	return snap, nil
-}
-
 // The exists function is expected to return true for all known paths
 // (excluding "" and ".")
 func unifySubs(dirs []string, exists func(dir string) bool) []string {
@@ -1370,10 +1378,15 @@ func unifySubs(dirs []string, exists func(dir string) bool) []string {
 }
 
 type cFiler struct {
-	*db.Snapshot
+	db     db.DB
+	folder string
 }
 
 // Implements scanner.CurrentFiler
 func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
-	return cf.Get(protocol.LocalDeviceID, file)
+	fi, ok, err := cf.db.GetDeviceFile(cf.folder, protocol.LocalDeviceID, file)
+	if err != nil || !ok {
+		return protocol.FileInfo{}, false
+	}
+	return fi, true
 }
diff --git a/lib/model/folder_recvenc.go b/lib/model/folder_recvenc.go
index 2892be116b1..69b1c57711e 100644
--- a/lib/model/folder_recvenc.go
+++ b/lib/model/folder_recvenc.go
@@ -10,8 +10,8 @@ import (
 	"fmt"
 	"sort"
 
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -28,8 +28,8 @@ type receiveEncryptedFolder struct {
 	*sendReceiveFolder
 }
 
-func newReceiveEncryptedFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
-	f := &receiveEncryptedFolder{newSendReceiveFolder(model, fset, ignores, cfg, ver, evLogger, ioLimiter).(*sendReceiveFolder)}
+func newReceiveEncryptedFolder(model *model, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
+	f := &receiveEncryptedFolder{newSendReceiveFolder(model, ignores, cfg, ver, evLogger, ioLimiter).(*sendReceiveFolder)}
 	f.localFlags = protocol.FlagLocalReceiveOnly // gets propagated to the scanner, and set on locally changed files
 	return f
 }
@@ -44,30 +44,27 @@ func (f *receiveEncryptedFolder) revert() error {
 	f.setState(FolderScanning)
 	defer f.setState(FolderIdle)
 
-	batch := db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
+	batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
 		f.updateLocalsFromScanning(fs)
 		return nil
 	})
 
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return err
-	}
-	defer snap.Release()
-	var iterErr error
 	var dirs []string
-	snap.WithHaveTruncated(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-		if iterErr = batch.FlushIfFull(); iterErr != nil {
-			return false
+	for fi, err := range itererr.Zip(f.db.AllLocalFiles(f.folderID, protocol.LocalDeviceID)) {
+		if err != nil {
+			return err
+		}
+		if err := batch.FlushIfFull(); err != nil {
+			return err
 		}
 
 		if !fi.IsReceiveOnlyChanged() || fi.IsDeleted() {
-			return true
+			continue
 		}
 
 		if fi.IsDirectory() {
 			dirs = append(dirs, fi.Name)
-			return true
+			continue
 		}
 
 		if err := f.inWritableDir(f.mtimefs.Remove, fi.Name); err != nil && !fs.IsNotExist(err) {
@@ -84,15 +81,10 @@ func (f *receiveEncryptedFolder) revert() error {
 		// deleted, it will not show up as an unexpected file in the UI
 		// anymore.
 		batch.Append(fi)
+	}
 
-		return true
-	})
-
-	f.revertHandleDirs(dirs, snap)
+	f.revertHandleDirs(dirs)
 
-	if iterErr != nil {
-		return iterErr
-	}
 	if err := batch.Flush(); err != nil {
 		return err
 	}
@@ -103,7 +95,7 @@ func (f *receiveEncryptedFolder) revert() error {
 	return nil
 }
 
-func (f *receiveEncryptedFolder) revertHandleDirs(dirs []string, snap *db.Snapshot) {
+func (f *receiveEncryptedFolder) revertHandleDirs(dirs []string) {
 	if len(dirs) == 0 {
 		return
 	}
@@ -114,7 +106,7 @@ func (f *receiveEncryptedFolder) revertHandleDirs(dirs []string, snap *db.Snapsh
 
 	sort.Sort(sort.Reverse(sort.StringSlice(dirs)))
 	for _, dir := range dirs {
-		if err := f.deleteDirOnDisk(dir, snap, scanChan); err != nil {
+		if err := f.deleteDirOnDisk(dir, scanChan); err != nil {
 			f.newScanError(dir, fmt.Errorf("deleting unexpected dir: %w", err))
 		}
 		scanChan <- dir
diff --git a/lib/model/folder_recvonly.go b/lib/model/folder_recvonly.go
index 501a61e3447..535483c558a 100644
--- a/lib/model/folder_recvonly.go
+++ b/lib/model/folder_recvonly.go
@@ -10,8 +10,8 @@ import (
 	"sort"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/ignore"
 	"github.com/syncthing/syncthing/lib/protocol"
@@ -57,8 +57,8 @@ type receiveOnlyFolder struct {
 	*sendReceiveFolder
 }
 
-func newReceiveOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
-	sr := newSendReceiveFolder(model, fset, ignores, cfg, ver, evLogger, ioLimiter).(*sendReceiveFolder)
+func newReceiveOnlyFolder(model *model, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
+	sr := newSendReceiveFolder(model, ignores, cfg, ver, evLogger, ioLimiter).(*sendReceiveFolder)
 	sr.localFlags = protocol.FlagLocalReceiveOnly // gets propagated to the scanner, and set on locally changed files
 	return &receiveOnlyFolder{sr}
 }
@@ -83,30 +83,31 @@ func (f *receiveOnlyFolder) revert() error {
 		scanChan: scanChan,
 	}
 
-	batch := db.NewFileInfoBatch(func(files []protocol.FileInfo) error {
+	batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
 		f.updateLocalsFromScanning(files)
 		return nil
 	})
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return err
-	}
-	defer snap.Release()
-	snap.WithHave(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
+
+	for fi, err := range itererr.Zip(f.db.AllLocalFiles(f.folderID, protocol.LocalDeviceID)) {
+		if err != nil {
+			return err
+		}
 		if !fi.IsReceiveOnlyChanged() {
 			// We're only interested in files that have changed locally in
 			// receive only mode.
-			return true
+			continue
 		}
 
 		fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
 
-		switch gf, ok := snap.GetGlobal(fi.Name); {
+		switch gf, ok, err := f.db.GetGlobalFile(f.folderID, fi.Name); {
+		case err != nil:
+			return err
 		case !ok:
-			msg := "Unexpected global file that we have locally"
+			msg := "Unexpectedly missing global file that we have locally"
 			l.Debugf("%v revert: %v: %v", f, msg, fi.Name)
 			f.evLogger.Log(events.Failure, msg)
-			return true
+			continue
 		case gf.IsReceiveOnlyChanged():
 			// The global file is our own. A revert then means to delete it.
 			// We'll delete files directly, directories get queued and
@@ -115,13 +116,13 @@ func (f *receiveOnlyFolder) revert() error {
 				fi.Version = protocol.Vector{} // if this file ever resurfaces anywhere we want our delete to be strictly older
 				break
 			}
-			handled, err := delQueue.handle(fi, snap)
+			l.Debugf("Revert: deleting %s: %v\n", fi.Name, err)
+			handled, err := delQueue.handle(fi)
 			if err != nil {
-				l.Infof("Revert: deleting %s: %v\n", fi.Name, err)
-				return true // continue
+				continue
 			}
 			if !handled {
-				return true // continue
+				continue
 			}
 			fi.SetDeleted(f.shortID)
 			fi.Version = protocol.Vector{} // if this file ever resurfaces anywhere we want our delete to be strictly older
@@ -144,13 +145,13 @@ func (f *receiveOnlyFolder) revert() error {
 
 		batch.Append(fi)
 		_ = batch.FlushIfFull()
-
-		return true
-	})
-	_ = batch.Flush()
+	}
+	if err := batch.Flush(); err != nil {
+		return err
+	}
 
 	// Handle any queued directories
-	deleted, err := delQueue.flush(snap)
+	deleted, err := delQueue.flush()
 	if err != nil {
 		l.Infoln("Revert:", err)
 	}
@@ -179,15 +180,15 @@ func (f *receiveOnlyFolder) revert() error {
 // directories for last.
 type deleteQueue struct {
 	handler interface {
-		deleteItemOnDisk(item protocol.FileInfo, snap *db.Snapshot, scanChan chan<- string) error
-		deleteDirOnDisk(dir string, snap *db.Snapshot, scanChan chan<- string) error
+		deleteItemOnDisk(item protocol.FileInfo, scanChan chan<- string) error
+		deleteDirOnDisk(dir string, scanChan chan<- string) error
 	}
 	ignores  *ignore.Matcher
 	dirs     []string
 	scanChan chan<- string
 }
 
-func (q *deleteQueue) handle(fi protocol.FileInfo, snap *db.Snapshot) (bool, error) {
+func (q *deleteQueue) handle(fi protocol.FileInfo) (bool, error) {
 	// Things that are ignored but not marked deletable are not processed.
 	ign := q.ignores.Match(fi.Name)
 	if ign.IsIgnored() && !ign.IsDeletable() {
@@ -201,11 +202,11 @@ func (q *deleteQueue) handle(fi protocol.FileInfo, snap *db.Snapshot) (bool, err
 	}
 
 	// Kill it.
-	err := q.handler.deleteItemOnDisk(fi, snap, q.scanChan)
+	err := q.handler.deleteItemOnDisk(fi, q.scanChan)
 	return true, err
 }
 
-func (q *deleteQueue) flush(snap *db.Snapshot) ([]string, error) {
+func (q *deleteQueue) flush() ([]string, error) {
 	// Process directories from the leaves inward.
 	sort.Sort(sort.Reverse(sort.StringSlice(q.dirs)))
 
@@ -213,7 +214,7 @@ func (q *deleteQueue) flush(snap *db.Snapshot) ([]string, error) {
 	var deleted []string
 
 	for _, dir := range q.dirs {
-		if err := q.handler.deleteDirOnDisk(dir, snap, q.scanChan); err == nil {
+		if err := q.handler.deleteDirOnDisk(dir, q.scanChan); err == nil {
 			deleted = append(deleted, dir)
 		} else if firstError == nil {
 			firstError = err
diff --git a/lib/model/folder_sendonly.go b/lib/model/folder_sendonly.go
index 4d5d36b19b5..228452d2e9e 100644
--- a/lib/model/folder_sendonly.go
+++ b/lib/model/folder_sendonly.go
@@ -7,8 +7,8 @@
 package model
 
 import (
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/ignore"
 	"github.com/syncthing/syncthing/lib/protocol"
@@ -24,9 +24,9 @@ type sendOnlyFolder struct {
 	folder
 }
 
-func newSendOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, _ versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
+func newSendOnlyFolder(model *model, ignores *ignore.Matcher, cfg config.FolderConfiguration, _ versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
 	f := &sendOnlyFolder{
-		folder: newFolder(model, fset, ignores, cfg, evLogger, ioLimiter, nil),
+		folder: newFolder(model, ignores, cfg, evLogger, ioLimiter, nil),
 	}
 	f.folder.puller = f
 	return f
@@ -38,36 +38,36 @@ func (*sendOnlyFolder) PullErrors() []FileError {
 
 // pull checks need for files that only differ by metadata (no changes on disk)
 func (f *sendOnlyFolder) pull() (bool, error) {
-	batch := db.NewFileInfoBatch(func(files []protocol.FileInfo) error {
+	batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
 		f.updateLocalsFromPulling(files)
 		return nil
 	})
 
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return false, err
-	}
-	defer snap.Release()
-	snap.WithNeed(protocol.LocalDeviceID, func(file protocol.FileInfo) bool {
-		batch.FlushIfFull()
+	for file, err := range itererr.Zip(f.db.AllNeededGlobalFiles(f.folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)) {
+		if err != nil {
+			return false, err
+		}
+		if err := batch.FlushIfFull(); err != nil {
+			return false, err
+		}
 
 		if f.ignores.Match(file.FileName()).IsIgnored() {
 			file.SetIgnored()
 			batch.Append(file)
 			l.Debugln(f, "Handling ignored file", file)
-			return true
+			continue
 		}
 
-		curFile, ok := snap.Get(protocol.LocalDeviceID, file.FileName())
+		curFile, ok, err := f.db.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.FileName())
+		if err != nil {
+			return false, err
+		}
 		if !ok {
-			if file.IsInvalid() {
-				// Global invalid file just exists for need accounting
+			if file.IsInvalid() || file.IsDeleted() {
+				// Accept the file for accounting purposes
 				batch.Append(file)
-			} else if file.IsDeleted() {
-				l.Debugln("Should never get a deleted file as needed when we don't have it")
-				f.evLogger.Log(events.Failure, "got deleted file that doesn't exist locally as needed when pulling on send-only")
 			}
-			return true
+			continue
 		}
 
 		if !file.IsEquivalentOptional(curFile, protocol.FileInfoComparison{
@@ -76,14 +76,12 @@ func (f *sendOnlyFolder) pull() (bool, error) {
 			IgnoreOwnership: !f.SyncOwnership,
 			IgnoreXattrs:    !f.SyncXattrs,
 		}) {
-			return true
+			continue
 		}
 
 		batch.Append(file)
 		l.Debugln(f, "Merging versions of identical file", file)
-
-		return true
-	})
+	}
 
 	batch.Flush()
 
@@ -100,25 +98,31 @@ func (f *sendOnlyFolder) override() error {
 	f.setState(FolderScanning)
 	defer f.setState(FolderIdle)
 
-	batch := db.NewFileInfoBatch(func(files []protocol.FileInfo) error {
+	batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
 		f.updateLocalsFromScanning(files)
 		return nil
 	})
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return err
-	}
-	defer snap.Release()
-	snap.WithNeed(protocol.LocalDeviceID, func(need protocol.FileInfo) bool {
-		_ = batch.FlushIfFull()
 
-		have, ok := snap.Get(protocol.LocalDeviceID, need.Name)
+	for need, err := range itererr.Zip(f.db.AllNeededGlobalFiles(f.folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)) {
+		if err != nil {
+			return err
+		}
+		if err := batch.FlushIfFull(); err != nil {
+			return err
+		}
+
+		have, haveOk, err := f.db.GetDeviceFile(f.folderID, protocol.LocalDeviceID, need.Name)
+		if err != nil {
+			return err
+		}
+
 		// Don't override files that are in a bad state (ignored,
 		// unsupported, must rescan, ...).
-		if ok && have.IsInvalid() {
-			return true
+		if haveOk && have.IsInvalid() {
+			continue
 		}
-		if !ok || have.Name != need.Name {
+
+		if !haveOk || have.Name != need.Name {
 			// We are missing the file
 			need.SetDeleted(f.shortID)
 		} else {
@@ -128,7 +132,6 @@ func (f *sendOnlyFolder) override() error {
 		}
 		need.Sequence = 0
 		batch.Append(need)
-		return true
-	})
+	}
 	return batch.Flush()
 }
diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go
index 8f6544126ee..5011cf9cdc6 100644
--- a/lib/model/folder_sendrecv.go
+++ b/lib/model/folder_sendrecv.go
@@ -19,9 +19,9 @@ import (
 	"strings"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -129,9 +129,9 @@ type sendReceiveFolder struct {
 	tempPullErrors map[string]string // pull errors that might be just transient
 }
 
-func newSendReceiveFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
+func newSendReceiveFolder(model *model, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, evLogger events.Logger, ioLimiter *semaphore.Semaphore) service {
 	f := &sendReceiveFolder{
-		folder:             newFolder(model, fset, ignores, cfg, evLogger, ioLimiter, ver),
+		folder:             newFolder(model, ignores, cfg, evLogger, ioLimiter, ver),
 		queue:              newJobQueue(),
 		blockPullReorderer: newBlockPullReorderer(cfg.BlockPullOrder, model.id, cfg.DeviceIDs()),
 		writeLimiter:       semaphore.New(cfg.MaxConcurrentWrites),
@@ -240,12 +240,6 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) (int, error)
 	f.tempPullErrors = make(map[string]string)
 	f.errorsMut.Unlock()
 
-	snap, err := f.dbSnapshot()
-	if err != nil {
-		return 0, err
-	}
-	defer snap.Release()
-
 	pullChan := make(chan pullBlockState)
 	copyChan := make(chan copyBlocksState)
 	finisherChan := make(chan *sharedPullerState)
@@ -277,18 +271,18 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) (int, error)
 	pullWg.Add(1)
 	go func() {
 		// pullerRoutine finishes when pullChan is closed
-		f.pullerRoutine(snap, pullChan, finisherChan)
+		f.pullerRoutine(pullChan, finisherChan)
 		pullWg.Done()
 	}()
 
 	doneWg.Add(1)
 	// finisherRoutine finishes when finisherChan is closed
 	go func() {
-		f.finisherRoutine(snap, finisherChan, dbUpdateChan, scanChan)
+		f.finisherRoutine(finisherChan, dbUpdateChan, scanChan)
 		doneWg.Done()
 	}()
 
-	changed, fileDeletions, dirDeletions, err := f.processNeeded(snap, dbUpdateChan, copyChan, scanChan)
+	changed, fileDeletions, dirDeletions, err := f.processNeeded(dbUpdateChan, copyChan, scanChan)
 
 	// Signal copy and puller routines that we are done with the in data for
 	// this iteration. Wait for them to finish.
@@ -303,7 +297,7 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) (int, error)
 	doneWg.Wait()
 
 	if err == nil {
-		f.processDeletions(fileDeletions, dirDeletions, snap, dbUpdateChan, scanChan)
+		f.processDeletions(fileDeletions, dirDeletions, dbUpdateChan, scanChan)
 	}
 
 	// Wait for db updates and scan scheduling to complete
@@ -315,7 +309,7 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) (int, error)
 	return changed, err
 }
 
-func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, copyChan chan<- copyBlocksState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
+func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyChan chan<- copyBlocksState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
 	changed := 0
 	var dirDeletions []protocol.FileInfo
 	fileDeletions := map[string]protocol.FileInfo{}
@@ -325,16 +319,20 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 	// Regular files to pull goes into the file queue, everything else
 	// (directories, symlinks and deletes) goes into the "process directly"
 	// pile.
-	snap.WithNeed(protocol.LocalDeviceID, func(file protocol.FileInfo) bool {
+loop:
+	for file, err := range itererr.Zip(f.model.sdb.AllNeededGlobalFiles(f.folderID, protocol.LocalDeviceID, f.Order, 0, 0)) {
+		if err != nil {
+			return changed, nil, nil, err
+		}
 		select {
 		case <-f.ctx.Done():
-			return false
+			break loop
 		default:
 		}
 
 		if f.IgnoreDelete && file.IsDeleted() {
 			l.Debugln(f, "ignore file deletion (config)", file.FileName())
-			return true
+			continue
 		}
 
 		changed++
@@ -366,9 +364,12 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 				// files to delete inside them before we get to that point.
 				dirDeletions = append(dirDeletions, file)
 			} else if file.IsSymlink() {
-				f.deleteFile(file, snap, dbUpdateChan, scanChan)
+				f.deleteFile(file, dbUpdateChan, scanChan)
 			} else {
-				df, ok := snap.Get(protocol.LocalDeviceID, file.Name)
+				df, ok, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+				if err != nil {
+					return changed, nil, nil, err
+				}
 				// Local file can be already deleted, but with a lower version
 				// number, hence the deletion coming in again as part of
 				// WithNeed, furthermore, the file can simply be of the wrong
@@ -384,7 +385,10 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 			}
 
 		case file.Type == protocol.FileInfoTypeFile:
-			curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
+			curFile, hasCurFile, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+			if err != nil {
+				return changed, nil, nil, err
+			}
 			if hasCurFile && file.BlocksEqual(curFile) {
 				// We are supposed to copy the entire file, and then fetch nothing. We
 				// are only updating metadata, so we don't actually *need* to make the
@@ -396,7 +400,7 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 			}
 
 		case (build.IsWindows || build.IsAndroid) && file.IsSymlink():
-			if err := f.handleSymlinkCheckExisting(file, snap, scanChan); err != nil {
+			if err := f.handleSymlinkCheckExisting(file, scanChan); err != nil {
 				f.newPullError(file.Name, fmt.Errorf("handling unsupported symlink: %w", err))
 				break
 			}
@@ -407,22 +411,20 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 		case file.IsDirectory() && !file.IsSymlink():
 			l.Debugln(f, "Handling directory", file.Name)
 			if f.checkParent(file.Name, scanChan) {
-				f.handleDir(file, snap, dbUpdateChan, scanChan)
+				f.handleDir(file, dbUpdateChan, scanChan)
 			}
 
 		case file.IsSymlink():
 			l.Debugln(f, "Handling symlink", file.Name)
 			if f.checkParent(file.Name, scanChan) {
-				f.handleSymlink(file, snap, dbUpdateChan, scanChan)
+				f.handleSymlink(file, dbUpdateChan, scanChan)
 			}
 
 		default:
 			l.Warnln(file)
 			panic("unhandleable item type, can't happen")
 		}
-
-		return true
-	})
+	}
 
 	select {
 	case <-f.ctx.Done():
@@ -430,23 +432,6 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
 	default:
 	}
 
-	// Now do the file queue. Reorder it according to configuration.
-
-	switch f.Order {
-	case config.PullOrderRandom:
-		f.queue.Shuffle()
-	case config.PullOrderAlphabetic:
-	// The queue is already in alphabetic order.
-	case config.PullOrderSmallestFirst:
-		f.queue.SortSmallestFirst()
-	case config.PullOrderLargestFirst:
-		f.queue.SortLargestFirst()
-	case config.PullOrderOldestFirst:
-		f.queue.SortOldestFirst()
-	case config.PullOrderNewestFirst:
-		f.queue.SortNewestFirst()
-	}
-
 	// Process the file queue.
 
 nextFile:
@@ -462,7 +447,10 @@ nextFile:
 			break
 		}
 
-		fi, ok := snap.GetGlobal(fileName)
+		fi, ok, err := f.model.sdb.GetGlobalFile(f.folderID, fileName)
+		if err != nil {
+			return changed, nil, nil, err
+		}
 		if !ok {
 			// File is no longer in the index. Mark it as done and drop it.
 			f.queue.Done(fileName)
@@ -489,7 +477,7 @@ nextFile:
 			// desired state with the delete bit set is in the deletion
 			// map.
 			desired := fileDeletions[candidate.Name]
-			if err := f.renameFile(candidate, desired, fi, snap, dbUpdateChan, scanChan); err != nil {
+			if err := f.renameFile(candidate, desired, fi, dbUpdateChan, scanChan); err != nil {
 				l.Debugf("rename shortcut for %s failed: %s", fi.Name, err.Error())
 				// Failed to rename, try next one.
 				continue
@@ -502,9 +490,11 @@ nextFile:
 			continue nextFile
 		}
 
-		devices := f.model.fileAvailability(f.FolderConfiguration, snap, fi)
+		devices := f.model.fileAvailability(f.FolderConfiguration, fi)
 		if len(devices) > 0 {
-			f.handleFile(fi, snap, copyChan)
+			if err := f.handleFile(fi, copyChan); err != nil {
+				f.newPullError(fileName, err)
+			}
 			continue
 		}
 		f.newPullError(fileName, errNotAvailable)
@@ -524,7 +514,7 @@ func popCandidate(buckets map[string][]protocol.FileInfo, key string) (protocol.
 	return cands[0], true
 }
 
-func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.FileInfo, dirDeletions []protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.FileInfo, dirDeletions []protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
 	for _, file := range fileDeletions {
 		select {
 		case <-f.ctx.Done():
@@ -532,7 +522,7 @@ func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.F
 		default:
 		}
 
-		f.deleteFile(file, snap, dbUpdateChan, scanChan)
+		f.deleteFile(file, dbUpdateChan, scanChan)
 	}
 
 	// Process in reverse order to delete depth first
@@ -545,12 +535,12 @@ func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.F
 
 		dir := dirDeletions[len(dirDeletions)-i-1]
 		l.Debugln(f, "Deleting dir", dir.Name)
-		f.deleteDir(dir, snap, dbUpdateChan, scanChan)
+		f.deleteDir(dir, dbUpdateChan, scanChan)
 	}
 }
 
 // handleDir creates or updates the given directory
-func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
 	// Used in the defer closure below, updated by the function body. Take
 	// care not declare another err.
 	var err error
@@ -578,7 +568,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot,
 	}
 
 	if shouldDebug() {
-		curFile, _ := snap.Get(protocol.LocalDeviceID, file.Name)
+		curFile, _, _ := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
 		l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
 	}
 
@@ -589,7 +579,11 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot,
 	// that don't result in a conflict.
 	case err == nil && !info.IsDir():
 		// Check that it is what we have in the database.
-		curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
+		curFile, hasCurFile, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+		if err != nil {
+			f.newPullError(file.Name, fmt.Errorf("handling dir: %w", err))
+			return
+		}
 		if err := f.scanIfItemChanged(file.Name, info, curFile, hasCurFile, false, scanChan); err != nil {
 			f.newPullError(file.Name, fmt.Errorf("handling dir: %w", err))
 			return
@@ -606,7 +600,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, snap *db.Snapshot,
 				return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
 			}, curFile.Name)
 		} else {
-			err = f.deleteItemOnDisk(curFile, snap, scanChan)
+			err = f.deleteItemOnDisk(curFile, scanChan)
 		}
 		if err != nil {
 			f.newPullError(file.Name, err)
@@ -715,7 +709,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
 }
 
 // handleSymlink creates or updates the given symlink
-func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
 	// Used in the defer closure below, updated by the function body. Take
 	// care not declare another err.
 	var err error
@@ -738,8 +732,8 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, snap *db.Snaps
 	}()
 
 	if shouldDebug() {
-		curFile, _ := snap.Get(protocol.LocalDeviceID, file.Name)
-		l.Debugf("need symlink\n\t%v\n\t%v", file, curFile)
+		curFile, ok, _ := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+		l.Debugf("need symlink\n\t%v\n\t%v", file, curFile, ok)
 	}
 
 	if len(file.SymlinkTarget) == 0 {
@@ -749,7 +743,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, snap *db.Snaps
 		return
 	}
 
-	if err = f.handleSymlinkCheckExisting(file, snap, scanChan); err != nil {
+	if err = f.handleSymlinkCheckExisting(file, scanChan); err != nil {
 		f.newPullError(file.Name, fmt.Errorf("handling symlink: %w", err))
 		return
 	}
@@ -770,7 +764,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, snap *db.Snaps
 	}
 }
 
-func (f *sendReceiveFolder) handleSymlinkCheckExisting(file protocol.FileInfo, snap *db.Snapshot, scanChan chan<- string) error {
+func (f *sendReceiveFolder) handleSymlinkCheckExisting(file protocol.FileInfo, scanChan chan<- string) error {
 	// If there is already something under that name, we need to handle that.
 	info, err := f.mtimefs.Lstat(file.Name)
 	if err != nil {
@@ -780,7 +774,10 @@ func (f *sendReceiveFolder) handleSymlinkCheckExisting(file protocol.FileInfo, s
 		return err
 	}
 	// Check that it is what we have in the database.
-	curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
+	curFile, hasCurFile, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+	if err != nil {
+		return err
+	}
 	if err := f.scanIfItemChanged(file.Name, info, curFile, hasCurFile, false, scanChan); err != nil {
 		return err
 	}
@@ -796,12 +793,12 @@ func (f *sendReceiveFolder) handleSymlinkCheckExisting(file protocol.FileInfo, s
 			return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
 		}, curFile.Name)
 	} else {
-		return f.deleteItemOnDisk(curFile, snap, scanChan)
+		return f.deleteItemOnDisk(curFile, scanChan)
 	}
 }
 
 // deleteDir attempts to remove a directory that was deleted on a remote
-func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
 	// Used in the defer closure below, updated by the function body. Take
 	// care not declare another err.
 	var err error
@@ -826,7 +823,10 @@ func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot,
 		})
 	}()
 
-	cur, hasCur := snap.Get(protocol.LocalDeviceID, file.Name)
+	cur, hasCur, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+	if err != nil {
+		return
+	}
 
 	if err = f.checkToBeDeleted(file, cur, hasCur, scanChan); err != nil {
 		if fs.IsNotExist(err) || fs.IsErrCaseConflict(err) {
@@ -836,7 +836,7 @@ func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot,
 		return
 	}
 
-	if err = f.deleteDirOnDisk(file.Name, snap, scanChan); err != nil {
+	if err = f.deleteDirOnDisk(file.Name, scanChan); err != nil {
 		return
 	}
 
@@ -844,8 +844,12 @@ func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot,
 }
 
 // deleteFile attempts to delete the given file
-func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
-	cur, hasCur := snap.Get(protocol.LocalDeviceID, file.Name)
+func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+	cur, hasCur, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+	if err != nil {
+		f.newPullError(file.Name, fmt.Errorf("delete file: %w", err))
+		return
+	}
 	f.deleteFileWithCurrent(file, cur, hasCur, dbUpdateChan, scanChan)
 }
 
@@ -924,7 +928,7 @@ func (f *sendReceiveFolder) deleteFileWithCurrent(file, cur protocol.FileInfo, h
 
 // renameFile attempts to rename an existing file to a destination
 // and set the right attributes on it.
-func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
+func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
 	// Used in the defer closure below, updated by the function body. Take
 	// care not declare another err.
 	var err error
@@ -966,7 +970,10 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
 		return err
 	}
 	// Check that the target corresponds to what we have in the DB
-	curTarget, ok := snap.Get(protocol.LocalDeviceID, target.Name)
+	curTarget, ok, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, target.Name)
+	if err != nil {
+		return err
+	}
 	switch stat, serr := f.mtimefs.Lstat(target.Name); {
 	case serr != nil:
 		var caseErr *fs.ErrCaseConflict
@@ -1040,7 +1047,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
 	// of the source and the creation of the target temp file. Fix-up the metadata,
 	// update the local index of the target file and rename from temp to real name.
 
-	if err = f.performFinish(target, curTarget, true, tempName, snap, dbUpdateChan, scanChan); err != nil {
+	if err = f.performFinish(target, curTarget, true, tempName, dbUpdateChan, scanChan); err != nil {
 		return err
 	}
 
@@ -1085,8 +1092,11 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
 
 // handleFile queues the copies and pulls as necessary for a single new or
 // changed file.
-func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, snap *db.Snapshot, copyChan chan<- copyBlocksState) {
-	curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
+func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState) error {
+	curFile, hasCurFile, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
+	if err != nil {
+		return err
+	}
 
 	have, _ := blockDiff(curFile.Blocks, file.Blocks)
 
@@ -1130,6 +1140,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, snap *db.Snapshot
 		have:              len(have),
 	}
 	copyChan <- cs
+	return nil
 }
 
 func (f *sendReceiveFolder) reuseBlocks(blocks []protocol.BlockInfo, reused []int, file protocol.FileInfo, tempName string) ([]protocol.BlockInfo, []int) {
@@ -1284,7 +1295,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
 	// Hope that it's usually in the same folder, so start with that one.
 	folders := []string{f.folderID}
 	for folder, cfg := range f.model.cfg.Folders() {
-		folderFilesystems[folder] = cfg.Filesystem(nil)
+		folderFilesystems[folder] = cfg.Filesystem()
 		if folder != f.folderID {
 			folders = append(folders, folder)
 		}
@@ -1333,49 +1344,61 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
 
 			buf = protocol.BufferPool.Upgrade(buf, int(block.Size))
 
-			found := f.model.finder.Iterate(folders, block.Hash, func(folder, path string, index int32) bool {
-				ffs := folderFilesystems[folder]
-				fd, err := ffs.Open(path)
+			found := false
+			for e, err := range itererr.Zip(f.model.sdb.AllLocalBlocksWithHash(block.Hash)) {
 				if err != nil {
-					return false
+					break
 				}
-				defer fd.Close()
+				it, errFn := f.model.sdb.AllLocalFilesWithBlocksHashAnyFolder(e.BlocklistHash)
+				for folderID, fi := range it {
+					ffs := folderFilesystems[folderID]
+					fd, err := ffs.Open(fi.Name)
+					if err != nil {
+						continue
+					}
+					defer fd.Close()
 
-				srcOffset := int64(state.file.BlockSize()) * int64(index)
-				_, err = fd.ReadAt(buf, srcOffset)
-				if err != nil {
-					return false
-				}
+					_, err = fd.ReadAt(buf, e.Offset)
+					if err != nil {
+						fd.Close()
+						continue
+					}
 
-				// Hash is not SHA256 as it's an encrypted hash token. In that
-				// case we can't verify the block integrity so we'll take it on
-				// trust. (The other side can and will verify.)
-				if f.Type != config.FolderTypeReceiveEncrypted {
-					if err := f.verifyBuffer(buf, block); err != nil {
-						l.Debugln("Finder failed to verify buffer", err)
-						return false
+					// Hash is not SHA256 as it's an encrypted hash token. In that
+					// case we can't verify the block integrity so we'll take it on
+					// trust. (The other side can and will verify.)
+					if f.Type != config.FolderTypeReceiveEncrypted {
+						if err := f.verifyBuffer(buf, block); err != nil {
+							l.Debugln("Finder failed to verify buffer", err)
+							continue
+						}
 					}
-				}
 
-				if f.CopyRangeMethod != config.CopyRangeMethodStandard {
-					err = f.withLimiter(func() error {
-						dstFd.mut.Lock()
-						defer dstFd.mut.Unlock()
-						return fs.CopyRange(f.CopyRangeMethod.ToFS(), fd, dstFd.fd, srcOffset, block.Offset, int64(block.Size))
-					})
-				} else {
-					err = f.limitedWriteAt(dstFd, buf, block.Offset)
-				}
-				if err != nil {
-					state.fail(fmt.Errorf("dst write: %w", err))
+					if f.CopyRangeMethod != config.CopyRangeMethodStandard {
+						err = f.withLimiter(func() error {
+							dstFd.mut.Lock()
+							defer dstFd.mut.Unlock()
+							return fs.CopyRange(f.CopyRangeMethod.ToFS(), fd, dstFd.fd, e.Offset, block.Offset, int64(block.Size))
+						})
+					} else {
+						err = f.limitedWriteAt(dstFd, buf, block.Offset)
+					}
+					if err != nil {
+						state.fail(fmt.Errorf("dst write: %w", err))
+						break
+					}
+					if fi.Name == state.file.Name {
+						state.copiedFromOrigin(block.Size)
+					} else {
+						state.copiedFromElsewhere(block.Size)
+					}
+					found = true
+					break
 				}
-				if path == state.file.Name {
-					state.copiedFromOrigin(block.Size)
-				} else {
-					state.copiedFromElsewhere(block.Size)
+				if err := errFn(); err != nil {
+					l.Warnln(err)
 				}
-				return true
-			})
+			}
 
 			if state.failed() != nil {
 				break
@@ -1410,7 +1433,7 @@ func (*sendReceiveFolder) verifyBuffer(buf []byte, block protocol.BlockInfo) err
 	return nil
 }
 
-func (f *sendReceiveFolder) pullerRoutine(snap *db.Snapshot, in <-chan pullBlockState, out chan<- *sharedPullerState) {
+func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
 	requestLimiter := semaphore.New(f.PullerMaxPendingKiB * 1024)
 	wg := sync.NewWaitGroup()
 
@@ -1441,13 +1464,13 @@ func (f *sendReceiveFolder) pullerRoutine(snap *db.Snapshot, in <-chan pullBlock
 			defer wg.Done()
 			defer requestLimiter.Give(bytes)
 
-			f.pullBlock(state, snap, out)
+			f.pullBlock(state, out)
 		}()
 	}
 	wg.Wait()
 }
 
-func (f *sendReceiveFolder) pullBlock(state pullBlockState, snap *db.Snapshot, out chan<- *sharedPullerState) {
+func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPullerState) {
 	// Get an fd to the temporary file. Technically we don't need it until
 	// after fetching the block, but if we run into an error here there is
 	// no point in issuing the request to the network.
@@ -1466,7 +1489,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, snap *db.Snapshot, o
 	}
 
 	var lastError error
-	candidates := f.model.blockAvailability(f.FolderConfiguration, snap, state.file, state.block)
+	candidates := f.model.blockAvailability(f.FolderConfiguration, state.file, state.block)
 loop:
 	for {
 		select {
@@ -1531,7 +1554,7 @@ loop:
 	out <- state.sharedPullerState
 }
 
-func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCurFile bool, tempName string, snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
+func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCurFile bool, tempName string, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
 	// Set the correct permission bits on the new file
 	if !f.IgnorePerms && !file.NoPermissions {
 		if err := f.mtimefs.Chmod(tempName, fs.FileMode(file.Permissions&0o777)); err != nil {
@@ -1562,7 +1585,7 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
 				return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
 			}, curFile.Name)
 		} else {
-			err = f.deleteItemOnDisk(curFile, snap, scanChan)
+			err = f.deleteItemOnDisk(curFile, scanChan)
 		}
 		if err != nil {
 			return fmt.Errorf("moving for conflict: %w", err)
@@ -1585,7 +1608,7 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
 	return nil
 }
 
-func (f *sendReceiveFolder) finisherRoutine(snap *db.Snapshot, in <-chan *sharedPullerState, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
+func (f *sendReceiveFolder) finisherRoutine(in <-chan *sharedPullerState, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
 	for state := range in {
 		if closed, err := state.finalClose(); closed {
 			l.Debugln(f, "closing", state.file.Name)
@@ -1593,7 +1616,7 @@ func (f *sendReceiveFolder) finisherRoutine(snap *db.Snapshot, in <-chan *shared
 			f.queue.Done(state.file.Name)
 
 			if err == nil {
-				err = f.performFinish(state.file, state.curFile, state.hasCurFile, state.tempName, snap, dbUpdateChan, scanChan)
+				err = f.performFinish(state.file, state.curFile, state.hasCurFile, state.tempName, dbUpdateChan, scanChan)
 			}
 
 			if err != nil {
@@ -1646,7 +1669,7 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) {
 	var lastFile protocol.FileInfo
 	tick := time.NewTicker(maxBatchTime)
 	defer tick.Stop()
-	batch := db.NewFileInfoBatch(func(files []protocol.FileInfo) error {
+	batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
 		// sync directories
 		for dir := range changedDirs {
 			delete(changedDirs, dir)
@@ -1830,7 +1853,7 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
 }
 
 // deleteItemOnDisk deletes the file represented by old that is about to be replaced by new.
-func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, snap *db.Snapshot, scanChan chan<- string) (err error) {
+func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, scanChan chan<- string) (err error) {
 	defer func() {
 		if err != nil {
 			err = fmt.Errorf("%s: %w", contextRemovingOldItem, err)
@@ -1841,7 +1864,7 @@ func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, snap *db.Sn
 	case item.IsDirectory():
 		// Directories aren't archived and need special treatment due
 		// to potential children.
-		return f.deleteDirOnDisk(item.Name, snap, scanChan)
+		return f.deleteDirOnDisk(item.Name, scanChan)
 
 	case !item.IsSymlink() && f.versioner != nil:
 		// If we should use versioning, let the versioner archive the
@@ -1857,12 +1880,12 @@ func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, snap *db.Sn
 
 // deleteDirOnDisk attempts to delete a directory. It checks for files/dirs inside
 // the directory and removes them if possible or returns an error if it fails
-func (f *sendReceiveFolder) deleteDirOnDisk(dir string, snap *db.Snapshot, scanChan chan<- string) error {
+func (f *sendReceiveFolder) deleteDirOnDisk(dir string, scanChan chan<- string) error {
 	if err := osutil.TraversesSymlink(f.mtimefs, filepath.Dir(dir)); err != nil {
 		return err
 	}
 
-	if err := f.deleteDirOnDiskHandleChildren(dir, snap, scanChan); err != nil {
+	if err := f.deleteDirOnDiskHandleChildren(dir, scanChan); err != nil {
 		return err
 	}
 
@@ -1882,7 +1905,7 @@ func (f *sendReceiveFolder) deleteDirOnDisk(dir string, snap *db.Snapshot, scanC
 	return err
 }
 
-func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, snap *db.Snapshot, scanChan chan<- string) error {
+func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, scanChan chan<- string) error {
 	var dirsToDelete []string
 	var hasIgnored, hasKnown, hasToBeScanned, hasReceiveOnlyChanged bool
 	var delErr error
@@ -1909,7 +1932,7 @@ func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, snap *db.S
 			hasIgnored = true
 			return nil
 		}
-		cf, ok := snap.Get(protocol.LocalDeviceID, path)
+		cf, ok, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, path)
 		switch {
 		case !ok || cf.IsDeleted():
 			// Something appeared in the dir that we either are not
diff --git a/lib/model/folder_summary.go b/lib/model/folder_summary.go
index b1d1e77d8d4..ad71fb158fe 100644
--- a/lib/model/folder_summary.go
+++ b/lib/model/folder_summary.go
@@ -17,8 +17,8 @@ import (
 
 	"github.com/thejerf/suture/v4"
 
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/svcutil"
@@ -127,16 +127,12 @@ func (c *folderSummaryService) Summary(folder string) (*FolderSummary, error) {
 	var remoteSeq map[protocol.DeviceID]int64
 	errors, err := c.model.FolderErrors(folder)
 	if err == nil {
-		var snap *db.Snapshot
-		if snap, err = c.model.DBSnapshot(folder); err == nil {
-			global = snap.GlobalSize()
-			local = snap.LocalSize()
-			need = snap.NeedSize(protocol.LocalDeviceID)
-			ro = snap.ReceiveOnlyChangedSize()
-			ourSeq = snap.Sequence(protocol.LocalDeviceID)
-			remoteSeq = snap.RemoteSequences()
-			snap.Release()
-		}
+		global, _ = c.model.GlobalSize(folder)
+		local, _ = c.model.LocalSize(folder, protocol.LocalDeviceID)
+		need, _ = c.model.NeedSize(folder, protocol.LocalDeviceID)
+		ro, _ = c.model.ReceiveOnlySize(folder)
+		ourSeq, _ = c.model.Sequence(folder, protocol.LocalDeviceID)
+		remoteSeq, _ = c.model.RemoteSequences(folder)
 	}
 	// For API backwards compatibility (SyncTrayzor needs it) an empty folder
 	// summary is returned for not running folders, an error might actually be
diff --git a/lib/model/indexhandler.go b/lib/model/indexhandler.go
index 23fe49ac6d7..49c33666e74 100644
--- a/lib/model/indexhandler.go
+++ b/lib/model/indexhandler.go
@@ -8,12 +8,14 @@ package model
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"sync"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/svcutil"
@@ -45,13 +47,19 @@ type indexHandler struct {
 
 	cond   *sync.Cond
 	paused bool
-	fset   *db.FileSet
+	sdb    db.DB
 	runner service
 }
 
-func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, folder config.FolderConfiguration, fset *db.FileSet, runner service, startInfo *clusterConfigDeviceInfo, evLogger events.Logger) *indexHandler {
-	myIndexID := fset.IndexID(protocol.LocalDeviceID)
-	mySequence := fset.Sequence(protocol.LocalDeviceID)
+func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, folder config.FolderConfiguration, sdb db.DB, runner service, startInfo *clusterConfigDeviceInfo, evLogger events.Logger) (*indexHandler, error) {
+	myIndexID, err := sdb.GetIndexID(folder.ID, protocol.LocalDeviceID)
+	if err != nil {
+		return nil, err
+	}
+	mySequence, err := sdb.GetDeviceSequence(folder.ID, protocol.LocalDeviceID)
+	if err != nil {
+		return nil, err
+	}
 	var startSequence int64
 
 	// This is the other side's description of what it knows
@@ -91,14 +99,14 @@ func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, f
 	// otherwise we drop our old index data and expect to get a
 	// completely new set.
 
-	theirIndexID := fset.IndexID(conn.DeviceID())
+	theirIndexID, _ := sdb.GetIndexID(folder.ID, conn.DeviceID())
 	if startInfo.remote.IndexID == 0 {
 		// They're not announcing an index ID. This means they
 		// do not support delta indexes and we should clear any
 		// information we have from them before accepting their
 		// index, which will presumably be a full index.
 		l.Debugf("Device %v folder %s does not announce an index ID", conn.DeviceID().Short(), folder.Description())
-		fset.Drop(conn.DeviceID())
+		sdb.DropAllFiles(folder.ID, conn.DeviceID())
 	} else if startInfo.remote.IndexID != theirIndexID {
 		// The index ID we have on file is not what they're
 		// announcing. They must have reset their database and
@@ -106,8 +114,8 @@ func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, f
 		// information we have and remember this new index ID
 		// instead.
 		l.Infof("Device %v folder %s has a new index ID (%v)", conn.DeviceID().Short(), folder.Description(), startInfo.remote.IndexID)
-		fset.Drop(conn.DeviceID())
-		fset.SetIndexID(conn.DeviceID(), startInfo.remote.IndexID)
+		sdb.DropAllFiles(folder.ID, conn.DeviceID())
+		sdb.SetIndexID(folder.ID, conn.DeviceID(), startInfo.remote.IndexID)
 	}
 
 	return &indexHandler{
@@ -119,27 +127,27 @@ func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, f
 		sentPrevSequence:         startSequence,
 		evLogger:                 evLogger,
 
-		fset:   fset,
+		sdb:    sdb,
 		runner: runner,
 		cond:   sync.NewCond(new(sync.Mutex)),
-	}
+	}, nil
 }
 
-// waitForFileset waits for the handler to resume and fetches the current fileset.
-func (s *indexHandler) waitForFileset(ctx context.Context) (*db.FileSet, error) {
+// waitWhilePaused waits for the handler to resume
+func (s *indexHandler) waitWhilePaused(ctx context.Context) error {
 	s.cond.L.Lock()
 	defer s.cond.L.Unlock()
 
 	for s.paused {
 		select {
 		case <-ctx.Done():
-			return nil, ctx.Err()
+			return ctx.Err()
 		default:
 			s.cond.Wait()
 		}
 	}
 
-	return s.fset, nil
+	return nil
 }
 
 func (s *indexHandler) Serve(ctx context.Context) (err error) {
@@ -162,11 +170,10 @@ func (s *indexHandler) Serve(ctx context.Context) (err error) {
 	}()
 
 	// We need to send one index, regardless of whether there is something to send or not
-	fset, err := s.waitForFileset(ctx)
-	if err != nil {
+	if err := s.waitWhilePaused(ctx); err != nil {
 		return err
 	}
-	err = s.sendIndexTo(ctx, fset)
+	err = s.sendIndexTo(ctx)
 
 	// Subscribe to LocalIndexUpdated (we have new information to send) and
 	// DeviceDisconnected (it might be us who disconnected, so we should
@@ -179,8 +186,7 @@ func (s *indexHandler) Serve(ctx context.Context) (err error) {
 	defer ticker.Stop()
 
 	for err == nil {
-		fset, err = s.waitForFileset(ctx)
-		if err != nil {
+		if err := s.waitWhilePaused(ctx); err != nil {
 			return err
 		}
 
@@ -188,7 +194,11 @@ func (s *indexHandler) Serve(ctx context.Context) (err error) {
 		// currently in the database, wait for the local index to update. The
 		// local index may update for other folders than the one we are
 		// sending for.
-		if fset.Sequence(protocol.LocalDeviceID) <= s.localPrevSequence {
+		seq, err := s.sdb.GetDeviceSequence(s.folder, protocol.LocalDeviceID)
+		if err != nil {
+			return err
+		}
+		if seq <= s.localPrevSequence {
 			select {
 			case <-ctx.Done():
 				return ctx.Err()
@@ -198,7 +208,7 @@ func (s *indexHandler) Serve(ctx context.Context) (err error) {
 			continue
 		}
 
-		err = s.sendIndexTo(ctx, fset)
+		err = s.sendIndexTo(ctx)
 
 		// Wait a short amount of time before entering the next loop. If there
 		// are continuous changes happening to the local index, this gives us
@@ -215,10 +225,9 @@ func (s *indexHandler) Serve(ctx context.Context) (err error) {
 
 // resume might be called because the folder was actually resumed, or just
 // because the folder config changed (and thus the runner and potentially fset).
-func (s *indexHandler) resume(fset *db.FileSet, runner service) {
+func (s *indexHandler) resume(runner service) {
 	s.cond.L.Lock()
 	s.paused = false
-	s.fset = fset
 	s.runner = runner
 	s.cond.Broadcast()
 	s.cond.L.Unlock()
@@ -230,7 +239,6 @@ func (s *indexHandler) pause() {
 		s.evLogger.Log(events.Failure, "index handler got paused while already paused")
 	}
 	s.paused = true
-	s.fset = nil
 	s.runner = nil
 	s.cond.Broadcast()
 	s.cond.L.Unlock()
@@ -238,9 +246,9 @@ func (s *indexHandler) pause() {
 
 // sendIndexTo sends file infos with a sequence number higher than prevSequence and
 // returns the highest sent sequence number.
-func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error {
+func (s *indexHandler) sendIndexTo(ctx context.Context) error {
 	initial := s.localPrevSequence == 0
-	batch := db.NewFileInfoBatch(nil)
+	batch := NewFileInfoBatch(nil)
 	var batchError error
 	batch.SetFlushFunc(func(fs []protocol.FileInfo) error {
 		select {
@@ -284,21 +292,26 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
 		return nil
 	})
 
-	var err error
 	var f protocol.FileInfo
-	snap, err := fset.Snapshot()
-	if err != nil {
-		return svcutil.AsFatalErr(err, svcutil.ExitError)
-	}
-	defer snap.Release()
 	previousWasDelete := false
-	snap.WithHaveSequence(s.localPrevSequence+1, func(fi protocol.FileInfo) bool {
+
+	t0 := time.Now()
+	for fi, err := range itererr.Zip(s.sdb.AllLocalFilesBySequence(s.folder, protocol.LocalDeviceID, s.localPrevSequence+1, 5000)) {
+		if err != nil {
+			return err
+		}
 		// This is to make sure that renames (which is an add followed by a delete) land in the same batch.
 		// Even if the batch is full, we allow a last delete to slip in, we do this by making sure that
 		// the batch ends with a non-delete, or that the last item in the batch is already a delete
 		if batch.Full() && (!fi.IsDeleted() || previousWasDelete) {
-			if err = batch.Flush(); err != nil {
-				return false
+			if err := batch.Flush(); err != nil {
+				return err
+			}
+			if time.Since(t0) > 5*time.Second {
+				// minor hack -- avoid very long running read transactions
+				// during index transmission, to help prevent excessive
+				// growth of database WAL file
+				break
 			}
 		}
 
@@ -307,6 +320,7 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
 				"sequence": fi.SequenceNo(),
 				"start":    s.localPrevSequence + 1,
 			})
+			return errors.New("database misbehaved")
 		}
 
 		if f.Sequence > 0 && fi.SequenceNo() <= f.Sequence {
@@ -315,27 +329,17 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
 				"start":    s.localPrevSequence + 1,
 				"previous": f.Sequence,
 			})
-			// Abort this round of index sending - the next one will pick
-			// up from the last successful one with the repeaired db.
-			defer func() {
-				if fixed, dbErr := fset.RepairSequence(); dbErr != nil {
-					l.Warnln("Failed repairing sequence entries:", dbErr)
-					panic("Failed repairing sequence entries")
-				} else {
-					s.evLogger.Log(events.Failure, "detected and repaired non-increasing sequence")
-					l.Infof("Repaired %v sequence entries in database", fixed)
-				}
-			}()
-			return false
+			return errors.New("database misbehaved")
 		}
 
 		f = fi
+		s.localPrevSequence = f.Sequence
 
 		// If this is a folder receiving encrypted files only, we
 		// mustn't ever send locally changed file infos. Those aren't
 		// encrypted and thus would be a protocol error at the remote.
 		if s.folderIsReceiveEncrypted && fi.IsReceiveOnlyChanged() {
-			return true
+			continue
 		}
 
 		f = prepareFileInfoForIndex(f)
@@ -343,23 +347,11 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
 		previousWasDelete = f.IsDeleted()
 
 		batch.Append(f)
-		return true
-	})
-	if err != nil {
-		return err
 	}
-
 	if err := batch.Flush(); err != nil {
 		return err
 	}
 
-	// Use the sequence of the snapshot we iterated as a starting point for the
-	// next run. Previously we used the sequence of the last file we sent,
-	// however it's possible that a higher sequence exists, just doesn't need to
-	// be sent (e.g. in a receive-only folder, when a local change was
-	// reverted). No point trying to send nothing again.
-	s.localPrevSequence = snap.Sequence(protocol.LocalDeviceID)
-
 	return nil
 }
 
@@ -368,7 +360,6 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, p
 
 	s.cond.L.Lock()
 	paused := s.paused
-	fset := s.fset
 	runner := s.runner
 	s.cond.L.Unlock()
 
@@ -382,13 +373,19 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, p
 	s.downloads.Update(s.folder, makeForgetUpdate(fs))
 
 	if !update {
-		fset.Drop(deviceID)
+		if err := s.sdb.DropAllFiles(s.folder, deviceID); err != nil {
+			return err
+		}
 	}
 
 	l.Debugf("Received %d files for %s from %s, prevSeq=%d, lastSeq=%d", len(fs), s.folder, deviceID.Short(), prevSequence, lastSequence)
 
 	// Verify that the previous sequence number matches what we expected
-	if exp := fset.Sequence(deviceID); prevSequence > 0 && prevSequence != exp {
+	exp, err := s.sdb.GetDeviceSequence(s.folder, deviceID)
+	if err != nil {
+		return err
+	}
+	if prevSequence > 0 && prevSequence != exp {
 		s.logSequenceAnomaly("index update with unexpected sequence", map[string]any{
 			"prevSeq":      prevSequence,
 			"lastSeq":      lastSequence,
@@ -444,8 +441,13 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, p
 		})
 	}
 
-	fset.Update(deviceID, fs)
-	seq := fset.Sequence(deviceID)
+	if err := s.sdb.Update(s.folder, deviceID, fs); err != nil {
+		return err
+	}
+	seq, err := s.sdb.GetDeviceSequence(s.folder, deviceID)
+	if err != nil {
+		return err
+	}
 
 	// Check that the sequence we get back is what we put in...
 	if lastSequence > 0 && len(fs) > 0 && seq != lastSequence {
@@ -508,6 +510,7 @@ func (s *indexHandler) String() string {
 type indexHandlerRegistry struct {
 	evLogger      events.Logger
 	conn          protocol.Connection
+	sdb           db.DB
 	downloads     *deviceDownloadState
 	indexHandlers *serviceMap[string, *indexHandler]
 	startInfos    map[string]*clusterConfigDeviceInfo
@@ -517,14 +520,14 @@ type indexHandlerRegistry struct {
 
 type indexHandlerFolderState struct {
 	cfg    config.FolderConfiguration
-	fset   *db.FileSet
 	runner service
 }
 
-func newIndexHandlerRegistry(conn protocol.Connection, downloads *deviceDownloadState, evLogger events.Logger) *indexHandlerRegistry {
+func newIndexHandlerRegistry(conn protocol.Connection, sdb db.DB, downloads *deviceDownloadState, evLogger events.Logger) *indexHandlerRegistry {
 	r := &indexHandlerRegistry{
 		evLogger:      evLogger,
 		conn:          conn,
+		sdb:           sdb,
 		downloads:     downloads,
 		indexHandlers: newServiceMap[string, *indexHandler](evLogger),
 		startInfos:    make(map[string]*clusterConfigDeviceInfo),
@@ -544,15 +547,19 @@ func (r *indexHandlerRegistry) Serve(ctx context.Context) error {
 	return r.indexHandlers.Serve(ctx)
 }
 
-func (r *indexHandlerRegistry) startLocked(folder config.FolderConfiguration, fset *db.FileSet, runner service, startInfo *clusterConfigDeviceInfo) {
+func (r *indexHandlerRegistry) startLocked(folder config.FolderConfiguration, runner service, startInfo *clusterConfigDeviceInfo) error {
 	r.indexHandlers.RemoveAndWait(folder.ID, 0)
 	delete(r.startInfos, folder.ID)
 
-	is := newIndexHandler(r.conn, r.downloads, folder, fset, runner, startInfo, r.evLogger)
+	is, err := newIndexHandler(r.conn, r.downloads, folder, r.sdb, runner, startInfo, r.evLogger)
+	if err != nil {
+		return err
+	}
 	r.indexHandlers.Add(folder.ID, is)
 
 	// This new connection might help us get in sync.
 	runner.SchedulePull()
+	return nil
 }
 
 // AddIndexInfo starts an index handler for given folder, unless it is paused.
@@ -572,7 +579,7 @@ func (r *indexHandlerRegistry) AddIndexInfo(folder string, startInfo *clusterCon
 		r.startInfos[folder] = startInfo
 		return
 	}
-	r.startLocked(folderState.cfg, folderState.fset, folderState.runner, startInfo)
+	_ = r.startLocked(folderState.cfg, folderState.runner, startInfo) // XXX error handling...
 }
 
 // Remove stops a running index handler or removes one pending to be started.
@@ -612,7 +619,7 @@ func (r *indexHandlerRegistry) RemoveAllExcept(except map[string]remoteFolderSta
 // RegisterFolderState must be called whenever something about the folder
 // changes. The exception being if the folder is removed entirely, then call
 // Remove. The fset and runner arguments may be nil, if given folder is paused.
-func (r *indexHandlerRegistry) RegisterFolderState(folder config.FolderConfiguration, fset *db.FileSet, runner service) {
+func (r *indexHandlerRegistry) RegisterFolderState(folder config.FolderConfiguration, runner service) {
 	if !folder.SharedWith(r.conn.DeviceID()) {
 		r.Remove(folder.ID)
 		return
@@ -622,7 +629,7 @@ func (r *indexHandlerRegistry) RegisterFolderState(folder config.FolderConfigura
 	if folder.Paused {
 		r.folderPausedLocked(folder.ID)
 	} else {
-		r.folderRunningLocked(folder, fset, runner)
+		r.folderRunningLocked(folder, runner)
 	}
 	r.mut.Unlock()
 }
@@ -643,10 +650,9 @@ func (r *indexHandlerRegistry) folderPausedLocked(folder string) {
 // folderRunningLocked resumes an already running index handler or starts it, if it
 // was added while paused.
 // It is a noop if the folder isn't known.
-func (r *indexHandlerRegistry) folderRunningLocked(folder config.FolderConfiguration, fset *db.FileSet, runner service) {
+func (r *indexHandlerRegistry) folderRunningLocked(folder config.FolderConfiguration, runner service) {
 	r.folderStates[folder.ID] = &indexHandlerFolderState{
 		cfg:    folder,
-		fset:   fset,
 		runner: runner,
 	}
 
@@ -656,12 +662,12 @@ func (r *indexHandlerRegistry) folderRunningLocked(folder config.FolderConfigura
 			r.indexHandlers.RemoveAndWait(folder.ID, 0)
 			l.Debugf("Removed index handler for device %v and folder %v in resume", r.conn.DeviceID().Short(), folder.ID)
 		}
-		r.startLocked(folder, fset, runner, info)
+		_ = r.startLocked(folder, runner, info) // XXX error handling...
 		delete(r.startInfos, folder.ID)
 		l.Debugf("Started index handler for device %v and folder %v in resume", r.conn.DeviceID().Short(), folder.ID)
 	} else if isOk {
 		l.Debugf("Resuming index handler for device %v and folder %v", r.conn.DeviceID().Short(), folder)
-		is.resume(fset, runner)
+		is.resume(runner)
 	} else {
 		l.Debugf("Not resuming index handler for device %v and folder %v as none is paused and there is no start info", r.conn.DeviceID().Short(), folder.ID)
 	}
diff --git a/lib/model/mocks/model.go b/lib/model/mocks/model.go
index 6aa17ef60d2..cdceb850e50 100644
--- a/lib/model/mocks/model.go
+++ b/lib/model/mocks/model.go
@@ -3,12 +3,12 @@ package mocks
 
 import (
 	"context"
+	"iter"
 	"net"
 	"sync"
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/fs"
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/stats"
@@ -23,6 +23,19 @@ type Model struct {
 		arg1 protocol.Connection
 		arg2 protocol.Hello
 	}
+	AllGlobalFilesStub        func(string) (iter.Seq[db.FileMetadata], func() error)
+	allGlobalFilesMutex       sync.RWMutex
+	allGlobalFilesArgsForCall []struct {
+		arg1 string
+	}
+	allGlobalFilesReturns struct {
+		result1 iter.Seq[db.FileMetadata]
+		result2 func() error
+	}
+	allGlobalFilesReturnsOnCall map[int]struct {
+		result1 iter.Seq[db.FileMetadata]
+		result2 func() error
+	}
 	AvailabilityStub        func(string, protocol.FileInfo, protocol.BlockInfo) ([]model.Availability, error)
 	availabilityMutex       sync.RWMutex
 	availabilityArgsForCall []struct {
@@ -144,19 +157,6 @@ type Model struct {
 		result2 []string
 		result3 error
 	}
-	DBSnapshotStub        func(string) (*db.Snapshot, error)
-	dBSnapshotMutex       sync.RWMutex
-	dBSnapshotArgsForCall []struct {
-		arg1 string
-	}
-	dBSnapshotReturns struct {
-		result1 *db.Snapshot
-		result2 error
-	}
-	dBSnapshotReturnsOnCall map[int]struct {
-		result1 *db.Snapshot
-		result2 error
-	}
 	DelayScanStub        func(string, time.Duration)
 	delayScanMutex       sync.RWMutex
 	delayScanArgsForCall []struct {
@@ -259,20 +259,6 @@ type Model struct {
 		result1 map[string][]versioner.FileVersion
 		result2 error
 	}
-	GetMtimeMappingStub        func(string, string) (fs.MtimeMapping, error)
-	getMtimeMappingMutex       sync.RWMutex
-	getMtimeMappingArgsForCall []struct {
-		arg1 string
-		arg2 string
-	}
-	getMtimeMappingReturns struct {
-		result1 fs.MtimeMapping
-		result2 error
-	}
-	getMtimeMappingReturnsOnCall map[int]struct {
-		result1 fs.MtimeMapping
-		result2 error
-	}
 	GlobalDirectoryTreeStub        func(string, string, int, bool) ([]*model.TreeEntry, error)
 	globalDirectoryTreeMutex       sync.RWMutex
 	globalDirectoryTreeArgsForCall []struct {
@@ -289,6 +275,19 @@ type Model struct {
 		result1 []*model.TreeEntry
 		result2 error
 	}
+	GlobalSizeStub        func(string) (db.Counts, error)
+	globalSizeMutex       sync.RWMutex
+	globalSizeArgsForCall []struct {
+		arg1 string
+	}
+	globalSizeReturns struct {
+		result1 db.Counts
+		result2 error
+	}
+	globalSizeReturnsOnCall map[int]struct {
+		result1 db.Counts
+		result2 error
+	}
 	IndexStub        func(protocol.Connection, *protocol.Index) error
 	indexMutex       sync.RWMutex
 	indexArgsForCall []struct {
@@ -343,6 +342,49 @@ type Model struct {
 		result1 []protocol.FileInfo
 		result2 error
 	}
+	LocalFilesStub        func(string, protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)
+	localFilesMutex       sync.RWMutex
+	localFilesArgsForCall []struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}
+	localFilesReturns struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}
+	localFilesReturnsOnCall map[int]struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}
+	LocalFilesSequencedStub        func(string, protocol.DeviceID, int64) (iter.Seq[protocol.FileInfo], func() error)
+	localFilesSequencedMutex       sync.RWMutex
+	localFilesSequencedArgsForCall []struct {
+		arg1 string
+		arg2 protocol.DeviceID
+		arg3 int64
+	}
+	localFilesSequencedReturns struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}
+	localFilesSequencedReturnsOnCall map[int]struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}
+	LocalSizeStub        func(string, protocol.DeviceID) (db.Counts, error)
+	localSizeMutex       sync.RWMutex
+	localSizeArgsForCall []struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}
+	localSizeReturns struct {
+		result1 db.Counts
+		result2 error
+	}
+	localSizeReturnsOnCall map[int]struct {
+		result1 db.Counts
+		result2 error
+	}
 	NeedFolderFilesStub        func(string, int, int) ([]protocol.FileInfo, []protocol.FileInfo, []protocol.FileInfo, error)
 	needFolderFilesMutex       sync.RWMutex
 	needFolderFilesArgsForCall []struct {
@@ -362,6 +404,20 @@ type Model struct {
 		result3 []protocol.FileInfo
 		result4 error
 	}
+	NeedSizeStub        func(string, protocol.DeviceID) (db.Counts, error)
+	needSizeMutex       sync.RWMutex
+	needSizeArgsForCall []struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}
+	needSizeReturns struct {
+		result1 db.Counts
+		result2 error
+	}
+	needSizeReturnsOnCall map[int]struct {
+		result1 db.Counts
+		result2 error
+	}
 	OnHelloStub        func(protocol.DeviceID, net.Addr, protocol.Hello) error
 	onHelloMutex       sync.RWMutex
 	onHelloArgsForCall []struct {
@@ -405,6 +461,19 @@ type Model struct {
 		result1 map[string]db.PendingFolder
 		result2 error
 	}
+	ReceiveOnlySizeStub        func(string) (db.Counts, error)
+	receiveOnlySizeMutex       sync.RWMutex
+	receiveOnlySizeArgsForCall []struct {
+		arg1 string
+	}
+	receiveOnlySizeReturns struct {
+		result1 db.Counts
+		result2 error
+	}
+	receiveOnlySizeReturnsOnCall map[int]struct {
+		result1 db.Counts
+		result2 error
+	}
 	RemoteNeedFolderFilesStub        func(string, protocol.DeviceID, int, int) ([]protocol.FileInfo, error)
 	remoteNeedFolderFilesMutex       sync.RWMutex
 	remoteNeedFolderFilesArgsForCall []struct {
@@ -421,6 +490,19 @@ type Model struct {
 		result1 []protocol.FileInfo
 		result2 error
 	}
+	RemoteSequencesStub        func(string) (map[protocol.DeviceID]int64, error)
+	remoteSequencesMutex       sync.RWMutex
+	remoteSequencesArgsForCall []struct {
+		arg1 string
+	}
+	remoteSequencesReturns struct {
+		result1 map[protocol.DeviceID]int64
+		result2 error
+	}
+	remoteSequencesReturnsOnCall map[int]struct {
+		result1 map[protocol.DeviceID]int64
+		result2 error
+	}
 	RequestStub        func(protocol.Connection, *protocol.Request) (protocol.RequestResponse, error)
 	requestMutex       sync.RWMutex
 	requestArgsForCall []struct {
@@ -519,6 +601,20 @@ type Model struct {
 	scanFoldersReturnsOnCall map[int]struct {
 		result1 map[string]error
 	}
+	SequenceStub        func(string, protocol.DeviceID) (int64, error)
+	sequenceMutex       sync.RWMutex
+	sequenceArgsForCall []struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}
+	sequenceReturns struct {
+		result1 int64
+		result2 error
+	}
+	sequenceReturnsOnCall map[int]struct {
+		result1 int64
+		result2 error
+	}
 	ServeStub        func(context.Context) error
 	serveMutex       sync.RWMutex
 	serveArgsForCall []struct {
@@ -612,6 +708,70 @@ func (fake *Model) AddConnectionArgsForCall(i int) (protocol.Connection, protoco
 	return argsForCall.arg1, argsForCall.arg2
 }
 
+func (fake *Model) AllGlobalFiles(arg1 string) (iter.Seq[db.FileMetadata], func() error) {
+	fake.allGlobalFilesMutex.Lock()
+	ret, specificReturn := fake.allGlobalFilesReturnsOnCall[len(fake.allGlobalFilesArgsForCall)]
+	fake.allGlobalFilesArgsForCall = append(fake.allGlobalFilesArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.AllGlobalFilesStub
+	fakeReturns := fake.allGlobalFilesReturns
+	fake.recordInvocation("AllGlobalFiles", []interface{}{arg1})
+	fake.allGlobalFilesMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) AllGlobalFilesCallCount() int {
+	fake.allGlobalFilesMutex.RLock()
+	defer fake.allGlobalFilesMutex.RUnlock()
+	return len(fake.allGlobalFilesArgsForCall)
+}
+
+func (fake *Model) AllGlobalFilesCalls(stub func(string) (iter.Seq[db.FileMetadata], func() error)) {
+	fake.allGlobalFilesMutex.Lock()
+	defer fake.allGlobalFilesMutex.Unlock()
+	fake.AllGlobalFilesStub = stub
+}
+
+func (fake *Model) AllGlobalFilesArgsForCall(i int) string {
+	fake.allGlobalFilesMutex.RLock()
+	defer fake.allGlobalFilesMutex.RUnlock()
+	argsForCall := fake.allGlobalFilesArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *Model) AllGlobalFilesReturns(result1 iter.Seq[db.FileMetadata], result2 func() error) {
+	fake.allGlobalFilesMutex.Lock()
+	defer fake.allGlobalFilesMutex.Unlock()
+	fake.AllGlobalFilesStub = nil
+	fake.allGlobalFilesReturns = struct {
+		result1 iter.Seq[db.FileMetadata]
+		result2 func() error
+	}{result1, result2}
+}
+
+func (fake *Model) AllGlobalFilesReturnsOnCall(i int, result1 iter.Seq[db.FileMetadata], result2 func() error) {
+	fake.allGlobalFilesMutex.Lock()
+	defer fake.allGlobalFilesMutex.Unlock()
+	fake.AllGlobalFilesStub = nil
+	if fake.allGlobalFilesReturnsOnCall == nil {
+		fake.allGlobalFilesReturnsOnCall = make(map[int]struct {
+			result1 iter.Seq[db.FileMetadata]
+			result2 func() error
+		})
+	}
+	fake.allGlobalFilesReturnsOnCall[i] = struct {
+		result1 iter.Seq[db.FileMetadata]
+		result2 func() error
+	}{result1, result2}
+}
+
 func (fake *Model) Availability(arg1 string, arg2 protocol.FileInfo, arg3 protocol.BlockInfo) ([]model.Availability, error) {
 	fake.availabilityMutex.Lock()
 	ret, specificReturn := fake.availabilityReturnsOnCall[len(fake.availabilityArgsForCall)]
@@ -1188,70 +1348,6 @@ func (fake *Model) CurrentIgnoresReturnsOnCall(i int, result1 []string, result2
 	}{result1, result2, result3}
 }
 
-func (fake *Model) DBSnapshot(arg1 string) (*db.Snapshot, error) {
-	fake.dBSnapshotMutex.Lock()
-	ret, specificReturn := fake.dBSnapshotReturnsOnCall[len(fake.dBSnapshotArgsForCall)]
-	fake.dBSnapshotArgsForCall = append(fake.dBSnapshotArgsForCall, struct {
-		arg1 string
-	}{arg1})
-	stub := fake.DBSnapshotStub
-	fakeReturns := fake.dBSnapshotReturns
-	fake.recordInvocation("DBSnapshot", []interface{}{arg1})
-	fake.dBSnapshotMutex.Unlock()
-	if stub != nil {
-		return stub(arg1)
-	}
-	if specificReturn {
-		return ret.result1, ret.result2
-	}
-	return fakeReturns.result1, fakeReturns.result2
-}
-
-func (fake *Model) DBSnapshotCallCount() int {
-	fake.dBSnapshotMutex.RLock()
-	defer fake.dBSnapshotMutex.RUnlock()
-	return len(fake.dBSnapshotArgsForCall)
-}
-
-func (fake *Model) DBSnapshotCalls(stub func(string) (*db.Snapshot, error)) {
-	fake.dBSnapshotMutex.Lock()
-	defer fake.dBSnapshotMutex.Unlock()
-	fake.DBSnapshotStub = stub
-}
-
-func (fake *Model) DBSnapshotArgsForCall(i int) string {
-	fake.dBSnapshotMutex.RLock()
-	defer fake.dBSnapshotMutex.RUnlock()
-	argsForCall := fake.dBSnapshotArgsForCall[i]
-	return argsForCall.arg1
-}
-
-func (fake *Model) DBSnapshotReturns(result1 *db.Snapshot, result2 error) {
-	fake.dBSnapshotMutex.Lock()
-	defer fake.dBSnapshotMutex.Unlock()
-	fake.DBSnapshotStub = nil
-	fake.dBSnapshotReturns = struct {
-		result1 *db.Snapshot
-		result2 error
-	}{result1, result2}
-}
-
-func (fake *Model) DBSnapshotReturnsOnCall(i int, result1 *db.Snapshot, result2 error) {
-	fake.dBSnapshotMutex.Lock()
-	defer fake.dBSnapshotMutex.Unlock()
-	fake.DBSnapshotStub = nil
-	if fake.dBSnapshotReturnsOnCall == nil {
-		fake.dBSnapshotReturnsOnCall = make(map[int]struct {
-			result1 *db.Snapshot
-			result2 error
-		})
-	}
-	fake.dBSnapshotReturnsOnCall[i] = struct {
-		result1 *db.Snapshot
-		result2 error
-	}{result1, result2}
-}
-
 func (fake *Model) DelayScan(arg1 string, arg2 time.Duration) {
 	fake.delayScanMutex.Lock()
 	fake.delayScanArgsForCall = append(fake.delayScanArgsForCall, struct {
@@ -1771,71 +1867,6 @@ func (fake *Model) GetFolderVersionsReturnsOnCall(i int, result1 map[string][]ve
 	}{result1, result2}
 }
 
-func (fake *Model) GetMtimeMapping(arg1 string, arg2 string) (fs.MtimeMapping, error) {
-	fake.getMtimeMappingMutex.Lock()
-	ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)]
-	fake.getMtimeMappingArgsForCall = append(fake.getMtimeMappingArgsForCall, struct {
-		arg1 string
-		arg2 string
-	}{arg1, arg2})
-	stub := fake.GetMtimeMappingStub
-	fakeReturns := fake.getMtimeMappingReturns
-	fake.recordInvocation("GetMtimeMapping", []interface{}{arg1, arg2})
-	fake.getMtimeMappingMutex.Unlock()
-	if stub != nil {
-		return stub(arg1, arg2)
-	}
-	if specificReturn {
-		return ret.result1, ret.result2
-	}
-	return fakeReturns.result1, fakeReturns.result2
-}
-
-func (fake *Model) GetMtimeMappingCallCount() int {
-	fake.getMtimeMappingMutex.RLock()
-	defer fake.getMtimeMappingMutex.RUnlock()
-	return len(fake.getMtimeMappingArgsForCall)
-}
-
-func (fake *Model) GetMtimeMappingCalls(stub func(string, string) (fs.MtimeMapping, error)) {
-	fake.getMtimeMappingMutex.Lock()
-	defer fake.getMtimeMappingMutex.Unlock()
-	fake.GetMtimeMappingStub = stub
-}
-
-func (fake *Model) GetMtimeMappingArgsForCall(i int) (string, string) {
-	fake.getMtimeMappingMutex.RLock()
-	defer fake.getMtimeMappingMutex.RUnlock()
-	argsForCall := fake.getMtimeMappingArgsForCall[i]
-	return argsForCall.arg1, argsForCall.arg2
-}
-
-func (fake *Model) GetMtimeMappingReturns(result1 fs.MtimeMapping, result2 error) {
-	fake.getMtimeMappingMutex.Lock()
-	defer fake.getMtimeMappingMutex.Unlock()
-	fake.GetMtimeMappingStub = nil
-	fake.getMtimeMappingReturns = struct {
-		result1 fs.MtimeMapping
-		result2 error
-	}{result1, result2}
-}
-
-func (fake *Model) GetMtimeMappingReturnsOnCall(i int, result1 fs.MtimeMapping, result2 error) {
-	fake.getMtimeMappingMutex.Lock()
-	defer fake.getMtimeMappingMutex.Unlock()
-	fake.GetMtimeMappingStub = nil
-	if fake.getMtimeMappingReturnsOnCall == nil {
-		fake.getMtimeMappingReturnsOnCall = make(map[int]struct {
-			result1 fs.MtimeMapping
-			result2 error
-		})
-	}
-	fake.getMtimeMappingReturnsOnCall[i] = struct {
-		result1 fs.MtimeMapping
-		result2 error
-	}{result1, result2}
-}
-
 func (fake *Model) GlobalDirectoryTree(arg1 string, arg2 string, arg3 int, arg4 bool) ([]*model.TreeEntry, error) {
 	fake.globalDirectoryTreeMutex.Lock()
 	ret, specificReturn := fake.globalDirectoryTreeReturnsOnCall[len(fake.globalDirectoryTreeArgsForCall)]
@@ -1903,6 +1934,70 @@ func (fake *Model) GlobalDirectoryTreeReturnsOnCall(i int, result1 []*model.Tree
 	}{result1, result2}
 }
 
+func (fake *Model) GlobalSize(arg1 string) (db.Counts, error) {
+	fake.globalSizeMutex.Lock()
+	ret, specificReturn := fake.globalSizeReturnsOnCall[len(fake.globalSizeArgsForCall)]
+	fake.globalSizeArgsForCall = append(fake.globalSizeArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.GlobalSizeStub
+	fakeReturns := fake.globalSizeReturns
+	fake.recordInvocation("GlobalSize", []interface{}{arg1})
+	fake.globalSizeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) GlobalSizeCallCount() int {
+	fake.globalSizeMutex.RLock()
+	defer fake.globalSizeMutex.RUnlock()
+	return len(fake.globalSizeArgsForCall)
+}
+
+func (fake *Model) GlobalSizeCalls(stub func(string) (db.Counts, error)) {
+	fake.globalSizeMutex.Lock()
+	defer fake.globalSizeMutex.Unlock()
+	fake.GlobalSizeStub = stub
+}
+
+func (fake *Model) GlobalSizeArgsForCall(i int) string {
+	fake.globalSizeMutex.RLock()
+	defer fake.globalSizeMutex.RUnlock()
+	argsForCall := fake.globalSizeArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *Model) GlobalSizeReturns(result1 db.Counts, result2 error) {
+	fake.globalSizeMutex.Lock()
+	defer fake.globalSizeMutex.Unlock()
+	fake.GlobalSizeStub = nil
+	fake.globalSizeReturns = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) GlobalSizeReturnsOnCall(i int, result1 db.Counts, result2 error) {
+	fake.globalSizeMutex.Lock()
+	defer fake.globalSizeMutex.Unlock()
+	fake.GlobalSizeStub = nil
+	if fake.globalSizeReturnsOnCall == nil {
+		fake.globalSizeReturnsOnCall = make(map[int]struct {
+			result1 db.Counts
+			result2 error
+		})
+	}
+	fake.globalSizeReturnsOnCall[i] = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) Index(arg1 protocol.Connection, arg2 *protocol.Index) error {
 	fake.indexMutex.Lock()
 	ret, specificReturn := fake.indexReturnsOnCall[len(fake.indexArgsForCall)]
@@ -2160,6 +2255,202 @@ func (fake *Model) LocalChangedFolderFilesReturnsOnCall(i int, result1 []protoco
 	}{result1, result2}
 }
 
+func (fake *Model) LocalFiles(arg1 string, arg2 protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
+	fake.localFilesMutex.Lock()
+	ret, specificReturn := fake.localFilesReturnsOnCall[len(fake.localFilesArgsForCall)]
+	fake.localFilesArgsForCall = append(fake.localFilesArgsForCall, struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}{arg1, arg2})
+	stub := fake.LocalFilesStub
+	fakeReturns := fake.localFilesReturns
+	fake.recordInvocation("LocalFiles", []interface{}{arg1, arg2})
+	fake.localFilesMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) LocalFilesCallCount() int {
+	fake.localFilesMutex.RLock()
+	defer fake.localFilesMutex.RUnlock()
+	return len(fake.localFilesArgsForCall)
+}
+
+func (fake *Model) LocalFilesCalls(stub func(string, protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)) {
+	fake.localFilesMutex.Lock()
+	defer fake.localFilesMutex.Unlock()
+	fake.LocalFilesStub = stub
+}
+
+func (fake *Model) LocalFilesArgsForCall(i int) (string, protocol.DeviceID) {
+	fake.localFilesMutex.RLock()
+	defer fake.localFilesMutex.RUnlock()
+	argsForCall := fake.localFilesArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *Model) LocalFilesReturns(result1 iter.Seq[protocol.FileInfo], result2 func() error) {
+	fake.localFilesMutex.Lock()
+	defer fake.localFilesMutex.Unlock()
+	fake.LocalFilesStub = nil
+	fake.localFilesReturns = struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}{result1, result2}
+}
+
+func (fake *Model) LocalFilesReturnsOnCall(i int, result1 iter.Seq[protocol.FileInfo], result2 func() error) {
+	fake.localFilesMutex.Lock()
+	defer fake.localFilesMutex.Unlock()
+	fake.LocalFilesStub = nil
+	if fake.localFilesReturnsOnCall == nil {
+		fake.localFilesReturnsOnCall = make(map[int]struct {
+			result1 iter.Seq[protocol.FileInfo]
+			result2 func() error
+		})
+	}
+	fake.localFilesReturnsOnCall[i] = struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}{result1, result2}
+}
+
+func (fake *Model) LocalFilesSequenced(arg1 string, arg2 protocol.DeviceID, arg3 int64) (iter.Seq[protocol.FileInfo], func() error) {
+	fake.localFilesSequencedMutex.Lock()
+	ret, specificReturn := fake.localFilesSequencedReturnsOnCall[len(fake.localFilesSequencedArgsForCall)]
+	fake.localFilesSequencedArgsForCall = append(fake.localFilesSequencedArgsForCall, struct {
+		arg1 string
+		arg2 protocol.DeviceID
+		arg3 int64
+	}{arg1, arg2, arg3})
+	stub := fake.LocalFilesSequencedStub
+	fakeReturns := fake.localFilesSequencedReturns
+	fake.recordInvocation("LocalFilesSequenced", []interface{}{arg1, arg2, arg3})
+	fake.localFilesSequencedMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2, arg3)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) LocalFilesSequencedCallCount() int {
+	fake.localFilesSequencedMutex.RLock()
+	defer fake.localFilesSequencedMutex.RUnlock()
+	return len(fake.localFilesSequencedArgsForCall)
+}
+
+func (fake *Model) LocalFilesSequencedCalls(stub func(string, protocol.DeviceID, int64) (iter.Seq[protocol.FileInfo], func() error)) {
+	fake.localFilesSequencedMutex.Lock()
+	defer fake.localFilesSequencedMutex.Unlock()
+	fake.LocalFilesSequencedStub = stub
+}
+
+func (fake *Model) LocalFilesSequencedArgsForCall(i int) (string, protocol.DeviceID, int64) {
+	fake.localFilesSequencedMutex.RLock()
+	defer fake.localFilesSequencedMutex.RUnlock()
+	argsForCall := fake.localFilesSequencedArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3
+}
+
+func (fake *Model) LocalFilesSequencedReturns(result1 iter.Seq[protocol.FileInfo], result2 func() error) {
+	fake.localFilesSequencedMutex.Lock()
+	defer fake.localFilesSequencedMutex.Unlock()
+	fake.LocalFilesSequencedStub = nil
+	fake.localFilesSequencedReturns = struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}{result1, result2}
+}
+
+func (fake *Model) LocalFilesSequencedReturnsOnCall(i int, result1 iter.Seq[protocol.FileInfo], result2 func() error) {
+	fake.localFilesSequencedMutex.Lock()
+	defer fake.localFilesSequencedMutex.Unlock()
+	fake.LocalFilesSequencedStub = nil
+	if fake.localFilesSequencedReturnsOnCall == nil {
+		fake.localFilesSequencedReturnsOnCall = make(map[int]struct {
+			result1 iter.Seq[protocol.FileInfo]
+			result2 func() error
+		})
+	}
+	fake.localFilesSequencedReturnsOnCall[i] = struct {
+		result1 iter.Seq[protocol.FileInfo]
+		result2 func() error
+	}{result1, result2}
+}
+
+func (fake *Model) LocalSize(arg1 string, arg2 protocol.DeviceID) (db.Counts, error) {
+	fake.localSizeMutex.Lock()
+	ret, specificReturn := fake.localSizeReturnsOnCall[len(fake.localSizeArgsForCall)]
+	fake.localSizeArgsForCall = append(fake.localSizeArgsForCall, struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}{arg1, arg2})
+	stub := fake.LocalSizeStub
+	fakeReturns := fake.localSizeReturns
+	fake.recordInvocation("LocalSize", []interface{}{arg1, arg2})
+	fake.localSizeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) LocalSizeCallCount() int {
+	fake.localSizeMutex.RLock()
+	defer fake.localSizeMutex.RUnlock()
+	return len(fake.localSizeArgsForCall)
+}
+
+func (fake *Model) LocalSizeCalls(stub func(string, protocol.DeviceID) (db.Counts, error)) {
+	fake.localSizeMutex.Lock()
+	defer fake.localSizeMutex.Unlock()
+	fake.LocalSizeStub = stub
+}
+
+func (fake *Model) LocalSizeArgsForCall(i int) (string, protocol.DeviceID) {
+	fake.localSizeMutex.RLock()
+	defer fake.localSizeMutex.RUnlock()
+	argsForCall := fake.localSizeArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *Model) LocalSizeReturns(result1 db.Counts, result2 error) {
+	fake.localSizeMutex.Lock()
+	defer fake.localSizeMutex.Unlock()
+	fake.LocalSizeStub = nil
+	fake.localSizeReturns = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) LocalSizeReturnsOnCall(i int, result1 db.Counts, result2 error) {
+	fake.localSizeMutex.Lock()
+	defer fake.localSizeMutex.Unlock()
+	fake.LocalSizeStub = nil
+	if fake.localSizeReturnsOnCall == nil {
+		fake.localSizeReturnsOnCall = make(map[int]struct {
+			result1 db.Counts
+			result2 error
+		})
+	}
+	fake.localSizeReturnsOnCall[i] = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) NeedFolderFiles(arg1 string, arg2 int, arg3 int) ([]protocol.FileInfo, []protocol.FileInfo, []protocol.FileInfo, error) {
 	fake.needFolderFilesMutex.Lock()
 	ret, specificReturn := fake.needFolderFilesReturnsOnCall[len(fake.needFolderFilesArgsForCall)]
@@ -2232,6 +2523,71 @@ func (fake *Model) NeedFolderFilesReturnsOnCall(i int, result1 []protocol.FileIn
 	}{result1, result2, result3, result4}
 }
 
+func (fake *Model) NeedSize(arg1 string, arg2 protocol.DeviceID) (db.Counts, error) {
+	fake.needSizeMutex.Lock()
+	ret, specificReturn := fake.needSizeReturnsOnCall[len(fake.needSizeArgsForCall)]
+	fake.needSizeArgsForCall = append(fake.needSizeArgsForCall, struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}{arg1, arg2})
+	stub := fake.NeedSizeStub
+	fakeReturns := fake.needSizeReturns
+	fake.recordInvocation("NeedSize", []interface{}{arg1, arg2})
+	fake.needSizeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) NeedSizeCallCount() int {
+	fake.needSizeMutex.RLock()
+	defer fake.needSizeMutex.RUnlock()
+	return len(fake.needSizeArgsForCall)
+}
+
+func (fake *Model) NeedSizeCalls(stub func(string, protocol.DeviceID) (db.Counts, error)) {
+	fake.needSizeMutex.Lock()
+	defer fake.needSizeMutex.Unlock()
+	fake.NeedSizeStub = stub
+}
+
+func (fake *Model) NeedSizeArgsForCall(i int) (string, protocol.DeviceID) {
+	fake.needSizeMutex.RLock()
+	defer fake.needSizeMutex.RUnlock()
+	argsForCall := fake.needSizeArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *Model) NeedSizeReturns(result1 db.Counts, result2 error) {
+	fake.needSizeMutex.Lock()
+	defer fake.needSizeMutex.Unlock()
+	fake.NeedSizeStub = nil
+	fake.needSizeReturns = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) NeedSizeReturnsOnCall(i int, result1 db.Counts, result2 error) {
+	fake.needSizeMutex.Lock()
+	defer fake.needSizeMutex.Unlock()
+	fake.NeedSizeStub = nil
+	if fake.needSizeReturnsOnCall == nil {
+		fake.needSizeReturnsOnCall = make(map[int]struct {
+			result1 db.Counts
+			result2 error
+		})
+	}
+	fake.needSizeReturnsOnCall[i] = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) OnHello(arg1 protocol.DeviceID, arg2 net.Addr, arg3 protocol.Hello) error {
 	fake.onHelloMutex.Lock()
 	ret, specificReturn := fake.onHelloReturnsOnCall[len(fake.onHelloArgsForCall)]
@@ -2447,6 +2803,70 @@ func (fake *Model) PendingFoldersReturnsOnCall(i int, result1 map[string]db.Pend
 	}{result1, result2}
 }
 
+func (fake *Model) ReceiveOnlySize(arg1 string) (db.Counts, error) {
+	fake.receiveOnlySizeMutex.Lock()
+	ret, specificReturn := fake.receiveOnlySizeReturnsOnCall[len(fake.receiveOnlySizeArgsForCall)]
+	fake.receiveOnlySizeArgsForCall = append(fake.receiveOnlySizeArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.ReceiveOnlySizeStub
+	fakeReturns := fake.receiveOnlySizeReturns
+	fake.recordInvocation("ReceiveOnlySize", []interface{}{arg1})
+	fake.receiveOnlySizeMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) ReceiveOnlySizeCallCount() int {
+	fake.receiveOnlySizeMutex.RLock()
+	defer fake.receiveOnlySizeMutex.RUnlock()
+	return len(fake.receiveOnlySizeArgsForCall)
+}
+
+func (fake *Model) ReceiveOnlySizeCalls(stub func(string) (db.Counts, error)) {
+	fake.receiveOnlySizeMutex.Lock()
+	defer fake.receiveOnlySizeMutex.Unlock()
+	fake.ReceiveOnlySizeStub = stub
+}
+
+func (fake *Model) ReceiveOnlySizeArgsForCall(i int) string {
+	fake.receiveOnlySizeMutex.RLock()
+	defer fake.receiveOnlySizeMutex.RUnlock()
+	argsForCall := fake.receiveOnlySizeArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *Model) ReceiveOnlySizeReturns(result1 db.Counts, result2 error) {
+	fake.receiveOnlySizeMutex.Lock()
+	defer fake.receiveOnlySizeMutex.Unlock()
+	fake.ReceiveOnlySizeStub = nil
+	fake.receiveOnlySizeReturns = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) ReceiveOnlySizeReturnsOnCall(i int, result1 db.Counts, result2 error) {
+	fake.receiveOnlySizeMutex.Lock()
+	defer fake.receiveOnlySizeMutex.Unlock()
+	fake.ReceiveOnlySizeStub = nil
+	if fake.receiveOnlySizeReturnsOnCall == nil {
+		fake.receiveOnlySizeReturnsOnCall = make(map[int]struct {
+			result1 db.Counts
+			result2 error
+		})
+	}
+	fake.receiveOnlySizeReturnsOnCall[i] = struct {
+		result1 db.Counts
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) RemoteNeedFolderFiles(arg1 string, arg2 protocol.DeviceID, arg3 int, arg4 int) ([]protocol.FileInfo, error) {
 	fake.remoteNeedFolderFilesMutex.Lock()
 	ret, specificReturn := fake.remoteNeedFolderFilesReturnsOnCall[len(fake.remoteNeedFolderFilesArgsForCall)]
@@ -2514,6 +2934,70 @@ func (fake *Model) RemoteNeedFolderFilesReturnsOnCall(i int, result1 []protocol.
 	}{result1, result2}
 }
 
+func (fake *Model) RemoteSequences(arg1 string) (map[protocol.DeviceID]int64, error) {
+	fake.remoteSequencesMutex.Lock()
+	ret, specificReturn := fake.remoteSequencesReturnsOnCall[len(fake.remoteSequencesArgsForCall)]
+	fake.remoteSequencesArgsForCall = append(fake.remoteSequencesArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.RemoteSequencesStub
+	fakeReturns := fake.remoteSequencesReturns
+	fake.recordInvocation("RemoteSequences", []interface{}{arg1})
+	fake.remoteSequencesMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) RemoteSequencesCallCount() int {
+	fake.remoteSequencesMutex.RLock()
+	defer fake.remoteSequencesMutex.RUnlock()
+	return len(fake.remoteSequencesArgsForCall)
+}
+
+func (fake *Model) RemoteSequencesCalls(stub func(string) (map[protocol.DeviceID]int64, error)) {
+	fake.remoteSequencesMutex.Lock()
+	defer fake.remoteSequencesMutex.Unlock()
+	fake.RemoteSequencesStub = stub
+}
+
+func (fake *Model) RemoteSequencesArgsForCall(i int) string {
+	fake.remoteSequencesMutex.RLock()
+	defer fake.remoteSequencesMutex.RUnlock()
+	argsForCall := fake.remoteSequencesArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *Model) RemoteSequencesReturns(result1 map[protocol.DeviceID]int64, result2 error) {
+	fake.remoteSequencesMutex.Lock()
+	defer fake.remoteSequencesMutex.Unlock()
+	fake.RemoteSequencesStub = nil
+	fake.remoteSequencesReturns = struct {
+		result1 map[protocol.DeviceID]int64
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) RemoteSequencesReturnsOnCall(i int, result1 map[protocol.DeviceID]int64, result2 error) {
+	fake.remoteSequencesMutex.Lock()
+	defer fake.remoteSequencesMutex.Unlock()
+	fake.RemoteSequencesStub = nil
+	if fake.remoteSequencesReturnsOnCall == nil {
+		fake.remoteSequencesReturnsOnCall = make(map[int]struct {
+			result1 map[protocol.DeviceID]int64
+			result2 error
+		})
+	}
+	fake.remoteSequencesReturnsOnCall[i] = struct {
+		result1 map[protocol.DeviceID]int64
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) Request(arg1 protocol.Connection, arg2 *protocol.Request) (protocol.RequestResponse, error) {
 	fake.requestMutex.Lock()
 	ret, specificReturn := fake.requestReturnsOnCall[len(fake.requestArgsForCall)]
@@ -2995,6 +3479,71 @@ func (fake *Model) ScanFoldersReturnsOnCall(i int, result1 map[string]error) {
 	}{result1}
 }
 
+func (fake *Model) Sequence(arg1 string, arg2 protocol.DeviceID) (int64, error) {
+	fake.sequenceMutex.Lock()
+	ret, specificReturn := fake.sequenceReturnsOnCall[len(fake.sequenceArgsForCall)]
+	fake.sequenceArgsForCall = append(fake.sequenceArgsForCall, struct {
+		arg1 string
+		arg2 protocol.DeviceID
+	}{arg1, arg2})
+	stub := fake.SequenceStub
+	fakeReturns := fake.sequenceReturns
+	fake.recordInvocation("Sequence", []interface{}{arg1, arg2})
+	fake.sequenceMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *Model) SequenceCallCount() int {
+	fake.sequenceMutex.RLock()
+	defer fake.sequenceMutex.RUnlock()
+	return len(fake.sequenceArgsForCall)
+}
+
+func (fake *Model) SequenceCalls(stub func(string, protocol.DeviceID) (int64, error)) {
+	fake.sequenceMutex.Lock()
+	defer fake.sequenceMutex.Unlock()
+	fake.SequenceStub = stub
+}
+
+func (fake *Model) SequenceArgsForCall(i int) (string, protocol.DeviceID) {
+	fake.sequenceMutex.RLock()
+	defer fake.sequenceMutex.RUnlock()
+	argsForCall := fake.sequenceArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *Model) SequenceReturns(result1 int64, result2 error) {
+	fake.sequenceMutex.Lock()
+	defer fake.sequenceMutex.Unlock()
+	fake.SequenceStub = nil
+	fake.sequenceReturns = struct {
+		result1 int64
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *Model) SequenceReturnsOnCall(i int, result1 int64, result2 error) {
+	fake.sequenceMutex.Lock()
+	defer fake.sequenceMutex.Unlock()
+	fake.SequenceStub = nil
+	if fake.sequenceReturnsOnCall == nil {
+		fake.sequenceReturnsOnCall = make(map[int]struct {
+			result1 int64
+			result2 error
+		})
+	}
+	fake.sequenceReturnsOnCall[i] = struct {
+		result1 int64
+		result2 error
+	}{result1, result2}
+}
+
 func (fake *Model) Serve(arg1 context.Context) error {
 	fake.serveMutex.Lock()
 	ret, specificReturn := fake.serveReturnsOnCall[len(fake.serveArgsForCall)]
@@ -3290,6 +3839,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.invocationsMutex.RUnlock()
 	fake.addConnectionMutex.RLock()
 	defer fake.addConnectionMutex.RUnlock()
+	fake.allGlobalFilesMutex.RLock()
+	defer fake.allGlobalFilesMutex.RUnlock()
 	fake.availabilityMutex.RLock()
 	defer fake.availabilityMutex.RUnlock()
 	fake.bringToFrontMutex.RLock()
@@ -3310,8 +3861,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.currentGlobalFileMutex.RUnlock()
 	fake.currentIgnoresMutex.RLock()
 	defer fake.currentIgnoresMutex.RUnlock()
-	fake.dBSnapshotMutex.RLock()
-	defer fake.dBSnapshotMutex.RUnlock()
 	fake.delayScanMutex.RLock()
 	defer fake.delayScanMutex.RUnlock()
 	fake.deviceStatisticsMutex.RLock()
@@ -3330,10 +3879,10 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.folderStatisticsMutex.RUnlock()
 	fake.getFolderVersionsMutex.RLock()
 	defer fake.getFolderVersionsMutex.RUnlock()
-	fake.getMtimeMappingMutex.RLock()
-	defer fake.getMtimeMappingMutex.RUnlock()
 	fake.globalDirectoryTreeMutex.RLock()
 	defer fake.globalDirectoryTreeMutex.RUnlock()
+	fake.globalSizeMutex.RLock()
+	defer fake.globalSizeMutex.RUnlock()
 	fake.indexMutex.RLock()
 	defer fake.indexMutex.RUnlock()
 	fake.indexUpdateMutex.RLock()
@@ -3342,8 +3891,16 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.loadIgnoresMutex.RUnlock()
 	fake.localChangedFolderFilesMutex.RLock()
 	defer fake.localChangedFolderFilesMutex.RUnlock()
+	fake.localFilesMutex.RLock()
+	defer fake.localFilesMutex.RUnlock()
+	fake.localFilesSequencedMutex.RLock()
+	defer fake.localFilesSequencedMutex.RUnlock()
+	fake.localSizeMutex.RLock()
+	defer fake.localSizeMutex.RUnlock()
 	fake.needFolderFilesMutex.RLock()
 	defer fake.needFolderFilesMutex.RUnlock()
+	fake.needSizeMutex.RLock()
+	defer fake.needSizeMutex.RUnlock()
 	fake.onHelloMutex.RLock()
 	defer fake.onHelloMutex.RUnlock()
 	fake.overrideMutex.RLock()
@@ -3352,8 +3909,12 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.pendingDevicesMutex.RUnlock()
 	fake.pendingFoldersMutex.RLock()
 	defer fake.pendingFoldersMutex.RUnlock()
+	fake.receiveOnlySizeMutex.RLock()
+	defer fake.receiveOnlySizeMutex.RUnlock()
 	fake.remoteNeedFolderFilesMutex.RLock()
 	defer fake.remoteNeedFolderFilesMutex.RUnlock()
+	fake.remoteSequencesMutex.RLock()
+	defer fake.remoteSequencesMutex.RUnlock()
 	fake.requestMutex.RLock()
 	defer fake.requestMutex.RUnlock()
 	fake.requestGlobalMutex.RLock()
@@ -3370,6 +3931,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
 	defer fake.scanFolderSubdirsMutex.RUnlock()
 	fake.scanFoldersMutex.RLock()
 	defer fake.scanFoldersMutex.RUnlock()
+	fake.sequenceMutex.RLock()
+	defer fake.sequenceMutex.RUnlock()
 	fake.serveMutex.RLock()
 	defer fake.serveMutex.RUnlock()
 	fake.setIgnoresMutex.RLock()
diff --git a/lib/model/model.go b/lib/model/model.go
index 63bdbea2f06..9d46d925dbe 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -16,11 +16,13 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"iter"
 	"net"
 	"os"
 	"path/filepath"
 	"reflect"
 	"runtime"
+	"slices"
 	"strings"
 	stdsync "sync"
 	"sync/atomic"
@@ -28,10 +30,11 @@ import (
 
 	"github.com/thejerf/suture/v4"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -93,7 +96,16 @@ type Model interface {
 	GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
 	RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]error, error)
 
-	DBSnapshot(folder string) (*db.Snapshot, error)
+	LocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)
+	LocalFilesSequenced(folder string, device protocol.DeviceID, startSet int64) (iter.Seq[protocol.FileInfo], func() error)
+	LocalSize(folder string, device protocol.DeviceID) (db.Counts, error)
+	GlobalSize(folder string) (db.Counts, error)
+	NeedSize(folder string, device protocol.DeviceID) (db.Counts, error)
+	ReceiveOnlySize(folder string) (db.Counts, error)
+	Sequence(folder string, device protocol.DeviceID) (int64, error)
+	AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error)
+	RemoteSequences(folder string) (map[protocol.DeviceID]int64, error)
+
 	NeedFolderFiles(folder string, page, perpage int) ([]protocol.FileInfo, []protocol.FileInfo, []protocol.FileInfo, error)
 	RemoteNeedFolderFiles(folder string, device protocol.DeviceID, page, perpage int) ([]protocol.FileInfo, error)
 	LocalChangedFolderFiles(folder string, page, perpage int) ([]protocol.FileInfo, error)
@@ -101,7 +113,6 @@ type Model interface {
 
 	CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool, error)
 	CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool, error)
-	GetMtimeMapping(folder string, file string) (fs.MtimeMapping, error)
 	Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) ([]Availability, error)
 
 	Completion(device protocol.DeviceID, folder string) (FolderCompletion, error)
@@ -127,12 +138,11 @@ type model struct {
 	// constructor parameters
 	cfg            config.Wrapper
 	id             protocol.DeviceID
-	db             *db.Lowlevel
+	sdb            db.DB
 	protectedFiles []string
 	evLogger       events.Logger
 
 	// constant or concurrency safe fields
-	finder          *db.BlockFinder
 	progressEmitter *ProgressEmitter
 	shortID         protocol.ShortID
 	// globalRequestLimiter limits the amount of data in concurrent incoming
@@ -145,11 +155,11 @@ type model struct {
 	started         chan struct{}
 	keyGen          *protocol.KeyGenerator
 	promotionTimer  *time.Timer
+	observed        *db.ObservedDB
 
 	// fields protected by mut
 	mut                            sync.RWMutex
 	folderCfgs                     map[string]config.FolderConfiguration                  // folder -> cfg
-	folderFiles                    map[string]*db.FileSet                                 // folder -> files
 	deviceStatRefs                 map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
 	folderIgnores                  map[string]*ignore.Matcher                             // folder -> matcher object
 	folderRunners                  *serviceMap[string, service]                           // folder -> puller or scanner
@@ -173,7 +183,7 @@ type model struct {
 
 var _ config.Verifier = &model{}
 
-type folderFactory func(*model, *db.FileSet, *ignore.Matcher, config.FolderConfiguration, versioner.Versioner, events.Logger, *semaphore.Semaphore) service
+type folderFactory func(*model, *ignore.Matcher, config.FolderConfiguration, versioner.Versioner, events.Logger, *semaphore.Semaphore) service
 
 var folderFactories = make(map[config.FolderType]folderFactory)
 
@@ -202,7 +212,7 @@ var (
 // NewModel creates and starts a new model. The model starts in read-only mode,
 // where it sends index information to connected peers and responds to requests
 // for file data without altering the local folder in any way.
-func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model {
+func NewModel(cfg config.Wrapper, id protocol.DeviceID, sdb db.DB, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model {
 	spec := svcutil.SpecWithDebugLogger(l)
 	m := &model{
 		Supervisor: suture.New("model", spec),
@@ -210,12 +220,11 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protec
 		// constructor parameters
 		cfg:            cfg,
 		id:             id,
-		db:             ldb,
+		sdb:            sdb,
 		protectedFiles: protectedFiles,
 		evLogger:       evLogger,
 
 		// constant or concurrency safe fields
-		finder:               db.NewBlockFinder(ldb),
 		progressEmitter:      NewProgressEmitter(cfg, evLogger),
 		shortID:              id.Short(),
 		globalRequestLimiter: semaphore.New(1024 * cfg.Options().MaxConcurrentIncomingRequestKiB()),
@@ -224,11 +233,11 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protec
 		started:              make(chan struct{}),
 		keyGen:               keyGen,
 		promotionTimer:       time.NewTimer(0),
+		observed:             db.NewObservedDB(sdb),
 
 		// fields protected by mut
 		mut:                            sync.NewRWMutex(),
 		folderCfgs:                     make(map[string]config.FolderConfiguration),
-		folderFiles:                    make(map[string]*db.FileSet),
 		deviceStatRefs:                 make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
 		folderIgnores:                  make(map[string]*ignore.Matcher),
 		folderRunners:                  newServiceMap[string, service](evLogger),
@@ -246,7 +255,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protec
 		indexHandlers:                  newServiceMap[protocol.DeviceID, *indexHandlerRegistry](evLogger),
 	}
 	for devID, cfg := range cfg.Devices() {
-		m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID)
+		m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(db.NewTyped(sdb, "devicestats/"+devID.String()))
 		m.setConnRequestLimitersLocked(cfg)
 	}
 	m.Add(m.folderRunners)
@@ -327,21 +336,20 @@ func (m *model) fatal(err error) {
 }
 
 // Need to hold lock on m.mut when calling this.
-func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet, cacheIgnoredFiles bool) {
-	ignores := ignore.New(cfg.Filesystem(nil), ignore.WithCache(cacheIgnoredFiles))
+func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, cacheIgnoredFiles bool) {
+	ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
 	if cfg.Type != config.FolderTypeReceiveEncrypted {
 		if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
 			l.Warnln("Loading ignores:", err)
 		}
 	}
 
-	m.addAndStartFolderLockedWithIgnores(cfg, fset, ignores)
+	m.addAndStartFolderLockedWithIgnores(cfg, ignores)
 }
 
 // Only needed for testing, use addAndStartFolderLocked instead.
-func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguration, fset *db.FileSet, ignores *ignore.Matcher) {
+func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguration, ignores *ignore.Matcher) {
 	m.folderCfgs[cfg.ID] = cfg
-	m.folderFiles[cfg.ID] = fset
 	m.folderIgnores[cfg.ID] = ignores
 
 	_, ok := m.folderRunners.Get(cfg.ID)
@@ -360,16 +368,19 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
 	// Find any devices for which we hold the index in the db, but the folder
 	// is not shared, and drop it.
 	expected := mapDevices(cfg.DeviceIDs())
-	for _, available := range fset.ListDevices() {
+	devs, _ := m.sdb.ListDevicesForFolder(cfg.ID)
+	for _, available := range devs {
 		if _, ok := expected[available]; !ok {
 			l.Debugln("dropping", folder, "state for", available)
-			fset.Drop(available)
+			m.sdb.DropAllFiles(folder, available)
 		}
 	}
 
-	v, ok := fset.Sequence(protocol.LocalDeviceID), true
-	indexHasFiles := ok && v > 0
-	if !indexHasFiles {
+	seq, err := m.sdb.GetDeviceSequence(folder, protocol.LocalDeviceID)
+	if err != nil {
+		panic(fmt.Errorf("error getting sequence number: %w", err))
+	}
+	if seq == 0 {
 		// It's a blank folder, so this may the first time we're looking at
 		// it. Attempt to create and tag with our marker as appropriate. We
 		// don't really do anything with errors at this point except warn -
@@ -392,7 +403,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
 	}
 
 	// These are our metadata files, and they should always be hidden.
-	ffs := cfg.Filesystem(nil)
+	ffs := cfg.Filesystem()
 	_ = ffs.Hide(config.DefaultMarkerName)
 	_ = ffs.Hide(versioner.DefaultPath)
 	_ = ffs.Hide(".stignore")
@@ -409,7 +420,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
 
 	m.warnAboutOverwritingProtectedFiles(cfg, ignores)
 
-	p := folderFactory(m, fset, ignores, cfg, ver, m.evLogger, m.folderIOLimiter)
+	p := folderFactory(m, ignores, cfg, ver, m.evLogger, m.folderIOLimiter)
 	m.folderRunners.Add(folder, p)
 
 	l.Infof("Ready to synchronize %s (%s)", cfg.Description(), cfg.Type)
@@ -421,7 +432,7 @@ func (m *model) warnAboutOverwritingProtectedFiles(cfg config.FolderConfiguratio
 	}
 
 	// This is a bit of a hack.
-	ffs := cfg.Filesystem(nil)
+	ffs := cfg.Filesystem()
 	if ffs.Type() != fs.FilesystemTypeBasic {
 		return
 	}
@@ -468,7 +479,7 @@ func (m *model) removeFolder(cfg config.FolderConfiguration) {
 		// otherwise not removable) Syncthing-specific marker files.
 		if err := cfg.RemoveMarker(); err != nil && !errors.Is(err, os.ErrNotExist) {
 			moved := config.DefaultMarkerName + time.Now().Format(".removed-20060102-150405")
-			fs := cfg.Filesystem(nil)
+			fs := cfg.Filesystem()
 			_ = fs.Rename(config.DefaultMarkerName, moved)
 		}
 	}
@@ -482,7 +493,7 @@ func (m *model) removeFolder(cfg config.FolderConfiguration) {
 	m.mut.Unlock()
 
 	// Remove it from the database
-	db.DropFolder(m.db, cfg.ID)
+	m.sdb.DropFolder(cfg.ID)
 }
 
 // Need to hold lock on m.mut when calling this.
@@ -490,7 +501,6 @@ func (m *model) cleanupFolderLocked(cfg config.FolderConfiguration) {
 	// clear up our config maps
 	m.folderRunners.Remove(cfg.ID)
 	delete(m.folderCfgs, cfg.ID)
-	delete(m.folderFiles, cfg.ID)
 	delete(m.folderIgnores, cfg.ID)
 	delete(m.folderVersioners, cfg.ID)
 	delete(m.folderEncryptionPasswordTokens, cfg.ID)
@@ -525,28 +535,14 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
 	m.mut.Lock()
 	defer m.mut.Unlock()
 
-	// Cache the (maybe) existing fset before it's removed by cleanupFolderLocked
-	fset := m.folderFiles[folder]
-	fsetNil := fset == nil
-
 	m.cleanupFolderLocked(from)
 	if !to.Paused {
-		if fsetNil {
-			// Create a new fset. Might take a while and we do it under
-			// locking, but it's unsafe to create fset:s concurrently so
-			// that's the price we pay.
-			var err error
-			fset, err = db.NewFileSet(folder, m.db)
-			if err != nil {
-				return fmt.Errorf("restarting %v: %w", to.Description(), err)
-			}
-		}
-		m.addAndStartFolderLocked(to, fset, cacheIgnoredFiles)
+		m.addAndStartFolderLocked(to, cacheIgnoredFiles)
 	}
 
 	runner, _ := m.folderRunners.Get(to.ID)
 	m.indexHandlers.Each(func(_ protocol.DeviceID, r *indexHandlerRegistry) error {
-		r.RegisterFolderState(to, fset, runner)
+		r.RegisterFolderState(to, runner)
 		return nil
 	})
 
@@ -568,22 +564,14 @@ func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool
 	m.mut.Lock()
 	defer m.mut.Unlock()
 
-	// Creating the fileset can take a long time (metadata calculation), but
-	// nevertheless should happen inside the lock (same as when restarting
-	// a folder).
-	fset, err := db.NewFileSet(cfg.ID, m.db)
-	if err != nil {
-		return fmt.Errorf("adding %v: %w", cfg.Description(), err)
-	}
-
-	m.addAndStartFolderLocked(cfg, fset, cacheIgnoredFiles)
+	m.addAndStartFolderLocked(cfg, cacheIgnoredFiles)
 
 	// Cluster configs might be received and processed before reaching this
 	// point, i.e. before the folder is started. If that's the case, start
 	// index senders here.
 	m.indexHandlers.Each(func(_ protocol.DeviceID, r *indexHandlerRegistry) error {
 		runner, _ := m.folderRunners.Get(cfg.ID)
-		r.RegisterFolderState(cfg, fset, runner)
+		r.RegisterFolderState(cfg, runner)
 		return nil
 	})
 
@@ -923,46 +911,78 @@ func (m *model) Completion(device protocol.DeviceID, folder string) (FolderCompl
 func (m *model) folderCompletion(device protocol.DeviceID, folder string) (FolderCompletion, error) {
 	m.mut.RLock()
 	err := m.checkFolderRunningRLocked(folder)
-	rf := m.folderFiles[folder]
 	m.mut.RUnlock()
 	if err != nil {
 		return FolderCompletion{}, err
 	}
 
-	snap, err := rf.Snapshot()
-	if err != nil {
-		return FolderCompletion{}, err
-	}
-	defer snap.Release()
-
 	m.mut.RLock()
 	state := m.remoteFolderStates[device][folder]
 	downloaded := m.deviceDownloads[device].BytesDownloaded(folder)
 	m.mut.RUnlock()
 
-	need := snap.NeedSize(device)
+	need, err := m.sdb.CountNeed(folder, device)
+	if err != nil {
+		return FolderCompletion{}, err
+	}
 	need.Bytes -= downloaded
 	// This might be more than it really is, because some blocks can be of a smaller size.
 	if need.Bytes < 0 {
 		need.Bytes = 0
 	}
 
-	comp := newFolderCompletion(snap.GlobalSize(), need, snap.Sequence(device), state)
+	seq, err := m.sdb.GetDeviceSequence(folder, device)
+	if err != nil {
+		return FolderCompletion{}, err
+	}
+	glob, err := m.sdb.CountGlobal(folder)
+	if err != nil {
+		return FolderCompletion{}, err
+	}
+	comp := newFolderCompletion(glob, need, seq, state)
 
 	l.Debugf("%v Completion(%s, %q): %v", m, device, folder, comp.Map())
 	return comp, nil
 }
 
-// DBSnapshot returns a snapshot of the database content relevant to the given folder.
-func (m *model) DBSnapshot(folder string) (*db.Snapshot, error) {
-	m.mut.RLock()
-	err := m.checkFolderRunningRLocked(folder)
-	rf := m.folderFiles[folder]
-	m.mut.RUnlock()
-	if err != nil {
-		return nil, err
-	}
-	return rf.Snapshot()
+func (m *model) LocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
+	return m.sdb.AllLocalFiles(folder, device)
+}
+
+func (m *model) LocalFilesSequenced(folder string, device protocol.DeviceID, startSeq int64) (iter.Seq[protocol.FileInfo], func() error) {
+	return m.sdb.AllLocalFilesBySequence(folder, device, startSeq, 0)
+}
+
+func (m *model) AllForBlocksHash(folder string, h []byte) (iter.Seq[db.FileMetadata], func() error) {
+	return m.sdb.AllLocalFilesWithBlocksHash(folder, h)
+}
+
+func (m *model) LocalSize(folder string, device protocol.DeviceID) (db.Counts, error) {
+	return m.sdb.CountLocal(folder, device)
+}
+
+func (m *model) GlobalSize(folder string) (db.Counts, error) {
+	return m.sdb.CountGlobal(folder)
+}
+
+func (m *model) NeedSize(folder string, device protocol.DeviceID) (db.Counts, error) {
+	return m.sdb.CountNeed(folder, device)
+}
+
+func (m *model) ReceiveOnlySize(folder string) (db.Counts, error) {
+	return m.sdb.CountReceiveOnlyChanged(folder)
+}
+
+func (m *model) Sequence(folder string, device protocol.DeviceID) (int64, error) {
+	return m.sdb.GetDeviceSequence(folder, device)
+}
+
+func (m *model) AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error) {
+	return m.sdb.AllGlobalFiles(folder)
+}
+
+func (m *model) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
+	return m.sdb.RemoteSequences(folder)
 }
 
 func (m *model) FolderProgressBytesCompleted(folder string) int64 {
@@ -973,20 +993,14 @@ func (m *model) FolderProgressBytesCompleted(folder string) int64 {
 // progress, queued, and to be queued on next puller iteration.
 func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]protocol.FileInfo, []protocol.FileInfo, []protocol.FileInfo, error) {
 	m.mut.RLock()
-	rf, rfOk := m.folderFiles[folder]
 	runner, runnerOk := m.folderRunners.Get(folder)
-	cfg := m.folderCfgs[folder]
+	cfg, cfgOK := m.folderCfgs[folder]
 	m.mut.RUnlock()
 
-	if !rfOk {
+	if !cfgOK {
 		return nil, nil, nil, ErrFolderMissing
 	}
 
-	snap, err := rf.Snapshot()
-	if err != nil {
-		return nil, nil, nil, err
-	}
-	defer snap.Release()
 	var progress, queued, rest []protocol.FileInfo
 	var seen map[string]struct{}
 
@@ -1000,14 +1014,14 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]protocol.Fi
 		seen = make(map[string]struct{}, len(progressNames)+len(queuedNames))
 
 		for i, name := range progressNames {
-			if f, ok := snap.GetGlobalTruncated(name); ok {
+			if f, ok, err := m.sdb.GetGlobalFile(folder, name); err == nil && ok {
 				progress[i] = f
 				seen[name] = struct{}{}
 			}
 		}
 
 		for i, name := range queuedNames {
-			if f, ok := snap.GetGlobalTruncated(name); ok {
+			if f, ok, err := m.sdb.GetGlobalFile(folder, name); err == nil && ok {
 				queued[i] = f
 				seen[name] = struct{}{}
 			}
@@ -1020,21 +1034,29 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]protocol.Fi
 		p.toSkip -= skipped
 	}
 
-	rest = make([]protocol.FileInfo, 0, perpage)
-	snap.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileInfo) bool {
-		if cfg.IgnoreDelete && f.IsDeleted() {
-			return true
-		}
+	if p.get > 0 {
+		rest = make([]protocol.FileInfo, 0, p.get)
+		it, errFn := m.sdb.AllNeededGlobalFiles(folder, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)
+		for f := range it {
+			if cfg.IgnoreDelete && f.IsDeleted() {
+				continue
+			}
 
-		if p.skip() {
-			return true
+			if p.skip() {
+				continue
+			}
+			if _, ok := seen[f.Name]; !ok {
+				rest = append(rest, f)
+				p.get--
+			}
+			if p.get == 0 {
+				break
+			}
 		}
-		if _, ok := seen[f.Name]; !ok {
-			rest = append(rest, f)
-			p.get--
+		if err := errFn(); err != nil {
+			return nil, nil, nil, err
 		}
-		return p.get > 0
-	})
+	}
 
 	return progress, queued, rest, nil
 }
@@ -1043,63 +1065,56 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]protocol.Fi
 // remote device to become synced with a folder.
 func (m *model) RemoteNeedFolderFiles(folder string, device protocol.DeviceID, page, perpage int) ([]protocol.FileInfo, error) {
 	m.mut.RLock()
-	rf, ok := m.folderFiles[folder]
+	_, ok := m.folderCfgs[folder]
 	m.mut.RUnlock()
 
 	if !ok {
 		return nil, ErrFolderMissing
 	}
 
-	snap, err := rf.Snapshot()
-	if err != nil {
-		return nil, err
-	}
-	defer snap.Release()
-
-	files := make([]protocol.FileInfo, 0, perpage)
-	p := newPager(page, perpage)
-	snap.WithNeedTruncated(device, func(f protocol.FileInfo) bool {
-		if p.skip() {
-			return true
-		}
-		files = append(files, f)
-		return !p.done()
-	})
-	return files, nil
+	it, errFn := m.sdb.AllNeededGlobalFiles(folder, device, config.PullOrderAlphabetic, perpage, (page-1)*perpage)
+	files := slices.Collect(it)
+	return files, errFn()
 }
 
 func (m *model) LocalChangedFolderFiles(folder string, page, perpage int) ([]protocol.FileInfo, error) {
 	m.mut.RLock()
-	rf, ok := m.folderFiles[folder]
+	_, ok := m.folderCfgs[folder]
 	m.mut.RUnlock()
 
 	if !ok {
 		return nil, ErrFolderMissing
 	}
 
-	snap, err := rf.Snapshot()
+	ros, err := m.sdb.CountReceiveOnlyChanged(folder)
 	if err != nil {
 		return nil, err
 	}
-	defer snap.Release()
-
-	if snap.ReceiveOnlyChangedSize().TotalItems() == 0 {
+	if ros.TotalItems() == 0 {
 		return nil, nil
 	}
 
 	p := newPager(page, perpage)
 	files := make([]protocol.FileInfo, 0, perpage)
 
-	snap.WithHaveTruncated(protocol.LocalDeviceID, func(f protocol.FileInfo) bool {
+	// This could be made more efficient with a specifically targeted DB
+	// call
+	it, errFn := m.sdb.AllLocalFiles(folder, protocol.LocalDeviceID)
+	for f := range it {
 		if !f.IsReceiveOnlyChanged() {
-			return true
+			continue
 		}
 		if p.skip() {
-			return true
+			continue
 		}
 		files = append(files, f)
-		return !p.done()
-	})
+		if p.done() {
+			break
+		}
+	}
+	if err := errFn(); err != nil {
+		return nil, err
+	}
 
 	return files, nil
 }
@@ -1343,11 +1358,11 @@ func (m *model) ensureIndexHandler(conn protocol.Connection) *indexHandlerRegist
 	}
 
 	// Create a new index handler for this device.
-	indexHandlerRegistry = newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], m.evLogger)
+	indexHandlerRegistry = newIndexHandlerRegistry(conn, m.sdb, m.deviceDownloads[deviceID], m.evLogger)
 	for id, fcfg := range m.folderCfgs {
 		l.Debugln("Registering folder", id, "for", deviceID.Short())
 		runner, _ := m.folderRunners.Get(id)
-		indexHandlerRegistry.RegisterFolderState(fcfg, m.folderFiles[id], runner)
+		indexHandlerRegistry.RegisterFolderState(fcfg, runner)
 	}
 	m.indexHandlers.Add(deviceID, indexHandlerRegistry)
 
@@ -1376,7 +1391,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
 	seenFolders := make(map[string]remoteFolderState, len(folders))
 	updatedPending := make([]updatedPendingFolder, 0, len(folders))
 	deviceID := deviceCfg.DeviceID
-	expiredPending, err := m.db.PendingFoldersForDevice(deviceID)
+	expiredPending, err := m.observed.PendingFoldersForDevice(deviceID)
 	if err != nil {
 		l.Infof("Could not get pending folders for cleanup: %v", err)
 	}
@@ -1398,7 +1413,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
 			of.Label = folder.Label
 			of.ReceiveEncrypted = len(ccDeviceInfos[folder.ID].local.EncryptionPasswordToken) > 0
 			of.RemoteEncrypted = len(ccDeviceInfos[folder.ID].remote.EncryptionPasswordToken) > 0
-			if err := m.db.AddOrUpdatePendingFolder(folder.ID, of, deviceID); err != nil {
+			if err := m.observed.AddOrUpdatePendingFolder(folder.ID, of, deviceID); err != nil {
 				l.Warnf("Failed to persist pending folder entry to database: %v", err)
 			}
 			if !folder.Paused {
@@ -1485,7 +1500,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
 
 	expiredPendingList := make([]map[string]string, 0, len(expiredPending))
 	for folder := range expiredPending {
-		if err = m.db.RemovePendingFolderForDevice(folder, deviceID); err != nil {
+		if err = m.observed.RemovePendingFolderForDevice(folder, deviceID); err != nil {
 			msg := "Failed to remove pending folder-device entry"
 			l.Warnf("%v (%v, %v): %v", msg, folder, deviceID, err)
 			m.evLogger.Log(events.Failure, msg)
@@ -2015,7 +2030,7 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
 	// Grab the FS after limiting, as it causes I/O and we want to minimize
 	// the race time between the symlink check and the read.
 
-	folderFs := folderCfg.Filesystem(nil)
+	folderFs := folderCfg.Filesystem()
 
 	if err := osutil.TraversesSymlink(folderFs, filepath.Dir(req.Name)); err != nil {
 		l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
@@ -2138,46 +2153,11 @@ func (m *model) recheckFile(deviceID protocol.DeviceID, folder, name string, off
 }
 
 func (m *model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool, error) {
-	m.mut.RLock()
-	fs, ok := m.folderFiles[folder]
-	m.mut.RUnlock()
-	if !ok {
-		return protocol.FileInfo{}, false, ErrFolderMissing
-	}
-	snap, err := fs.Snapshot()
-	if err != nil {
-		return protocol.FileInfo{}, false, err
-	}
-	f, ok := snap.Get(protocol.LocalDeviceID, file)
-	snap.Release()
-	return f, ok, nil
+	return m.sdb.GetDeviceFile(folder, protocol.LocalDeviceID, file)
 }
 
 func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
-	m.mut.RLock()
-	ffs, ok := m.folderFiles[folder]
-	m.mut.RUnlock()
-	if !ok {
-		return protocol.FileInfo{}, false, ErrFolderMissing
-	}
-	snap, err := ffs.Snapshot()
-	if err != nil {
-		return protocol.FileInfo{}, false, err
-	}
-	f, ok := snap.GetGlobal(file)
-	snap.Release()
-	return f, ok, nil
-}
-
-func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, error) {
-	m.mut.RLock()
-	ffs, ok := m.folderFiles[folder]
-	fcfg := m.folderCfgs[folder]
-	m.mut.RUnlock()
-	if !ok {
-		return fs.MtimeMapping{}, ErrFolderMissing
-	}
-	return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
+	return m.sdb.GetGlobalFile(folder, file)
 }
 
 // Connection returns if we are connected to the given device.
@@ -2208,7 +2188,7 @@ func (m *model) LoadIgnores(folder string) ([]string, []string, error) {
 	}
 
 	if !ignoresOk {
-		ignores = ignore.New(cfg.Filesystem(nil))
+		ignores = ignore.New(cfg.Filesystem())
 	}
 
 	err := ignores.Load(".stignore")
@@ -2263,7 +2243,7 @@ func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) err
 		return err
 	}
 
-	if err := ignore.WriteIgnores(cfg.Filesystem(nil), ".stignore", content); err != nil {
+	if err := ignore.WriteIgnores(cfg.Filesystem(), ".stignore", content); err != nil {
 		l.Warnln("Saving .stignore:", err)
 		return err
 	}
@@ -2282,7 +2262,7 @@ func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) err
 // and add it to a list of known devices ahead of any checks.
 func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.Hello) error {
 	if _, ok := m.cfg.Device(remoteID); !ok {
-		if err := m.db.AddOrUpdatePendingDevice(remoteID, hello.DeviceName, addr.String()); err != nil {
+		if err := m.observed.AddOrUpdatePendingDevice(remoteID, hello.DeviceName, addr.String()); err != nil {
 			l.Warnf("Failed to persist pending device entry to database: %v", err)
 		}
 		m.evLogger.Log(events.PendingDevicesChanged, map[string][]interface{}{
@@ -2611,13 +2591,11 @@ func (m *model) generateClusterConfigRLocked(device protocol.DeviceID) (*protoco
 			DisableTempIndexes: folderCfg.DisableTempIndexes,
 		}
 
-		fs := m.folderFiles[folderCfg.ID]
-
 		// Even if we aren't paused, if we haven't started the folder yet
 		// pretend we are. Otherwise the remote might get confused about
 		// the missing index info (and drop all the info). We will send
 		// another cluster config once the folder is started.
-		protocolFolder.Paused = folderCfg.Paused || fs == nil
+		protocolFolder.Paused = folderCfg.Paused
 
 		for _, folderDevice := range folderCfg.Devices {
 			deviceCfg, _ := m.cfg.Device(folderDevice.DeviceID)
@@ -2640,14 +2618,12 @@ func (m *model) generateClusterConfigRLocked(device protocol.DeviceID) (*protoco
 				}
 			}
 
-			if fs != nil {
-				if deviceCfg.DeviceID == m.id {
-					protocolDevice.IndexID = fs.IndexID(protocol.LocalDeviceID)
-					protocolDevice.MaxSequence = fs.Sequence(protocol.LocalDeviceID)
-				} else {
-					protocolDevice.IndexID = fs.IndexID(deviceCfg.DeviceID)
-					protocolDevice.MaxSequence = fs.Sequence(deviceCfg.DeviceID)
-				}
+			if deviceCfg.DeviceID == m.id {
+				protocolDevice.IndexID, _ = m.sdb.GetIndexID(folderCfg.ID, protocol.LocalDeviceID)
+				protocolDevice.MaxSequence, _ = m.sdb.GetDeviceSequence(folderCfg.ID, protocol.LocalDeviceID)
+			} else {
+				protocolDevice.IndexID, _ = m.sdb.GetIndexID(folderCfg.ID, deviceCfg.DeviceID)
+				protocolDevice.MaxSequence, _ = m.sdb.GetDeviceSequence(folderCfg.ID, deviceCfg.DeviceID)
 			}
 
 			protocolFolder.Devices = append(protocolFolder.Devices, protocolDevice)
@@ -2744,7 +2720,7 @@ func findByName(slice []*TreeEntry, name string) *TreeEntry {
 
 func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error) {
 	m.mut.RLock()
-	files, ok := m.folderFiles[folder]
+	_, ok := m.folderCfgs[folder]
 	m.mut.RUnlock()
 	if !ok {
 		return nil, ErrFolderMissing
@@ -2760,15 +2736,14 @@ func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly
 		prefix = prefix + sep
 	}
 
-	snap, err := files.Snapshot()
-	if err != nil {
-		return nil, err
-	}
-	defer snap.Release()
-	snap.WithPrefixedGlobalTruncated(prefix, func(f protocol.FileInfo) bool {
+	for f, err := range itererr.Zip(m.sdb.AllGlobalFilesPrefix(folder, prefix)) {
+		if err != nil {
+			return nil, err
+		}
+
 		// Don't include the prefix itself.
-		if f.IsInvalid() || f.IsDeleted() || strings.HasPrefix(prefix, f.Name) {
-			return true
+		if f.Invalid || f.Deleted || strings.HasPrefix(prefix, f.Name) {
+			continue
 		}
 
 		f.Name = strings.Replace(f.Name, prefix, "", 1)
@@ -2777,7 +2752,7 @@ func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly
 		base := filepath.Base(f.Name)
 
 		if levels > -1 && strings.Count(f.Name, sep) > levels {
-			return true
+			continue
 		}
 
 		parent := root
@@ -2785,28 +2760,22 @@ func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly
 			for _, path := range strings.Split(dir, sep) {
 				child := findByName(parent.Children, path)
 				if child == nil {
-					err = fmt.Errorf("could not find child '%s' for path '%s' in parent '%s'", path, f.Name, parent.Name)
-					return false
+					return nil, fmt.Errorf("could not find child '%s' for path '%s' in parent '%s'", path, f.Name, parent.Name)
 				}
 				parent = child
 			}
 		}
 
 		if dirsOnly && !f.IsDirectory() {
-			return true
+			continue
 		}
 
 		parent.Children = append(parent.Children, &TreeEntry{
 			Name:    base,
 			Type:    f.Type.String(),
 			ModTime: f.ModTime(),
-			Size:    f.FileSize(),
+			Size:    f.Size,
 		})
-
-		return true
-	})
-	if err != nil {
-		return nil, err
 	}
 
 	return root.Children, nil
@@ -2860,46 +2829,42 @@ func (m *model) Availability(folder string, file protocol.FileInfo, block protoc
 	m.mut.RLock()
 	defer m.mut.RUnlock()
 
-	fs, ok := m.folderFiles[folder]
-	cfg := m.folderCfgs[folder]
-
+	cfg, ok := m.folderCfgs[folder]
 	if !ok {
 		return nil, ErrFolderMissing
 	}
 
-	snap, err := fs.Snapshot()
-	if err != nil {
-		return nil, err
-	}
-	defer snap.Release()
-
-	return m.blockAvailabilityRLocked(cfg, snap, file, block), nil
+	return m.blockAvailabilityRLocked(cfg, file, block), nil
 }
 
-func (m *model) blockAvailability(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
+func (m *model) blockAvailability(cfg config.FolderConfiguration, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
 	m.mut.RLock()
 	defer m.mut.RUnlock()
-	return m.blockAvailabilityRLocked(cfg, snap, file, block)
+	return m.blockAvailabilityRLocked(cfg, file, block)
 }
 
-func (m *model) blockAvailabilityRLocked(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
+func (m *model) blockAvailabilityRLocked(cfg config.FolderConfiguration, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
 	var candidates []Availability
 
-	candidates = append(candidates, m.fileAvailabilityRLocked(cfg, snap, file)...)
+	candidates = append(candidates, m.fileAvailabilityRLocked(cfg, file)...)
 	candidates = append(candidates, m.blockAvailabilityFromTemporaryRLocked(cfg, file, block)...)
 
 	return candidates
 }
 
-func (m *model) fileAvailability(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo) []Availability {
+func (m *model) fileAvailability(cfg config.FolderConfiguration, file protocol.FileInfo) []Availability {
 	m.mut.RLock()
 	defer m.mut.RUnlock()
-	return m.fileAvailabilityRLocked(cfg, snap, file)
+	return m.fileAvailabilityRLocked(cfg, file)
 }
 
-func (m *model) fileAvailabilityRLocked(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo) []Availability {
+func (m *model) fileAvailabilityRLocked(cfg config.FolderConfiguration, file protocol.FileInfo) []Availability {
 	var availabilities []Availability
-	for _, device := range snap.Availability(file.Name) {
+	devs, err := m.sdb.GetGlobalAvailability(cfg.ID, file.Name)
+	if err != nil {
+		return nil
+	}
+	for _, device := range devs {
 		if _, ok := m.remoteFolderStates[device]; !ok {
 			continue
 		}
@@ -2936,15 +2901,14 @@ func (m *model) BringToFront(folder, file string) {
 }
 
 func (m *model) ResetFolder(folder string) error {
-	m.mut.RLock()
-	defer m.mut.RUnlock()
+	m.mut.Lock()
+	defer m.mut.Unlock()
 	_, ok := m.folderRunners.Get(folder)
 	if ok {
 		return errors.New("folder must be paused when resetting")
 	}
 	l.Infof("Cleaning metadata for reset folder %q", folder)
-	db.DropFolder(m.db, folder)
-	return nil
+	return m.sdb.DropFolder(folder)
 }
 
 func (m *model) String() string {
@@ -3058,7 +3022,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	for deviceID, toCfg := range toDevices {
 		fromCfg, ok := fromDevices[deviceID]
 		if !ok {
-			sr := stats.NewDeviceStatisticsReference(m.db, deviceID)
+			sr := stats.NewDeviceStatisticsReference(db.NewTyped(m.sdb, "devicestats/"+deviceID.String()))
 			m.mut.Lock()
 			m.deviceStatRefs[deviceID] = sr
 			m.mut.Unlock()
@@ -3151,7 +3115,7 @@ func (m *model) setConnRequestLimitersLocked(cfg config.DeviceConfiguration) {
 
 func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.DeviceConfiguration, existingFolders map[string]config.FolderConfiguration, ignoredDevices deviceIDSet, removedFolders map[string]struct{}) {
 	var removedPendingFolders []map[string]string
-	pendingFolders, err := m.db.PendingFolders()
+	pendingFolders, err := m.observed.PendingFolders()
 	if err != nil {
 		msg := "Could not iterate through pending folder entries for cleanup"
 		l.Warnf("%v: %v", msg, err)
@@ -3164,7 +3128,7 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
 			// folders as well, assuming the folder is no longer of interest
 			// at all (but might become pending again).
 			l.Debugf("Discarding pending removed folder %v from all devices", folderID)
-			if err := m.db.RemovePendingFolder(folderID); err != nil {
+			if err := m.observed.RemovePendingFolder(folderID); err != nil {
 				msg := "Failed to remove pending folder entry"
 				l.Warnf("%v (%v): %v", msg, folderID, err)
 				m.evLogger.Log(events.Failure, msg)
@@ -3191,7 +3155,7 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
 			}
 			continue
 		removeFolderForDevice:
-			if err := m.db.RemovePendingFolderForDevice(folderID, deviceID); err != nil {
+			if err := m.observed.RemovePendingFolderForDevice(folderID, deviceID); err != nil {
 				msg := "Failed to remove pending folder-device entry"
 				l.Warnf("%v (%v, %v): %v", msg, folderID, deviceID, err)
 				m.evLogger.Log(events.Failure, msg)
@@ -3210,7 +3174,7 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
 	}
 
 	var removedPendingDevices []map[string]string
-	pendingDevices, err := m.db.PendingDevices()
+	pendingDevices, err := m.observed.PendingDevices()
 	if err != nil {
 		msg := "Could not iterate through pending device entries for cleanup"
 		l.Warnf("%v: %v", msg, err)
@@ -3228,7 +3192,7 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
 		}
 		continue
 	removeDevice:
-		if err := m.db.RemovePendingDevice(deviceID); err != nil {
+		if err := m.observed.RemovePendingDevice(deviceID); err != nil {
 			msg := "Failed to remove pending device entry"
 			l.Warnf("%v: %v", msg, err)
 			m.evLogger.Log(events.Failure, msg)
@@ -3265,20 +3229,20 @@ func (m *model) checkFolderRunningRLocked(folder string) error {
 
 // PendingDevices lists unknown devices that tried to connect.
 func (m *model) PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error) {
-	return m.db.PendingDevices()
+	return m.observed.PendingDevices()
 }
 
 // PendingFolders lists folders that we don't yet share with the offering devices.  It
 // returns the entries grouped by folder and filters for a given device unless the
 // argument is specified as EmptyDeviceID.
 func (m *model) PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error) {
-	return m.db.PendingFoldersForDevice(device)
+	return m.observed.PendingFoldersForDevice(device)
 }
 
 // DismissPendingDevices removes the record of a specific pending device.
 func (m *model) DismissPendingDevice(device protocol.DeviceID) error {
 	l.Debugf("Discarding pending device %v", device)
-	err := m.db.RemovePendingDevice(device)
+	err := m.observed.RemovePendingDevice(device)
 	if err != nil {
 		return err
 	}
@@ -3298,7 +3262,7 @@ func (m *model) DismissPendingFolder(device protocol.DeviceID, folder string) er
 	var removedPendingFolders []map[string]string
 	if device == protocol.EmptyDeviceID {
 		l.Debugf("Discarding pending removed folder %s from all devices", folder)
-		err := m.db.RemovePendingFolder(folder)
+		err := m.observed.RemovePendingFolder(folder)
 		if err != nil {
 			return err
 		}
@@ -3307,7 +3271,7 @@ func (m *model) DismissPendingFolder(device protocol.DeviceID, folder string) er
 		}
 	} else {
 		l.Debugf("Discarding pending folder %s from device %v", folder, device)
-		err := m.db.RemovePendingFolderForDevice(folder, device)
+		err := m.observed.RemovePendingFolderForDevice(folder, device)
 		if err != nil {
 			return err
 		}
@@ -3436,7 +3400,7 @@ type storedEncryptionToken struct {
 }
 
 func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
-	fd, err := cfg.Filesystem(nil).Open(encryptionTokenPath(cfg))
+	fd, err := cfg.Filesystem().Open(encryptionTokenPath(cfg))
 	if err != nil {
 		return nil, err
 	}
@@ -3450,7 +3414,7 @@ func readEncryptionToken(cfg config.FolderConfiguration) ([]byte, error) {
 
 func writeEncryptionToken(token []byte, cfg config.FolderConfiguration) error {
 	tokenName := encryptionTokenPath(cfg)
-	fd, err := cfg.Filesystem(nil).OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0o666)
+	fd, err := cfg.Filesystem().OpenFile(tokenName, fs.OptReadWrite|fs.OptCreate, 0o666)
 	if err != nil {
 		return err
 	}
diff --git a/lib/model/progressemitter.go b/lib/model/progressemitter.go
index 4a291a5195c..6242c33b8c0 100644
--- a/lib/model/progressemitter.go
+++ b/lib/model/progressemitter.go
@@ -76,7 +76,6 @@ func (t *ProgressEmitter) Serve(ctx context.Context) error {
 			return nil
 		case <-t.timer.C:
 			t.mut.Lock()
-			l.Debugln("progress emitter: timer - looking after", len(t.registry))
 
 			newLastUpdated := lastUpdate
 			newCount = t.lenRegistryLocked()
@@ -94,8 +93,6 @@ func (t *ProgressEmitter) Serve(ctx context.Context) error {
 				lastCount = newCount
 				t.sendDownloadProgressEventLocked()
 				progressUpdates = t.computeProgressUpdates()
-			} else {
-				l.Debugln("progress emitter: nothing new")
 			}
 
 			if newCount != 0 {
@@ -247,7 +244,6 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
 	t.mut.Lock()
 	defer t.mut.Unlock()
 	if t.disabled {
-		l.Debugln("progress emitter: disabled, skip registering")
 		return
 	}
 	l.Debugln("progress emitter: registering", s.folder, s.file.Name)
@@ -266,7 +262,6 @@ func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
 	defer t.mut.Unlock()
 
 	if t.disabled {
-		l.Debugln("progress emitter: disabled, skip deregistering")
 		return
 	}
 
diff --git a/lib/model/queue.go b/lib/model/queue.go
index 16ac83c9434..7ca129c2611 100644
--- a/lib/model/queue.go
+++ b/lib/model/queue.go
@@ -7,10 +7,8 @@
 package model
 
 import (
-	"sort"
 	"time"
 
-	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/sync"
 )
 
@@ -127,13 +125,6 @@ func (q *jobQueue) Jobs(page, perpage int) ([]string, []string, int) {
 	return progress, queued, (page - 1) * perpage
 }
 
-func (q *jobQueue) Shuffle() {
-	q.mut.Lock()
-	defer q.mut.Unlock()
-
-	rand.Shuffle(q.queued)
-}
-
 func (q *jobQueue) Reset() {
 	q.mut.Lock()
 	defer q.mut.Unlock()
@@ -152,45 +143,3 @@ func (q *jobQueue) lenProgress() int {
 	defer q.mut.Unlock()
 	return len(q.progress)
 }
-
-func (q *jobQueue) SortSmallestFirst() {
-	q.mut.Lock()
-	defer q.mut.Unlock()
-
-	sort.Sort(smallestFirst(q.queued))
-}
-
-func (q *jobQueue) SortLargestFirst() {
-	q.mut.Lock()
-	defer q.mut.Unlock()
-
-	sort.Sort(sort.Reverse(smallestFirst(q.queued)))
-}
-
-func (q *jobQueue) SortOldestFirst() {
-	q.mut.Lock()
-	defer q.mut.Unlock()
-
-	sort.Sort(oldestFirst(q.queued))
-}
-
-func (q *jobQueue) SortNewestFirst() {
-	q.mut.Lock()
-	defer q.mut.Unlock()
-
-	sort.Sort(sort.Reverse(oldestFirst(q.queued)))
-}
-
-// The usual sort.Interface boilerplate
-
-type smallestFirst []jobQueueEntry
-
-func (q smallestFirst) Len() int           { return len(q) }
-func (q smallestFirst) Less(a, b int) bool { return q[a].size < q[b].size }
-func (q smallestFirst) Swap(a, b int)      { q[a], q[b] = q[b], q[a] }
-
-type oldestFirst []jobQueueEntry
-
-func (q oldestFirst) Len() int           { return len(q) }
-func (q oldestFirst) Less(a, b int) bool { return q[a].modified < q[b].modified }
-func (q oldestFirst) Swap(a, b int)      { q[a], q[b] = q[b], q[a] }
diff --git a/lib/protocol/bep_fileinfo.go b/lib/protocol/bep_fileinfo.go
index f29fd1ae720..7925d14d699 100644
--- a/lib/protocol/bep_fileinfo.go
+++ b/lib/protocol/bep_fileinfo.go
@@ -19,10 +19,12 @@ import (
 
 // FileInfo.LocalFlags flags
 const (
-	FlagLocalUnsupported = 1 << 0 // The kind is unsupported, e.g. symlinks on Windows
-	FlagLocalIgnored     = 1 << 1 // Matches local ignore patterns
-	FlagLocalMustRescan  = 1 << 2 // Doesn't match content on disk, must be rechecked fully
-	FlagLocalReceiveOnly = 1 << 3 // Change detected on receive only folder
+	FlagLocalUnsupported = 1 << 0 // 1: The kind is unsupported, e.g. symlinks on Windows
+	FlagLocalIgnored     = 1 << 1 // 2: Matches local ignore patterns
+	FlagLocalMustRescan  = 1 << 2 // 4: Doesn't match content on disk, must be rechecked fully
+	FlagLocalReceiveOnly = 1 << 3 // 8: Change detected on receive only folder
+	FlagLocalGlobal      = 1 << 4 // 16: This is the global file version
+	FlagLocalNeeded      = 1 << 5 // 32: We need this file
 
 	// Flags that should result in the Invalid bit on outgoing updates
 	LocalInvalidFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly
@@ -32,7 +34,7 @@ const (
 	// disk.
 	LocalConflictFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalReceiveOnly
 
-	LocalAllFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly
+	LocalAllFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly | FlagLocalGlobal | FlagLocalNeeded
 )
 
 // BlockSizes is the list of valid block sizes, from min to max
diff --git a/lib/protocol/vector.go b/lib/protocol/vector.go
index 30cce42314d..91ee274f79a 100644
--- a/lib/protocol/vector.go
+++ b/lib/protocol/vector.go
@@ -7,6 +7,11 @@
 package protocol
 
 import (
+	"encoding/binary"
+	"encoding/hex"
+	"fmt"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/syncthing/syncthing/internal/gen/bep"
@@ -20,6 +25,17 @@ type Vector struct {
 	Counters []Counter
 }
 
+func (v *Vector) String() string {
+	var buf strings.Builder
+	for i, c := range v.Counters {
+		if i > 0 {
+			buf.WriteRune(',')
+		}
+		fmt.Fprintf(&buf, "%x:%d", c.ID, c.Value)
+	}
+	return buf.String()
+}
+
 func (v *Vector) ToWire() *bep.Vector {
 	counters := make([]*bep.Counter, len(v.Counters))
 	for i, c := range v.Counters {
@@ -42,6 +58,31 @@ func VectorFromWire(w *bep.Vector) Vector {
 	return v
 }
 
+func VectorFromString(s string) (Vector, error) {
+	pairs := strings.Split(s, ",")
+	var v Vector
+	v.Counters = make([]Counter, len(pairs))
+	for i, pair := range pairs {
+		idStr, valStr, ok := strings.Cut(pair, ":")
+		if !ok {
+			return Vector{}, fmt.Errorf("bad pair %q", pair)
+		}
+		idslice, err := hex.DecodeString(idStr)
+		if err != nil {
+			return Vector{}, fmt.Errorf("bad id in pair %q", pair)
+		}
+		var idbs [8]byte
+		copy(idbs[8-len(idslice):], idslice)
+		id := binary.BigEndian.Uint64(idbs[:])
+		val, err := strconv.ParseUint(valStr, 10, 64)
+		if err != nil {
+			return Vector{}, fmt.Errorf("bad val in pair %q", pair)
+		}
+		v.Counters[i] = Counter{ID: ShortID(id), Value: val}
+	}
+	return v, nil
+}
+
 // Counter represents a single counter in the version vector.
 type Counter struct {
 	ID    ShortID
diff --git a/lib/relay/protocol/packets_xdr.go b/lib/relay/protocol/packets_xdr.go
index fb0903b6228..0477c066b41 100644
--- a/lib/relay/protocol/packets_xdr.go
+++ b/lib/relay/protocol/packets_xdr.go
@@ -31,7 +31,7 @@ struct header {
 
 */
 
-func (header) XDRSize() int {
+func (o header) XDRSize() int {
 	return 4 + 4 + 4
 }
 
@@ -60,7 +60,6 @@ func (o *header) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *header) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.magic = u.UnmarshalUint32()
 	o.messageType = int32(u.UnmarshalUint32())
@@ -79,27 +78,26 @@ struct Ping {
 
 */
 
-func (Ping) XDRSize() int {
+func (o Ping) XDRSize() int {
 	return 0
 }
-
-func (Ping) MarshalXDR() ([]byte, error) {
+func (o Ping) MarshalXDR() ([]byte, error) {
 	return nil, nil
 }
 
-func (Ping) MustMarshalXDR() []byte {
+func (o Ping) MustMarshalXDR() []byte {
 	return nil
 }
 
-func (Ping) MarshalXDRInto(_ *xdr.Marshaller) error {
+func (o Ping) MarshalXDRInto(m *xdr.Marshaller) error {
 	return nil
 }
 
-func (*Ping) UnmarshalXDR(_ []byte) error {
+func (o *Ping) UnmarshalXDR(bs []byte) error {
 	return nil
 }
 
-func (*Ping) UnmarshalXDRFrom(_ *xdr.Unmarshaller) error {
+func (o *Ping) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	return nil
 }
 
@@ -114,27 +112,26 @@ struct Pong {
 
 */
 
-func (Pong) XDRSize() int {
+func (o Pong) XDRSize() int {
 	return 0
 }
-
-func (Pong) MarshalXDR() ([]byte, error) {
+func (o Pong) MarshalXDR() ([]byte, error) {
 	return nil, nil
 }
 
-func (Pong) MustMarshalXDR() []byte {
+func (o Pong) MustMarshalXDR() []byte {
 	return nil
 }
 
-func (Pong) MarshalXDRInto(_ *xdr.Marshaller) error {
+func (o Pong) MarshalXDRInto(m *xdr.Marshaller) error {
 	return nil
 }
 
-func (*Pong) UnmarshalXDR(_ []byte) error {
+func (o *Pong) UnmarshalXDR(bs []byte) error {
 	return nil
 }
 
-func (*Pong) UnmarshalXDRFrom(_ *xdr.Unmarshaller) error {
+func (o *Pong) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	return nil
 }
 
@@ -149,27 +146,26 @@ struct RelayFull {
 
 */
 
-func (RelayFull) XDRSize() int {
+func (o RelayFull) XDRSize() int {
 	return 0
 }
-
-func (RelayFull) MarshalXDR() ([]byte, error) {
+func (o RelayFull) MarshalXDR() ([]byte, error) {
 	return nil, nil
 }
 
-func (RelayFull) MustMarshalXDR() []byte {
+func (o RelayFull) MustMarshalXDR() []byte {
 	return nil
 }
 
-func (RelayFull) MarshalXDRInto(_ *xdr.Marshaller) error {
+func (o RelayFull) MarshalXDRInto(m *xdr.Marshaller) error {
 	return nil
 }
 
-func (*RelayFull) UnmarshalXDR(_ []byte) error {
+func (o *RelayFull) UnmarshalXDR(bs []byte) error {
 	return nil
 }
 
-func (*RelayFull) UnmarshalXDRFrom(_ *xdr.Unmarshaller) error {
+func (o *RelayFull) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	return nil
 }
 
@@ -219,7 +215,6 @@ func (o *JoinRelayRequest) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *JoinRelayRequest) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.Token = u.UnmarshalString()
 	return u.Error
@@ -274,7 +269,6 @@ func (o *JoinSessionRequest) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *JoinSessionRequest) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.Key = u.UnmarshalBytesMax(32)
 	return u.Error
@@ -331,7 +325,6 @@ func (o *Response) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *Response) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.Code = int32(u.UnmarshalUint32())
 	o.Message = u.UnmarshalString()
@@ -387,7 +380,6 @@ func (o *ConnectRequest) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *ConnectRequest) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.ID = u.UnmarshalBytesMax(32)
 	return u.Error
@@ -470,7 +462,6 @@ func (o *SessionInvitation) UnmarshalXDR(bs []byte) error {
 	u := &xdr.Unmarshaller{Data: bs}
 	return o.UnmarshalXDRFrom(u)
 }
-
 func (o *SessionInvitation) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
 	o.From = u.UnmarshalBytesMax(32)
 	o.Key = u.UnmarshalBytesMax(32)
diff --git a/lib/stats/debug.go b/lib/stats/debug.go
deleted file mode 100644
index 0681277df9a..00000000000
--- a/lib/stats/debug.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package stats
-
-import (
-	"github.com/syncthing/syncthing/lib/logger"
-)
-
-var l = logger.DefaultLogger.NewFacility("stats", "Persistent device and folder statistics")
diff --git a/lib/stats/device.go b/lib/stats/device.go
index e6d97f68328..d8d13a3da8b 100644
--- a/lib/stats/device.go
+++ b/lib/stats/device.go
@@ -9,9 +9,7 @@ package stats
 import (
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/internal/db"
 )
 
 const (
@@ -25,19 +23,17 @@ type DeviceStatistics struct {
 }
 
 type DeviceStatisticsReference struct {
-	ns     *db.NamespacedKV
-	device protocol.DeviceID
+	kv *db.Typed
 }
 
-func NewDeviceStatisticsReference(dba backend.Backend, device protocol.DeviceID) *DeviceStatisticsReference {
+func NewDeviceStatisticsReference(kv *db.Typed) *DeviceStatisticsReference {
 	return &DeviceStatisticsReference{
-		ns:     db.NewDeviceStatisticsNamespace(dba, device.String()),
-		device: device,
+		kv: kv,
 	}
 }
 
 func (s *DeviceStatisticsReference) GetLastSeen() (time.Time, error) {
-	t, ok, err := s.ns.Time(lastSeenKey)
+	t, ok, err := s.kv.Time(lastSeenKey)
 	if err != nil {
 		return time.Time{}, err
 	} else if !ok {
@@ -45,29 +41,25 @@ func (s *DeviceStatisticsReference) GetLastSeen() (time.Time, error) {
 		// time.Time{} from s.ns
 		return time.Unix(0, 0), nil
 	}
-	l.Debugln("stats.DeviceStatisticsReference.GetLastSeen:", s.device, t)
 	return t, nil
 }
 
 func (s *DeviceStatisticsReference) GetLastConnectionDuration() (time.Duration, error) {
-	d, ok, err := s.ns.Int64(connDurationKey)
+	d, ok, err := s.kv.Int64(connDurationKey)
 	if err != nil {
 		return 0, err
 	} else if !ok {
 		return 0, nil
 	}
-	l.Debugln("stats.DeviceStatisticsReference.GetLastConnectionDuration:", s.device, d)
 	return time.Duration(d), nil
 }
 
 func (s *DeviceStatisticsReference) WasSeen() error {
-	l.Debugln("stats.DeviceStatisticsReference.WasSeen:", s.device)
-	return s.ns.PutTime(lastSeenKey, time.Now().Truncate(time.Second))
+	return s.kv.PutTime(lastSeenKey, time.Now().Truncate(time.Second))
 }
 
 func (s *DeviceStatisticsReference) LastConnectionDuration(d time.Duration) error {
-	l.Debugln("stats.DeviceStatisticsReference.LastConnectionDuration:", s.device, d)
-	return s.ns.PutInt64(connDurationKey, d.Nanoseconds())
+	return s.kv.PutInt64(connDurationKey, d.Nanoseconds())
 }
 
 func (s *DeviceStatisticsReference) GetStatistics() (DeviceStatistics, error) {
diff --git a/lib/stats/folder.go b/lib/stats/folder.go
index 8e67c6e84d1..a8d58735934 100644
--- a/lib/stats/folder.go
+++ b/lib/stats/folder.go
@@ -9,7 +9,7 @@ package stats
 import (
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db"
+	"github.com/syncthing/syncthing/internal/db"
 )
 
 type FolderStatistics struct {
@@ -18,8 +18,7 @@ type FolderStatistics struct {
 }
 
 type FolderStatisticsReference struct {
-	ns     *db.NamespacedKV
-	folder string
+	kv *db.Typed
 }
 
 type LastFile struct {
@@ -28,27 +27,26 @@ type LastFile struct {
 	Deleted  bool      `json:"deleted"`
 }
 
-func NewFolderStatisticsReference(ldb *db.Lowlevel, folder string) *FolderStatisticsReference {
+func NewFolderStatisticsReference(kv *db.Typed) *FolderStatisticsReference {
 	return &FolderStatisticsReference{
-		ns:     db.NewFolderStatisticsNamespace(ldb, folder),
-		folder: folder,
+		kv: kv,
 	}
 }
 
 func (s *FolderStatisticsReference) GetLastFile() (LastFile, error) {
-	at, ok, err := s.ns.Time("lastFileAt")
+	at, ok, err := s.kv.Time("lastFileAt")
 	if err != nil {
 		return LastFile{}, err
 	} else if !ok {
 		return LastFile{}, nil
 	}
-	file, ok, err := s.ns.String("lastFileName")
+	file, ok, err := s.kv.String("lastFileName")
 	if err != nil {
 		return LastFile{}, err
 	} else if !ok {
 		return LastFile{}, nil
 	}
-	deleted, _, err := s.ns.Bool("lastFileDeleted")
+	deleted, _, err := s.kv.Bool("lastFileDeleted")
 	if err != nil {
 		return LastFile{}, err
 	}
@@ -60,25 +58,24 @@ func (s *FolderStatisticsReference) GetLastFile() (LastFile, error) {
 }
 
 func (s *FolderStatisticsReference) ReceivedFile(file string, deleted bool) error {
-	l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder, file)
-	if err := s.ns.PutTime("lastFileAt", time.Now().Truncate(time.Second)); err != nil {
+	if err := s.kv.PutTime("lastFileAt", time.Now().Truncate(time.Second)); err != nil {
 		return err
 	}
-	if err := s.ns.PutString("lastFileName", file); err != nil {
+	if err := s.kv.PutString("lastFileName", file); err != nil {
 		return err
 	}
-	if err := s.ns.PutBool("lastFileDeleted", deleted); err != nil {
+	if err := s.kv.PutBool("lastFileDeleted", deleted); err != nil {
 		return err
 	}
 	return nil
 }
 
 func (s *FolderStatisticsReference) ScanCompleted() error {
-	return s.ns.PutTime("lastScan", time.Now().Truncate(time.Second))
+	return s.kv.PutTime("lastScan", time.Now().Truncate(time.Second))
 }
 
 func (s *FolderStatisticsReference) GetLastScanTime() (time.Time, error) {
-	lastScan, ok, err := s.ns.Time("lastScan")
+	lastScan, ok, err := s.kv.Time("lastScan")
 	if err != nil {
 		return time.Time{}, err
 	} else if !ok {
diff --git a/lib/syncthing/globalmigrations.go b/lib/syncthing/globalmigrations.go
index cc49874974b..2589f1882ba 100644
--- a/lib/syncthing/globalmigrations.go
+++ b/lib/syncthing/globalmigrations.go
@@ -7,13 +7,8 @@
 package syncthing
 
 import (
-	"encoding/binary"
-	"io"
-
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/fs"
-	"github.com/syncthing/syncthing/lib/protocol"
 )
 
 const (
@@ -21,8 +16,8 @@ const (
 	globalMigrationDBKey   = "globalMigrationVersion"
 )
 
-func globalMigration(ll *db.Lowlevel, cfg config.Wrapper) error {
-	miscDB := db.NewMiscDataNamespace(ll)
+func globalMigration(kv db.KV, cfg config.Wrapper) error {
+	miscDB := db.NewMiscDB(kv)
 	prevVersion, _, err := miscDB.Int64(globalMigrationDBKey)
 	if err != nil {
 		return err
@@ -32,101 +27,7 @@ func globalMigration(ll *db.Lowlevel, cfg config.Wrapper) error {
 		return nil
 	}
 
-	if prevVersion < 1 {
-		if err := encryptionTrailerSizeMigration(ll, cfg); err != nil {
-			return err
-		}
-	}
+	// currently no migrations
 
 	return miscDB.PutInt64(globalMigrationDBKey, globalMigrationVersion)
 }
-
-func encryptionTrailerSizeMigration(ll *db.Lowlevel, cfg config.Wrapper) error {
-	encFolders := cfg.Folders()
-	for folderID, folderCfg := range cfg.Folders() {
-		if folderCfg.Type != config.FolderTypeReceiveEncrypted {
-			delete(encFolders, folderID)
-		}
-	}
-	if len(encFolders) == 0 {
-		return nil
-	}
-
-	l.Infoln("Running global migration to fix encryption file sizes")
-
-	// Trigger index re-transfer with fixed up sizes
-	db.DropDeltaIndexIDs(ll)
-
-	for folderID, folderCfg := range encFolders {
-		fset, err := db.NewFileSet(folderID, ll)
-		if err != nil {
-			return err
-		}
-		snap, err := fset.Snapshot()
-		if err != nil {
-			return err
-		}
-		batch := db.NewFileInfoBatch(func(files []protocol.FileInfo) error {
-			// As we can't touch the version, we need to first invalidate the
-			// files, and then re-add the modified valid files
-			invalidFiles := make([]protocol.FileInfo, len(files))
-			for i, f := range files {
-				f.SetUnsupported()
-				invalidFiles[i] = f
-			}
-			fset.Update(protocol.LocalDeviceID, invalidFiles)
-			fset.Update(protocol.LocalDeviceID, files)
-			return nil
-		})
-		filesystem := folderCfg.Filesystem(fset)
-		var innerErr error
-		snap.WithHave(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			size, err := sizeOfEncryptedTrailer(filesystem, fi.Name)
-			if err != nil {
-				// Best effort: If we fail to read a file, it will show as
-				// locally changed on next scan.
-				return true
-			}
-			fi.EncryptionTrailerSize = size
-			batch.Append(fi)
-			err = batch.FlushIfFull()
-			if err != nil {
-				innerErr = err
-				return false
-			}
-			return true
-		})
-		snap.Release()
-		if innerErr != nil {
-			return innerErr
-		}
-		err = batch.Flush()
-		if err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// sizeOfEncryptedTrailer returns the size of the encrypted trailer on disk.
-// This amount of bytes should be subtracted from the file size to get the
-// original file size.
-func sizeOfEncryptedTrailer(fs fs.Filesystem, name string) (int, error) {
-	f, err := fs.Open(name)
-	if err != nil {
-		return 0, err
-	}
-	defer f.Close()
-	if _, err := f.Seek(-4, io.SeekEnd); err != nil {
-		return 0, err
-	}
-	var buf [4]byte
-	if _, err := io.ReadFull(f, buf[:]); err != nil {
-		return 0, err
-	}
-	// The stored size is the size of the encrypted data.
-	size := int(binary.BigEndian.Uint32(buf[:]))
-	// We add the size of the length word itself as well.
-	return size + 4, nil
-}
diff --git a/lib/syncthing/internals.go b/lib/syncthing/internals.go
index 135b292634a..af82b9ac4e2 100644
--- a/lib/syncthing/internals.go
+++ b/lib/syncthing/internals.go
@@ -8,9 +8,10 @@ package syncthing
 
 import (
 	"context"
+	"iter"
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db"
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/stats"
@@ -23,6 +24,8 @@ type Internals struct {
 	model model.Model
 }
 
+type Counts = db.Counts
+
 func newInternals(model model.Model) *Internals {
 	return &Internals{
 		model: model,
@@ -77,14 +80,38 @@ func (m *Internals) PendingFolders(deviceID protocol.DeviceID) (map[string]db.Pe
 	return m.model.PendingFolders(deviceID)
 }
 
-func (m *Internals) DBSnapshot(folderID string) (*db.Snapshot, error) {
-	return m.model.DBSnapshot(folderID)
-}
-
 func (m *Internals) ScanFolderSubdirs(folderID string, paths []string) error {
 	return m.model.ScanFolderSubdirs(folderID, paths)
 }
 
+func (m *Internals) GlobalSize(folder string) (Counts, error) {
+	counts, err := m.model.GlobalSize(folder)
+	if err != nil {
+		return Counts{}, err
+	}
+	return counts, nil
+}
+
+func (m *Internals) LocalSize(folder string) (Counts, error) {
+	counts, err := m.model.LocalSize(folder, protocol.LocalDeviceID)
+	if err != nil {
+		return Counts{}, err
+	}
+	return counts, nil
+}
+
+func (m *Internals) NeedSize(folder string, device protocol.DeviceID) (Counts, error) {
+	counts, err := m.model.NeedSize(folder, device)
+	if err != nil {
+		return Counts{}, err
+	}
+	return counts, nil
+}
+
+func (m *Internals) AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error) {
+	return m.model.AllGlobalFiles(folder)
+}
+
 func (m *Internals) FolderProgressBytesCompleted(folder string) int64 {
 	return m.model.FolderProgressBytesCompleted(folder)
 }
diff --git a/lib/syncthing/syncthing.go b/lib/syncthing/syncthing.go
index 60ae452c966..866d3a5dd26 100644
--- a/lib/syncthing/syncthing.go
+++ b/lib/syncthing/syncthing.go
@@ -22,13 +22,12 @@ import (
 
 	"github.com/thejerf/suture/v4"
 
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/api"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
 	"github.com/syncthing/syncthing/lib/connections/registry"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/discover"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/locations"
@@ -52,21 +51,19 @@ const (
 )
 
 type Options struct {
-	AuditWriter    io.Writer
-	NoUpgrade      bool
-	ProfilerAddr   string
-	ResetDeltaIdxs bool
-	Verbose        bool
-	// null duration means use default value
-	DBRecheckInterval    time.Duration
-	DBIndirectGCInterval time.Duration
+	AuditWriter           io.Writer
+	NoUpgrade             bool
+	ProfilerAddr          string
+	ResetDeltaIdxs        bool
+	Verbose               bool
+	DBMaintenanceInterval time.Duration
 }
 
 type App struct {
 	myID              protocol.DeviceID
 	mainService       *suture.Supervisor
 	cfg               config.Wrapper
-	ll                *db.Lowlevel
+	sdb               db.DB
 	evLogger          events.Logger
 	cert              tls.Certificate
 	opts              Options
@@ -80,14 +77,10 @@ type App struct {
 	Internals *Internals
 }
 
-func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) {
-	ll, err := db.NewLowlevel(dbBackend, evLogger, db.WithRecheckInterval(opts.DBRecheckInterval), db.WithIndirectGCInterval(opts.DBIndirectGCInterval))
-	if err != nil {
-		return nil, err
-	}
+func New(cfg config.Wrapper, sdb db.DB, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) {
 	a := &App{
 		cfg:      cfg,
-		ll:       ll,
+		sdb:      sdb,
 		evLogger: evLogger,
 		opts:     opts,
 		cert:     cert,
@@ -124,7 +117,7 @@ func (a *App) Start() error {
 func (a *App) startup() error {
 	a.mainService.Add(ur.NewFailureHandler(a.cfg, a.evLogger))
 
-	a.mainService.Add(a.ll)
+	a.mainService.Add(a.sdb.Service(a.opts.DBMaintenanceInterval))
 
 	if a.opts.AuditWriter != nil {
 		a.mainService.Add(newAuditService(a.opts.AuditWriter, a.evLogger))
@@ -180,14 +173,12 @@ func (a *App) startup() error {
 	perf := ur.CpuBench(context.Background(), 3, 150*time.Millisecond)
 	l.Infof("Hashing performance is %.02f MB/s", perf)
 
-	if err := db.UpdateSchema(a.ll); err != nil {
-		l.Warnln("Database schema:", err)
-		return err
-	}
-
 	if a.opts.ResetDeltaIdxs {
 		l.Infoln("Reinitializing delta index IDs")
-		db.DropDeltaIndexIDs(a.ll)
+		if err := a.sdb.DropAllIndexIDs(); err != nil {
+			l.Warnln("Drop index IDs:", err)
+			return err
+		}
 	}
 
 	protectedFiles := []string{
@@ -198,17 +189,22 @@ func (a *App) startup() error {
 	}
 
 	// Remove database entries for folders that no longer exist in the config
-	folders := a.cfg.Folders()
-	for _, folder := range a.ll.ListFolders() {
-		if _, ok := folders[folder]; !ok {
+	cfgFolders := a.cfg.Folders()
+	dbFolders, err := a.sdb.ListFolders()
+	if err != nil {
+		l.Warnln("Listing folders:", err)
+		return err
+	}
+	for _, folder := range dbFolders {
+		if _, ok := cfgFolders[folder]; !ok {
 			l.Infof("Cleaning metadata for dropped folder %q", folder)
-			db.DropFolder(a.ll, folder)
+			a.sdb.DropFolder(folder)
 		}
 	}
 
 	// Grab the previously running version string from the database.
 
-	miscDB := db.NewMiscDataNamespace(a.ll)
+	miscDB := db.NewMiscDB(a.sdb)
 	prevVersion, _, err := miscDB.String("prevVersion")
 	if err != nil {
 		l.Warnln("Database:", err)
@@ -229,7 +225,10 @@ func (a *App) startup() error {
 		if a.cfg.Options().SendFullIndexOnUpgrade {
 			// Drop delta indexes in case we've changed random stuff we
 			// shouldn't have. We will resend our index on next connect.
-			db.DropDeltaIndexIDs(a.ll)
+			if err := a.sdb.DropAllIndexIDs(); err != nil {
+				l.Warnln("Drop index IDs:", err)
+				return err
+			}
 		}
 	}
 
@@ -238,13 +237,13 @@ func (a *App) startup() error {
 		miscDB.PutString("prevVersion", build.Version)
 	}
 
-	if err := globalMigration(a.ll, a.cfg); err != nil {
+	if err := globalMigration(a.sdb, a.cfg); err != nil {
 		l.Warnln("Global migration:", err)
 		return err
 	}
 
 	keyGen := protocol.NewKeyGenerator()
-	m := model.NewModel(a.cfg, a.myID, a.ll, protectedFiles, a.evLogger, keyGen)
+	m := model.NewModel(a.cfg, a.myID, a.sdb, protectedFiles, a.evLogger, keyGen)
 	a.Internals = newInternals(m)
 
 	a.mainService.Add(m)
@@ -333,7 +332,7 @@ func (a *App) wait(errChan <-chan error) {
 
 	done := make(chan struct{})
 	go func() {
-		a.ll.Close()
+		a.sdb.Close()
 		close(done)
 	}()
 	select {
@@ -399,7 +398,7 @@ func (a *App) stopWithErr(stopReason svcutil.ExitStatus, err error) svcutil.Exit
 	return a.exitStatus
 }
 
-func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, errors, systemLog logger.Recorder, miscDB *db.NamespacedKV) error {
+func (a *App) setupGUI(m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, errors, systemLog logger.Recorder, miscDB *db.Typed) error {
 	guiCfg := a.cfg.GUI()
 
 	if !guiCfg.Enabled {
diff --git a/lib/syncthing/utils.go b/lib/syncthing/utils.go
index 023bbd8f163..41fc7754567 100644
--- a/lib/syncthing/utils.go
+++ b/lib/syncthing/utils.go
@@ -12,9 +12,16 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"sync"
+	"time"
 
+	"github.com/syncthing/syncthing/internal/db"
+	newdb "github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/db/olddb"
+	"github.com/syncthing/syncthing/internal/db/olddb/backend"
+	"github.com/syncthing/syncthing/internal/db/sqlite"
+	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/locations"
@@ -150,6 +157,125 @@ func copyFile(src, dst string) error {
 	return nil
 }
 
-func OpenDBBackend(path string, tuning config.Tuning) (backend.Backend, error) {
-	return backend.Open(path, backend.Tuning(tuning))
+// Opens a database
+func OpenDatabase(path string) (newdb.DB, error) {
+	sql, err := sqlite.Open(path)
+	if err != nil {
+		return nil, err
+	}
+
+	sdb := newdb.MetricsWrap(sql)
+
+	return sdb, nil
+}
+
+// Attempts migration of the old (LevelDB-based) database type to the new (SQLite-based) type
+func TryMigrateDatabase() error {
+	oldDBDir := locations.Get(locations.LegacyDatabase)
+	if _, err := os.Lstat(oldDBDir); err != nil {
+		// No old database
+		return nil
+	}
+
+	be, err := backend.OpenLevelDBRO(oldDBDir)
+	if err != nil {
+		// Apparently, not a valid old database
+		return nil
+	}
+
+	sdb, err := sqlite.OpenForMigration(locations.Get(locations.Database))
+	if err != nil {
+		return err
+	}
+
+	miscDB := db.NewMiscDB(sdb)
+	if when, ok, err := miscDB.Time("migrated-from-leveldb-at"); err == nil && ok {
+		l.Warnf("Old-style database present but already migrated at %v; please manually move or remove %s.", when, oldDBDir)
+		return nil
+	}
+
+	l.Infoln("Migrating old-style database to SQLite; this may take a while...")
+	t0 := time.Now()
+
+	ll, err := olddb.NewLowlevel(be)
+	if err != nil {
+		return err
+	}
+
+	totFiles, totBlocks := 0, 0
+	for _, folder := range ll.ListFolders() {
+		// Start a writer routine
+		fis := make(chan protocol.FileInfo, 50)
+		var writeErr error
+		var wg sync.WaitGroup
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			var batch []protocol.FileInfo
+			files, blocks := 0, 0
+			t0 := time.Now()
+			t1 := time.Now()
+			for fi := range fis {
+				batch = append(batch, fi)
+				files++
+				blocks += len(fi.Blocks)
+				if len(batch) == 1000 {
+					writeErr = sdb.Update(folder, protocol.LocalDeviceID, batch)
+					if writeErr != nil {
+						return
+					}
+					batch = batch[:0]
+					if time.Since(t1) > 10*time.Second {
+						d := time.Since(t0) + 1
+						t1 = time.Now()
+						l.Infof("Migrating folder %s... (%d files and %dk blocks in %v, %.01f files/s)", folder, files, blocks/1000, d.Truncate(time.Second), float64(files)/d.Seconds())
+					}
+				}
+			}
+			if len(batch) > 0 {
+				writeErr = sdb.Update(folder, protocol.LocalDeviceID, batch)
+			}
+			d := time.Since(t0) + 1
+			l.Infof("Migrated folder %s; %d files and %dk blocks in %v, %.01f files/s", folder, files, blocks/1000, d.Truncate(time.Second), float64(files)/d.Seconds())
+			totFiles += files
+			totBlocks += blocks
+		}()
+
+		// Iterate the existing files
+		fs, err := olddb.NewFileSet(folder, ll)
+		if err != nil {
+			return err
+		}
+		snap, err := fs.Snapshot()
+		if err != nil {
+			return err
+		}
+		_ = snap.WithHaveSequence(0, func(fi protocol.FileInfo) bool {
+			fis <- fi
+			return true
+		})
+		close(fis)
+		snap.Release()
+
+		// Wait for writes to complete
+		wg.Wait()
+		if writeErr != nil {
+			return writeErr
+		}
+	}
+
+	l.Infoln("Migrating virtual mtimes...")
+	if err := ll.IterateMtimes(sdb.PutMtime); err != nil {
+		l.Warnln("Failed to migrate mtimes:", err)
+	}
+
+	_ = miscDB.PutTime("migrated-from-leveldb-at", time.Now())
+	_ = miscDB.PutString("migrated-from-leveldb-by", build.LongVersion)
+
+	be.Close()
+	sdb.Close()
+	_ = os.Rename(oldDBDir, oldDBDir+"-migrated")
+
+	l.Infof("Migration complete, %d files and %dk blocks in %s", totFiles, totBlocks/1000, time.Since(t0).Truncate(time.Second))
+	return nil
 }
diff --git a/lib/ur/usage_report.go b/lib/ur/usage_report.go
index a76e1512e0c..e6953699599 100644
--- a/lib/ur/usage_report.go
+++ b/lib/ur/usage_report.go
@@ -22,10 +22,10 @@ import (
 	"time"
 
 	"github.com/shirou/gopsutil/v4/process"
+	"github.com/syncthing/syncthing/internal/db"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
-	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/dialer"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/scanner"
@@ -41,7 +41,7 @@ const Version = 3
 var StartTime = time.Now().Truncate(time.Second)
 
 type Model interface {
-	DBSnapshot(folder string) (*db.Snapshot, error)
+	GlobalSize(folder string) (db.Counts, error)
 	UsageReportingStats(report *contract.Report, version int, preview bool)
 }
 
@@ -83,12 +83,10 @@ func (s *Service) reportData(ctx context.Context, urVersion int, preview bool) (
 	var totFiles, maxFiles int
 	var totBytes, maxBytes int64
 	for folderID := range s.cfg.Folders() {
-		snap, err := s.model.DBSnapshot(folderID)
+		global, err := s.model.GlobalSize(folderID)
 		if err != nil {
-			continue
+			return nil, err
 		}
-		global := snap.GlobalSize()
-		snap.Release()
 		totFiles += int(global.Files)
 		totBytes += global.Bytes
 		if int(global.Files) > maxFiles {
diff --git a/lib/versioner/external.go b/lib/versioner/external.go
index 2a5559408f3..07467cb9001 100644
--- a/lib/versioner/external.go
+++ b/lib/versioner/external.go
@@ -41,7 +41,7 @@ func newExternal(cfg config.FolderConfiguration) Versioner {
 
 	s := external{
 		command:    command,
-		filesystem: cfg.Filesystem(nil),
+		filesystem: cfg.Filesystem(),
 	}
 
 	l.Debugf("instantiated %#v", s)
diff --git a/lib/versioner/simple.go b/lib/versioner/simple.go
index f56a8279b4d..6a9f4d514e4 100644
--- a/lib/versioner/simple.go
+++ b/lib/versioner/simple.go
@@ -41,7 +41,7 @@ func newSimple(cfg config.FolderConfiguration) Versioner {
 	s := simple{
 		keep:            keep,
 		cleanoutDays:    cleanoutDays,
-		folderFs:        cfg.Filesystem(nil),
+		folderFs:        cfg.Filesystem(),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		copyRangeMethod: cfg.CopyRangeMethod.ToFS(),
 	}
diff --git a/lib/versioner/staggered.go b/lib/versioner/staggered.go
index e97c2e6d4b2..9b6ab5434a1 100644
--- a/lib/versioner/staggered.go
+++ b/lib/versioner/staggered.go
@@ -44,7 +44,7 @@ func newStaggered(cfg config.FolderConfiguration) Versioner {
 	versionsFs := versionerFsFromFolderCfg(cfg)
 
 	s := &staggered{
-		folderFs:   cfg.Filesystem(nil),
+		folderFs:   cfg.Filesystem(),
 		versionsFs: versionsFs,
 		interval: [4]interval{
 			{30, 60 * 60},                     // first hour -> 30 sec between versions
diff --git a/lib/versioner/trashcan.go b/lib/versioner/trashcan.go
index 7378c3a3352..2e228900dbd 100644
--- a/lib/versioner/trashcan.go
+++ b/lib/versioner/trashcan.go
@@ -33,7 +33,7 @@ func newTrashcan(cfg config.FolderConfiguration) Versioner {
 	// On error we default to 0, "do not clean out the trash can"
 
 	s := &trashcan{
-		folderFs:        cfg.Filesystem(nil),
+		folderFs:        cfg.Filesystem(),
 		versionsFs:      versionerFsFromFolderCfg(cfg),
 		cleanoutDays:    cleanoutDays,
 		copyRangeMethod: cfg.CopyRangeMethod.ToFS(),
diff --git a/lib/versioner/util.go b/lib/versioner/util.go
index 4693d1d9c33..41b4aaf3249 100644
--- a/lib/versioner/util.go
+++ b/lib/versioner/util.go
@@ -259,7 +259,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
 }
 
 func versionerFsFromFolderCfg(cfg config.FolderConfiguration) (versionsFs fs.Filesystem) {
-	folderFs := cfg.Filesystem(nil)
+	folderFs := cfg.Filesystem()
 	if cfg.Versioning.FSPath == "" {
 		versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), DefaultPath))
 	} else if cfg.Versioning.FSType == config.FilesystemTypeBasic {


Test patch:
diff --git a/internal/db/sqlite/db_bench_test.go b/internal/db/sqlite/db_bench_test.go
new file mode 100644
index 00000000000..5e0f7957450
--- /dev/null
+++ b/internal/db/sqlite/db_bench_test.go
@@ -0,0 +1,243 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/timeutil"
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/rand"
+)
+
+var globalFi protocol.FileInfo
+
+func BenchmarkUpdate(b *testing.B) {
+	db, err := OpenTemp()
+	if err != nil {
+		b.Fatal(err)
+	}
+	b.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			b.Fatal(err)
+		}
+	})
+	svc := db.Service(time.Hour).(*Service)
+
+	fs := make([]protocol.FileInfo, 100)
+	seed := 0
+
+	size := 10000
+	for size < 200_000 {
+		t0 := time.Now()
+		if err := svc.periodic(context.Background()); err != nil {
+			b.Fatal(err)
+		}
+		b.Log("garbage collect in", time.Since(t0))
+
+		for {
+			local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
+			if err != nil {
+				b.Fatal(err)
+			}
+			if local.Files >= size {
+				break
+			}
+			fs := make([]protocol.FileInfo, 1000)
+			for i := range fs {
+				fs[i] = genFile(rand.String(24), 64, 0)
+			}
+			if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
+				b.Fatal(err)
+			}
+		}
+
+		b.Run(fmt.Sprintf("Insert100Loc@%d", size), func(b *testing.B) {
+			for range b.N {
+				for i := range fs {
+					fs[i] = genFile(rand.String(24), 64, 0)
+				}
+				if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("RepBlocks100@%d", size), func(b *testing.B) {
+			for range b.N {
+				for i := range fs {
+					fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
+					fs[i].Version = fs[i].Version.Update(42)
+				}
+				seed++
+				if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("RepSame100@%d", size), func(b *testing.B) {
+			for range b.N {
+				for i := range fs {
+					fs[i].Version = fs[i].Version.Update(42)
+				}
+				if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("Insert100Rem@%d", size), func(b *testing.B) {
+			for range b.N {
+				for i := range fs {
+					fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
+					fs[i].Version = fs[i].Version.Update(42)
+					fs[i].Sequence = timeutil.StrictlyMonotonicNanos()
+				}
+				if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("GetGlobal100@%d", size), func(b *testing.B) {
+			for range b.N {
+				for i := range fs {
+					_, ok, err := db.GetGlobalFile(folderID, fs[i].Name)
+					if err != nil {
+						b.Fatal(err)
+					}
+					if !ok {
+						b.Fatal("should exist")
+					}
+				}
+			}
+			b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("LocalSequenced@%d", size), func(b *testing.B) {
+			count := 0
+			for range b.N {
+				cur, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
+				if err != nil {
+					b.Fatal(err)
+				}
+				it, errFn := db.AllLocalFilesBySequence(folderID, protocol.LocalDeviceID, cur-100, 0)
+				for f := range it {
+					count++
+					globalFi = f
+				}
+				if err := errFn(); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("GetDeviceSequenceLoc@%d", size), func(b *testing.B) {
+			for range b.N {
+				_, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
+				if err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+		b.Run(fmt.Sprintf("GetDeviceSequenceRem@%d", size), func(b *testing.B) {
+			for range b.N {
+				_, err := db.GetDeviceSequence(folderID, protocol.DeviceID{42})
+				if err != nil {
+					b.Fatal(err)
+				}
+			}
+		})
+
+		b.Run(fmt.Sprintf("RemoteNeed@%d", size), func(b *testing.B) {
+			count := 0
+			for range b.N {
+				it, errFn := db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)
+				for f := range it {
+					count++
+					globalFi = f
+				}
+				if err := errFn(); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
+		})
+
+		b.Run(fmt.Sprintf("LocalNeed100Largest@%d", size), func(b *testing.B) {
+			count := 0
+			for range b.N {
+				it, errFn := db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderLargestFirst, 100, 0)
+				for f := range it {
+					globalFi = f
+					count++
+				}
+				if err := errFn(); err != nil {
+					b.Fatal(err)
+				}
+			}
+			b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
+		})
+
+		size <<= 1
+	}
+}
+
+func TestBenchmarkDropAllRemote(t *testing.T) {
+	if testing.Short() {
+		t.Skip("slow test")
+	}
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	fs := make([]protocol.FileInfo, 1000)
+	seq := 0
+	for {
+		local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if local.Files >= 15_000 {
+			break
+		}
+		for i := range fs {
+			seq++
+			fs[i] = genFile(rand.String(24), 64, seq)
+		}
+		if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
+			t.Fatal(err)
+		}
+		if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	t0 := time.Now()
+	if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
+		t.Fatal(err)
+	}
+	d := time.Since(t0)
+	t.Log("drop all took", d)
+}
diff --git a/internal/db/sqlite/db_global_test.go b/internal/db/sqlite/db_global_test.go
new file mode 100644
index 00000000000..44130ac77b0
--- /dev/null
+++ b/internal/db/sqlite/db_global_test.go
@@ -0,0 +1,493 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"slices"
+	"testing"
+
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func TestNeed(t *testing.T) {
+	t.Helper()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	var v protocol.Vector
+	baseV := v.Update(1)
+	newerV := baseV.Update(42)
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 0), // remote need
+		genFile("test2", 2, 0), // local need
+		genFile("test3", 3, 0), // global
+	}
+	files[0].Version = baseV
+	files[1].Version = baseV
+	files[2].Version = newerV
+	err = db.Update(folderID, protocol.LocalDeviceID, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Some remote files
+	remote := []protocol.FileInfo{
+		genFile("test2", 2, 100), // global
+		genFile("test3", 3, 101), // remote need
+		genFile("test4", 4, 102), // local need
+	}
+	remote[0].Version = newerV
+	remote[1].Version = baseV
+	remote[2].Version = newerV
+	err = db.Update(folderID, protocol.DeviceID{42}, remote)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// A couple are needed locally
+	localNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)))
+	if !slices.Equal(localNeed, []string{"test2", "test4"}) {
+		t.Log(localNeed)
+		t.Fatal("bad local need")
+	}
+
+	// Another couple are needed remotely
+	remoteNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)))
+	if !slices.Equal(remoteNeed, []string{"test1", "test3"}) {
+		t.Log(remoteNeed)
+		t.Fatal("bad remote need")
+	}
+}
+
+func TestDropRecalcsGlobal(t *testing.T) {
+	// When we drop a device we may get a new global
+
+	t.Parallel()
+
+	t.Run("DropAllFiles", func(t *testing.T) {
+		t.Parallel()
+
+		testDropWithDropper(t, func(t *testing.T, db *DB) {
+			t.Helper()
+			if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
+				t.Fatal(err)
+			}
+		})
+	})
+
+	t.Run("DropDevice", func(t *testing.T) {
+		t.Parallel()
+
+		testDropWithDropper(t, func(t *testing.T, db *DB) {
+			t.Helper()
+			if err := db.DropDevice(protocol.DeviceID{42}); err != nil {
+				t.Fatal(err)
+			}
+		})
+	})
+
+	t.Run("DropFilesNamed", func(t *testing.T) {
+		t.Parallel()
+
+		testDropWithDropper(t, func(t *testing.T, db *DB) {
+			t.Helper()
+			if err := db.DropFilesNamed(folderID, protocol.DeviceID{42}, []string{"test1", "test42"}); err != nil {
+				t.Fatal(err)
+			}
+		})
+	})
+}
+
+func testDropWithDropper(t *testing.T, dropper func(t *testing.T, db *DB)) {
+	t.Helper()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Some remote files
+	remote := []protocol.FileInfo{
+		genFile("test1", 3, 0),
+	}
+	remote[0].Version = remote[0].Version.Update(42)
+	err = db.Update(folderID, protocol.DeviceID{42}, remote)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Remote test1 wins as the global, verify.
+	count, err := db.CountGlobal(folderID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if count.Bytes != (2+3)*128<<10 {
+		t.Log(count)
+		t.Fatal("bad global size to begin with")
+	}
+	if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
+		t.Fatal("missing global to begin with")
+	} else if g.Size != 3*128<<10 {
+		t.Fatal("remote test1 should be the global")
+	}
+
+	// Now remove that remote device
+	dropper(t, db)
+
+	// Our test1 should now be the global
+	count, err = db.CountGlobal(folderID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if count.Bytes != (1+2)*128<<10 {
+		t.Log(count)
+		t.Fatal("bad global size after drop")
+	}
+	if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
+		t.Fatal("missing global after drop")
+	} else if g.Size != 1*128<<10 {
+		t.Fatal("local test1 should be the global")
+	}
+}
+
+func TestNeedDeleted(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// A remote deleted file
+	remote := []protocol.FileInfo{
+		genFile("test1", 1, 101),
+	}
+	remote[0].SetDeleted(42)
+	err = db.Update(folderID, protocol.DeviceID{42}, remote)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// We need the one deleted file
+	s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Bytes != 0 || s.Deleted != 1 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+}
+
+func TestDontNeedIgnored(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// A remote file
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 103),
+	}
+	err = db.Update(folderID, protocol.DeviceID{42}, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Which we've ignored locally
+	files[0].SetIgnored()
+	err = db.Update(folderID, protocol.LocalDeviceID, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// We don't need it
+	s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Bytes != 0 || s.Files != 0 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+
+	// It shouldn't show up in the need list
+	names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
+	if len(names) != 0 {
+		t.Log(names)
+		t.Error("need no files")
+	}
+}
+
+func TestRemoveDontNeedLocalIgnored(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// A local ignored file
+	file := genFile("test1", 1, 103)
+	file.SetIgnored()
+	files := []protocol.FileInfo{file}
+	err = db.Update(folderID, protocol.LocalDeviceID, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Which the remote doesn't have (no update)
+
+	// They don't need it
+	s, err := db.CountNeed(folderID, protocol.DeviceID{42})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Bytes != 0 || s.Files != 0 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+
+	// It shouldn't show up in their need list
+	names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
+	if len(names) != 0 {
+		t.Log(names)
+		t.Error("need no files")
+	}
+}
+
+func TestLocalDontNeedDeletedMissing(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// A remote deleted file
+	file := genFile("test1", 1, 103)
+	file.SetDeleted(42)
+	files := []protocol.FileInfo{file}
+	err = db.Update(folderID, protocol.DeviceID{42}, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Which we don't have (no local update)
+
+	// We don't need it
+	s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+
+	// It shouldn't show up in the need list
+	names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
+	if len(names) != 0 {
+		t.Log(names)
+		t.Error("need no files")
+	}
+}
+
+func TestRemoteDontNeedDeletedMissing(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// A local deleted file
+	file := genFile("test1", 1, 103)
+	file.SetDeleted(42)
+	files := []protocol.FileInfo{file}
+	err = db.Update(folderID, protocol.LocalDeviceID, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Which the remote doesn't have (no local update)
+
+	// They don't need it
+	s, err := db.CountNeed(folderID, protocol.DeviceID{42})
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+
+	// It shouldn't show up in their need list
+	names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
+	if len(names) != 0 {
+		t.Log(names)
+		t.Error("need no files")
+	}
+}
+
+func TestNeedRemoteSymlinkAndDir(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Two remote "specials", a symlink and a directory
+	var v protocol.Vector
+	v.Update(1)
+	files := []protocol.FileInfo{
+		{Name: "sym", Type: protocol.FileInfoTypeSymlink, Sequence: 100, Version: v, Blocks: genBlocks("symlink", 0, 1)},
+		{Name: "dir", Type: protocol.FileInfoTypeDirectory, Sequence: 101, Version: v},
+	}
+	err = db.Update(folderID, protocol.DeviceID{42}, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// We need them
+	s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.Directories != 1 || s.Symlinks != 1 {
+		t.Log(s)
+		t.Error("bad need")
+	}
+
+	// They should be in the need list
+	names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
+	if len(names) != 2 {
+		t.Log(names)
+		t.Error("bad need")
+	}
+}
+
+func TestNeedPagination(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Several remote files
+	var v protocol.Vector
+	v.Update(1)
+	files := []protocol.FileInfo{
+		genFile("test0", 1, 100),
+		genFile("test1", 1, 101),
+		genFile("test2", 1, 102),
+		genFile("test3", 1, 103),
+		genFile("test4", 1, 104),
+		genFile("test5", 1, 105),
+		genFile("test6", 1, 106),
+		genFile("test7", 1, 107),
+		genFile("test8", 1, 108),
+		genFile("test9", 1, 109),
+	}
+	err = db.Update(folderID, protocol.DeviceID{42}, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// We should get the first two
+	names := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 2, 0)))
+	if !slices.Equal(names, []string{"test0", "test1"}) {
+		t.Log(names)
+		t.Error("bad need")
+	}
+
+	// We should get the next three
+	names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 3, 2)))
+	if !slices.Equal(names, []string{"test2", "test3", "test4"}) {
+		t.Log(names)
+		t.Error("bad need")
+	}
+
+	// We should get the last five
+	names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 5, 5)))
+	if !slices.Equal(names, []string{"test5", "test6", "test7", "test8", "test9"}) {
+		t.Log(names)
+		t.Error("bad need")
+	}
+}
diff --git a/internal/db/sqlite/db_indexid_test.go b/internal/db/sqlite/db_indexid_test.go
new file mode 100644
index 00000000000..82116734a13
--- /dev/null
+++ b/internal/db/sqlite/db_indexid_test.go
@@ -0,0 +1,81 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"testing"
+
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func TestIndexIDs(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal()
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("LocalDeviceID", func(t *testing.T) {
+		t.Parallel()
+
+		localID, err := db.GetIndexID("foo", protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if localID == 0 {
+			t.Fatal("should have been generated")
+		}
+
+		again, err := db.GetIndexID("foo", protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if again != localID {
+			t.Fatal("should get same again")
+		}
+
+		other, err := db.GetIndexID("bar", protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if other == localID {
+			t.Fatal("should not get same for other folder")
+		}
+	})
+
+	t.Run("OtherDeviceID", func(t *testing.T) {
+		t.Parallel()
+
+		localID, err := db.GetIndexID("foo", protocol.DeviceID{42})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if localID != 0 {
+			t.Fatal("should have been zero")
+		}
+
+		newID := protocol.NewIndexID()
+		if err := db.SetIndexID("foo", protocol.DeviceID{42}, newID); err != nil {
+			t.Fatal(err)
+		}
+
+		again, err := db.GetIndexID("foo", protocol.DeviceID{42})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if again != newID {
+			t.Log(again, newID)
+			t.Fatal("should get the ID we set")
+		}
+	})
+}
diff --git a/internal/db/sqlite/db_local_test.go b/internal/db/sqlite/db_local_test.go
new file mode 100644
index 00000000000..8444a07d877
--- /dev/null
+++ b/internal/db/sqlite/db_local_test.go
@@ -0,0 +1,202 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"testing"
+
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func TestBlocks(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal()
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	files := []protocol.FileInfo{
+		{
+			Name: "file1",
+			Blocks: []protocol.BlockInfo{
+				{Hash: []byte{1, 2, 3}, Offset: 0, Size: 42},
+				{Hash: []byte{2, 3, 4}, Offset: 42, Size: 42},
+				{Hash: []byte{3, 4, 5}, Offset: 84, Size: 42},
+			},
+		},
+		{
+			Name: "file2",
+			Blocks: []protocol.BlockInfo{
+				{Hash: []byte{2, 3, 4}, Offset: 0, Size: 42},
+				{Hash: []byte{3, 4, 5}, Offset: 42, Size: 42},
+				{Hash: []byte{4, 5, 6}, Offset: 84, Size: 42},
+			},
+		},
+	}
+
+	if err := db.Update("test", protocol.LocalDeviceID, files); err != nil {
+		t.Fatal(err)
+	}
+
+	// Search for blocks
+
+	vals, err := itererr.Collect(db.AllLocalBlocksWithHash([]byte{1, 2, 3}))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(vals) != 1 {
+		t.Log(vals)
+		t.Fatal("expected one hit")
+	} else if vals[0].BlockIndex != 0 || vals[0].Offset != 0 || vals[0].Size != 42 {
+		t.Log(vals[0])
+		t.Fatal("bad entry")
+	}
+
+	// Get FileInfos for those blocks
+
+	found := 0
+	it, errFn := db.AllLocalFilesWithBlocksHashAnyFolder(vals[0].BlocklistHash)
+	for folder, fileInfo := range it {
+		if folder != folderID {
+			t.Fatal("should be same folder")
+		}
+		if fileInfo.Name != "file1" {
+			t.Fatal("should be file1")
+		}
+		found++
+	}
+	if err := errFn(); err != nil {
+		t.Fatal(err)
+	}
+	if found != 1 {
+		t.Fatal("should find one file")
+	}
+
+	// Get the other blocks
+
+	vals, err = itererr.Collect(db.AllLocalBlocksWithHash([]byte{3, 4, 5}))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(vals) != 2 {
+		t.Log(vals)
+		t.Fatal("expected two hits")
+	}
+	// if vals[0].Index != 2 || vals[0].Offset != 84 || vals[0].Size != 42 {
+	// 	t.Log(vals[0])
+	// 	t.Fatal("bad entry 1")
+	// }
+	// if vals[1].Index != 1 || vals[1].Offset != 42 || vals[1].Size != 42 {
+	// 	t.Log(vals[1])
+	// 	t.Fatal("bad entry 2")
+	// }
+}
+
+func TestBlocksDeleted(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal()
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Insert a file
+	file := genFile("foo", 1, 0)
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+
+	// We should find one entry for the block hash
+	search := file.Blocks[0].Hash
+	es := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search))
+	if len(es) != 1 {
+		t.Fatal("expected one hit")
+	}
+
+	// Update the file with a new block hash
+	file.Blocks = genBlocks("foo", 42, 1)
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+
+	// Searching for the old hash should yield no hits
+	if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search)); len(hits) != 0 {
+		t.Log(hits)
+		t.Error("expected no hits")
+	}
+
+	// Searching for the new hash should yield one hits
+	if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(file.Blocks[0].Hash)); len(hits) != 1 {
+		t.Log(hits)
+		t.Error("expected one hit")
+	}
+}
+
+func TestRemoteSequence(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal()
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Insert a local file
+	file := genFile("foo", 1, 0)
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+
+	// Insert several remote files
+	file = genFile("foo1", 1, 42)
+	if err := sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+	if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+	file = genFile("foo2", 1, 43)
+	if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+	if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+	file = genFile("foo3", 1, 44)
+	if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
+		t.Fatal()
+	}
+
+	// Verify remote sequences
+	seqs, err := sdb.RemoteSequences(folderID)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(seqs) != 3 || seqs[protocol.DeviceID{42}] != 42 ||
+		seqs[protocol.DeviceID{43}] != 43 ||
+		seqs[protocol.DeviceID{44}] != 44 {
+		t.Log(seqs)
+		t.Error("bad seqs")
+	}
+}
diff --git a/internal/db/sqlite/db_mtimes_test.go b/internal/db/sqlite/db_mtimes_test.go
new file mode 100644
index 00000000000..71972eb5b3c
--- /dev/null
+++ b/internal/db/sqlite/db_mtimes_test.go
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"testing"
+	"time"
+)
+
+func TestMtimePairs(t *testing.T) {
+	t.Parallel()
+
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal()
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t0 := time.Now().Truncate(time.Second)
+	t1 := t0.Add(1234567890)
+
+	// Set a pair
+	if err := db.PutMtime("foo", "bar", t0, t1); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check it
+	gt0, gt1 := db.GetMtime("foo", "bar")
+	if !gt0.Equal(t0) || !gt1.Equal(t1) {
+		t.Log(t0, gt0)
+		t.Log(t1, gt1)
+		t.Log("bad times")
+	}
+
+	// Delete it
+	if err := db.DeleteMtime("foo", "bar"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check it
+	gt0, gt1 = db.GetMtime("foo", "bar")
+	if !gt0.IsZero() || !gt1.IsZero() {
+		t.Log(gt0, gt1)
+		t.Log("bad times")
+	}
+}
diff --git a/internal/db/sqlite/db_test.go b/internal/db/sqlite/db_test.go
new file mode 100644
index 00000000000..82749d4a9ca
--- /dev/null
+++ b/internal/db/sqlite/db_test.go
@@ -0,0 +1,1145 @@
+// Copyright (C) 2025 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package sqlite
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/binary"
+	"errors"
+	"iter"
+	"path/filepath"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
+	"github.com/syncthing/syncthing/internal/timeutil"
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+const (
+	folderID  = "test"
+	blockSize = 128 << 10
+	dirSize   = 128
+)
+
+func TestBasics(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	local := []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genDir("test2", 0),
+		genFile("test2/a", 2, 0),
+		genFile("test2/b", 3, 0),
+	}
+	err = sdb.Update(folderID, protocol.LocalDeviceID, local)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Some remote files
+	remote := []protocol.FileInfo{
+		genFile("test3", 3, 101),
+		genFile("test4", 4, 102),
+		genFile("test1", 5, 103),
+	}
+	// All newer than the local ones
+	for i := range remote {
+		remote[i].Version = remote[i].Version.Update(42)
+	}
+	err = sdb.Update(folderID, protocol.DeviceID{42}, remote)
+	if err != nil {
+		t.Fatal(err)
+	}
+	const (
+		localSize      = (1+2+3)*blockSize + dirSize
+		remoteSize     = (3 + 4 + 5) * blockSize
+		globalSize     = (2+3+3+4+5)*blockSize + dirSize
+		needSizeLocal  = remoteSize
+		needSizeRemote = (2+3)*blockSize + dirSize
+	)
+
+	t.Run("SchemaVersion", func(t *testing.T) {
+		ver, err := sdb.getAppliedSchemaVersion()
+		if err != nil {
+			t.Fatal(err)
+		}
+		if ver.SchemaVersion != currentSchemaVersion {
+			t.Log(ver)
+			t.Error("should be version 1")
+		}
+		if d := time.Since(ver.AppliedTime()); d > time.Minute || d < 0 {
+			t.Log(ver)
+			t.Error("suspicious applied tim")
+		}
+	})
+
+	t.Run("Local", func(t *testing.T) {
+		t.Parallel()
+
+		fi, ok, err := sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "test2/a") // exists
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !ok {
+			t.Fatal("not found")
+		}
+		if fi.Name != filepath.FromSlash("test2/a") {
+			t.Fatal("should have got test2/a")
+		}
+		if len(fi.Blocks) != 2 {
+			t.Fatal("expected two blocks")
+		}
+
+		_, ok, err = sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "test3") // does not exist
+		if err != nil {
+			t.Fatal(err)
+		}
+		if ok {
+			t.Fatal("should be not found")
+		}
+	})
+
+	t.Run("Global", func(t *testing.T) {
+		t.Parallel()
+
+		fi, ok, err := sdb.GetGlobalFile(folderID, "test1")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !ok {
+			t.Fatal("not found")
+		}
+		if fi.Size != 5*blockSize {
+			t.Fatal("should be the remote file")
+		}
+	})
+
+	t.Run("AllLocal", func(t *testing.T) {
+		t.Parallel()
+
+		have := mustCollect[protocol.FileInfo](t)(sdb.AllLocalFiles(folderID, protocol.LocalDeviceID))
+		if len(have) != 4 {
+			t.Log(have)
+			t.Error("expected four files")
+		}
+		have = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFiles(folderID, protocol.DeviceID{42}))
+		if len(have) != 3 {
+			t.Log(have)
+			t.Error("expected three files")
+		}
+	})
+
+	t.Run("AllNeededNamesLocal", func(t *testing.T) {
+		t.Parallel()
+
+		need := fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)))
+		if len(need) != 3 || need[0] != "test1" {
+			t.Log(need)
+			t.Error("expected three files, ordered alphabetically")
+		}
+
+		need = fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 1, 0)))
+		if len(need) != 1 || need[0] != "test1" {
+			t.Log(need)
+			t.Error("expected one file, limited, ordered alphabetically")
+		}
+		need = fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderLargestFirst, 0, 0)))
+		if len(need) != 3 || need[0] != "test1" { // largest
+			t.Log(need)
+			t.Error("expected three files, ordered largest to smallest")
+		}
+		need = fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderSmallestFirst, 0, 0)))
+		if len(need) != 3 || need[0] != "test3" { // smallest
+			t.Log(need)
+			t.Error("expected three files, ordered smallest to largest")
+		}
+
+		need = fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderNewestFirst, 0, 0)))
+		if len(need) != 3 || need[0] != "test1" { // newest
+			t.Log(need)
+			t.Error("expected three files, ordered newest to oldest")
+		}
+		need = fiNames(mustCollect[protocol.FileInfo](t)(sdb.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderOldestFirst, 0, 0)))
+		if len(need) != 3 || need[0] != "test3" { // oldest
+			t.Log(need)
+			t.Error("expected three files, ordered oldest to newest")
+		}
+	})
+
+	t.Run("LocalSize", func(t *testing.T) {
+		t.Parallel()
+
+		// Local device
+
+		c, err := sdb.CountLocal(folderID, protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if c.Files != 3 {
+			t.Log(c)
+			t.Error("one file expected")
+		}
+		if c.Directories != 1 {
+			t.Log(c)
+			t.Error("one directory expected")
+		}
+		if c.Bytes != localSize {
+			t.Log(c)
+			t.Error("size unexpected")
+		}
+
+		// Other device
+
+		c, err = sdb.CountLocal(folderID, protocol.DeviceID{42})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if c.Files != 3 {
+			t.Log(c)
+			t.Error("three files expected")
+		}
+		if c.Directories != 0 {
+			t.Log(c)
+			t.Error("no directories expected")
+		}
+		if c.Bytes != remoteSize {
+			t.Log(c)
+			t.Error("size unexpected")
+		}
+	})
+
+	t.Run("GlobalSize", func(t *testing.T) {
+		t.Parallel()
+
+		c, err := sdb.CountGlobal(folderID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if c.Files != 5 {
+			t.Log(c)
+			t.Error("five files expected")
+		}
+		if c.Directories != 1 {
+			t.Log(c)
+			t.Error("one directory expected")
+		}
+		if c.Bytes != int64(globalSize) {
+			t.Log(c)
+			t.Error("size unexpected")
+		}
+	})
+
+	t.Run("NeedSizeLocal", func(t *testing.T) {
+		t.Parallel()
+
+		c, err := sdb.CountNeed(folderID, protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if c.Files != 3 {
+			t.Log(c)
+			t.Error("three files expected")
+		}
+		if c.Directories != 0 {
+			t.Log(c)
+			t.Error("no directories expected")
+		}
+		if c.Bytes != needSizeLocal {
+			t.Log(c)
+			t.Error("size unexpected")
+		}
+	})
+
+	t.Run("NeedSizeRemote", func(t *testing.T) {
+		t.Parallel()
+
+		c, err := sdb.CountNeed(folderID, protocol.DeviceID{42})
+		if err != nil {
+			t.Fatal(err)
+		}
+		if c.Files != 2 {
+			t.Log(c)
+			t.Error("two files expected")
+		}
+		if c.Directories != 1 {
+			t.Log(c)
+			t.Error("one directory expected")
+		}
+		if c.Bytes != needSizeRemote {
+			t.Log(c)
+			t.Error("size unexpected")
+		}
+	})
+
+	t.Run("Folders", func(t *testing.T) {
+		t.Parallel()
+
+		folders, err := sdb.ListFolders()
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(folders) != 1 || folders[0] != folderID {
+			t.Error("expected one folder")
+		}
+	})
+
+	t.Run("DevicesForFolder", func(t *testing.T) {
+		t.Parallel()
+
+		devs, err := sdb.ListDevicesForFolder("test")
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(devs) != 1 || devs[0] != (protocol.DeviceID{42}) {
+			t.Log(devs)
+			t.Error("expected one device")
+		}
+	})
+
+	t.Run("Sequence", func(t *testing.T) {
+		t.Parallel()
+
+		iid, err := sdb.GetIndexID(folderID, protocol.LocalDeviceID)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if iid == 0 {
+			t.Log(iid)
+			t.Fatal("expected index ID")
+		}
+
+		if seq, err := sdb.GetDeviceSequence(folderID, protocol.LocalDeviceID); err != nil {
+			t.Fatal(err)
+		} else if seq != 4 {
+			t.Log(seq)
+			t.Error("expected local sequence to match number of files inserted")
+		}
+
+		if seq, err := sdb.GetDeviceSequence(folderID, protocol.DeviceID{42}); err != nil {
+			t.Fatal(err)
+		} else if seq != 103 {
+			t.Log(seq)
+			t.Error("expected remote sequence to match highest sent")
+		}
+
+		// Non-existent should be zero and no error
+		if seq, err := sdb.GetDeviceSequence("trolol", protocol.LocalDeviceID); err != nil {
+			t.Fatal(err)
+		} else if seq != 0 {
+			t.Log(seq)
+			t.Error("expected zero sequence")
+		}
+		if seq, err := sdb.GetDeviceSequence("trolol", protocol.DeviceID{42}); err != nil {
+			t.Fatal(err)
+		} else if seq != 0 {
+			t.Log(seq)
+			t.Error("expected zero sequence")
+		}
+		if seq, err := sdb.GetDeviceSequence(folderID, protocol.DeviceID{99}); err != nil {
+			t.Fatal(err)
+		} else if seq != 0 {
+			t.Log(seq)
+			t.Error("expected zero sequence")
+		}
+	})
+
+	t.Run("AllGlobalPrefix", func(t *testing.T) {
+		t.Parallel()
+
+		vals := mustCollect[db.FileMetadata](t)(sdb.AllGlobalFilesPrefix(folderID, "test2"))
+
+		// Vals should be test2, test2/a, test2/b
+		if len(vals) != 3 {
+			t.Log(vals)
+			t.Error("expected three items")
+		} else if vals[0].Name != "test2" {
+			t.Error(vals)
+		}
+
+		// Empty prefix should be all the files
+		vals = mustCollect[db.FileMetadata](t)(sdb.AllGlobalFilesPrefix(folderID, ""))
+		if len(vals) != 6 {
+			t.Log(vals)
+			t.Error("expected six items")
+		}
+	})
+
+	t.Run("AllLocalPrefix", func(t *testing.T) {
+		t.Parallel()
+
+		vals := mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "test2"))
+
+		// Vals should be test2, test2/a, test2/b
+		if len(vals) != 3 {
+			t.Log(vals)
+			t.Error("expected three items")
+		} else if vals[0].Name != "test2" {
+			t.Error(vals)
+		}
+
+		// Empty prefix should be all the files
+		vals = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, ""))
+
+		if len(vals) != 4 {
+			t.Log(vals)
+			t.Error("expected four items")
+		}
+	})
+
+	t.Run("AllLocalSequenced", func(t *testing.T) {
+		t.Parallel()
+
+		vals := mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesBySequence(folderID, protocol.LocalDeviceID, 3, 0))
+
+		// Vals should be test2/a, test2/b
+		if len(vals) != 2 {
+			t.Log(vals)
+			t.Error("expected three items")
+		} else if vals[0].Name != filepath.FromSlash("test2/a") || vals[0].Sequence != 3 {
+			t.Error(vals)
+		}
+	})
+}
+
+func TestPrefixGlobbing(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	local := []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genDir("test2", 0),
+		genFile("test2/a", 2, 0),
+		genDir("test2/b", 0),
+		genFile("test2/b/c", 3, 0),
+	}
+	err = sdb.Update(folderID, protocol.LocalDeviceID, local)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	vals := mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "test2"))
+
+	// Vals should be test2, test2/a, test2/b, test2/b/c
+	if len(vals) != 4 {
+		t.Log(vals)
+		t.Error("expected four items")
+	} else if vals[0].Name != "test2" || vals[3].Name != filepath.FromSlash("test2/b/c") {
+		t.Error(vals)
+	}
+
+	// Empty prefix should be all the files
+	vals = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, ""))
+
+	if len(vals) != 5 {
+		t.Log(vals)
+		t.Error("expected five items")
+	}
+
+	// Same as partial prefix
+	vals = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "tes"))
+
+	if len(vals) != 5 {
+		t.Log(vals)
+		t.Error("expected five items")
+	}
+
+	// Prefix should be case sensitive, so no match here
+	vals = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "tEsT2"))
+
+	if len(vals) != 0 {
+		t.Log(vals)
+		t.Error("expected no items")
+	}
+
+	// Subdir should match
+	vals = mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "test2/b"))
+
+	if len(vals) != 2 {
+		t.Log(vals)
+		t.Error("expected two items")
+	}
+}
+
+func TestPrefixGlobbingStar(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+	local := []protocol.FileInfo{
+		genFile("test1a", 1, 0),
+		genFile("test*a", 2, 0),
+		genFile("test2a", 3, 0),
+	}
+	err = sdb.Update(folderID, protocol.LocalDeviceID, local)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	vals := mustCollect[protocol.FileInfo](t)(sdb.AllLocalFilesWithPrefix(folderID, protocol.LocalDeviceID, "test*a"))
+
+	// Vals should be test*a
+	if len(vals) != 1 {
+		t.Log(vals)
+		t.Error("expected one item")
+	} else if vals[0].Name != "test*a" {
+		t.Error(vals)
+	}
+}
+
+func TestAvailability(t *testing.T) {
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	const folderID = "test"
+
+	// Some local files
+	err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Some remote files
+	err = db.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{
+		genFile("test2", 2, 1),
+		genFile("test3", 3, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Further remote files
+	err = db.Update(folderID, protocol.DeviceID{45}, []protocol.FileInfo{
+		genFile("test3", 3, 1),
+		genFile("test4", 4, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	a, err := db.GetGlobalAvailability(folderID, "test1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(a) != 0 {
+		t.Log(a)
+		t.Error("expected no availability (only local)")
+	}
+
+	a, err = db.GetGlobalAvailability(folderID, "test2")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(a) != 1 || a[0] != (protocol.DeviceID{42}) {
+		t.Log(a)
+		t.Error("expected one availability (only 42)")
+	}
+
+	a, err = db.GetGlobalAvailability(folderID, "test3")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(a) != 2 || a[0] != (protocol.DeviceID{42}) || a[1] != (protocol.DeviceID{45}) {
+		t.Log(a)
+		t.Error("expected two availabilities (both remotes)")
+	}
+
+	if err := db.Close(); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestDropFilesNamed(t *testing.T) {
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	const folderID = "test"
+
+	// Some local files
+	err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Drop test1
+	if err := db.DropFilesNamed(folderID, protocol.LocalDeviceID, []string{"test1"}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check
+	if _, ok, err := db.GetDeviceFile(folderID, protocol.LocalDeviceID, "test1"); err != nil || ok {
+		t.Log(err, ok)
+		t.Error("expected to not exist")
+	}
+	if c, err := db.CountLocal(folderID, protocol.LocalDeviceID); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 1 {
+		t.Log(c)
+		t.Error("expected count to be one")
+	}
+	if _, ok, err := db.GetDeviceFile(folderID, protocol.LocalDeviceID, "test2"); err != nil || !ok {
+		t.Log(err, ok)
+		t.Error("expected to exist")
+	}
+}
+
+func TestDropFolder(t *testing.T) {
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+
+	// Folder A
+	err = db.Update("a", protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Folder B
+	err = db.Update("b", protocol.LocalDeviceID, []protocol.FileInfo{
+		genFile("test1", 1, 0),
+		genFile("test2", 2, 0),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Drop A
+	if err := db.DropFolder("a"); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check
+	if _, ok, err := db.GetDeviceFile("a", protocol.LocalDeviceID, "test1"); err != nil || ok {
+		t.Log(err, ok)
+		t.Error("expected to not exist")
+	}
+	if c, err := db.CountLocal("a", protocol.LocalDeviceID); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 0 {
+		t.Log(c)
+		t.Error("expected count to be zero")
+	}
+
+	if _, ok, err := db.GetDeviceFile("b", protocol.LocalDeviceID, "test1"); err != nil || !ok {
+		t.Log(err, ok)
+		t.Error("expected to exist")
+	}
+	if c, err := db.CountLocal("b", protocol.LocalDeviceID); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 2 {
+		t.Log(c)
+		t.Error("expected count to be two")
+	}
+}
+
+func TestDropDevice(t *testing.T) {
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+
+	// Device 1
+	err = db.Update("a", protocol.DeviceID{1}, []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Device 2
+	err = db.Update("a", protocol.DeviceID{2}, []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Drop 1
+	if err := db.DropDevice(protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check
+	if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{1}, "test1"); err != nil || ok {
+		t.Log(err, ok)
+		t.Error("expected to not exist")
+	}
+	if c, err := db.CountLocal("a", protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 0 {
+		t.Log(c)
+		t.Error("expected count to be zero")
+	}
+	if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{2}, "test1"); err != nil || !ok {
+		t.Log(err, ok)
+		t.Error("expected to exist")
+	}
+	if c, err := db.CountLocal("a", protocol.DeviceID{2}); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 2 {
+		t.Log(c)
+		t.Error("expected count to be two")
+	}
+
+	// Drop something that doesn't exist
+	if err := db.DropDevice(protocol.DeviceID{99}); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestDropAllFiles(t *testing.T) {
+	db, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// Some local files
+
+	// Device 1 folder A
+	err = db.Update("a", protocol.DeviceID{1}, []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Device 1 folder B
+	err = db.Update("b", protocol.DeviceID{1}, []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Drop folder A
+	if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check
+	if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{1}, "test1"); err != nil || ok {
+		t.Log(err, ok)
+		t.Error("expected to not exist")
+	}
+	if c, err := db.CountLocal("a", protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 0 {
+		t.Log(c)
+		t.Error("expected count to be zero")
+	}
+	if _, ok, err := db.GetDeviceFile("b", protocol.DeviceID{1}, "test1"); err != nil || !ok {
+		t.Log(err, ok)
+		t.Error("expected to exist")
+	}
+	if c, err := db.CountLocal("b", protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	} else if c.Files != 2 {
+		t.Log(c)
+		t.Error("expected count to be two")
+	}
+
+	// Drop things that don't exist
+	if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil {
+		t.Fatal(err)
+	}
+	if err := db.DropAllFiles("trolol", protocol.DeviceID{1}); err != nil {
+		t.Fatal(err)
+	}
+	if err := db.DropAllFiles("trolol", protocol.DeviceID{99}); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestConcurrentUpdate(t *testing.T) {
+	t.Parallel()
+
+	db, err := Open(filepath.Join(t.TempDir(), "db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	const folderID = "test"
+
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+		genFile("test3", 3, 3),
+		genFile("test4", 4, 4),
+	}
+
+	const n = 32
+	res := make([]error, n)
+	var wg sync.WaitGroup
+	wg.Add(n)
+	for i := range n {
+		go func() {
+			res[i] = db.Update(folderID, protocol.DeviceID{byte(i), byte(i), byte(i)}, files)
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	for i, err := range res {
+		if err != nil {
+			t.Errorf("%d: %v", i, err)
+		}
+	}
+}
+
+func TestConcurrentUpdateSelect(t *testing.T) {
+	t.Parallel()
+
+	db, err := Open(filepath.Join(t.TempDir(), "db"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := db.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	const folderID = "test"
+
+	// Some local files
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+		genFile("test3", 3, 3),
+		genFile("test4", 4, 4),
+	}
+
+	// Insert the files for a remote device
+	if err := db.Update(folderID, protocol.DeviceID{42}, files); err != nil {
+		t.Fatal()
+	}
+
+	// Iterate over handled files and insert them for the local device.
+	// This is similar to a pattern we have in other places and should
+	// work.
+	handled := 0
+	it, errFn := db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)
+	for glob := range it {
+		glob.Version = glob.Version.Update(1)
+		if err := db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{glob}); err != nil {
+			t.Fatal(err)
+		}
+		handled++
+	}
+	if err := errFn(); err != nil {
+		t.Fatal(err)
+	}
+
+	if handled != len(files) {
+		t.Log(handled)
+		t.Error("should have handled all the files")
+	}
+}
+
+func TestAllForBlocksHash(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	// test1 is unique, while test2 and test3 have the same blocks and hence
+	// the same blocks hash
+
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+		genFile("test3", 3, 3),
+	}
+	files[2].Blocks = files[1].Blocks
+
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil {
+		t.Fatal(err)
+	}
+
+	// Check test1
+
+	test1, ok, err := sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "test1")
+	if err != nil || !ok {
+		t.Fatal("expected to exist")
+	}
+
+	vals := mustCollect[db.FileMetadata](t)(sdb.AllLocalFilesWithBlocksHash(folderID, test1.BlocksHash))
+	if len(vals) != 1 {
+		t.Log(vals)
+		t.Fatal("expected one file to match")
+	}
+
+	// Check test2 which also matches test3
+
+	test2, ok, err := sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "test2")
+	if err != nil || !ok {
+		t.Fatal("expected to exist")
+	}
+
+	vals = mustCollect[db.FileMetadata](t)(sdb.AllLocalFilesWithBlocksHash(folderID, test2.BlocksHash))
+	if len(vals) != 2 {
+		t.Log(vals)
+		t.Fatal("expected two files to match")
+	}
+	if vals[0].Name != "test2" {
+		t.Log(vals[0])
+		t.Error("expected test2")
+	}
+	if vals[1].Name != "test3" {
+		t.Log(vals[1])
+		t.Error("expected test3")
+	}
+}
+
+func TestBlocklistGarbageCollection(t *testing.T) {
+	t.Parallel()
+
+	sdb, err := OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		if err := sdb.Close(); err != nil {
+			t.Fatal(err)
+		}
+	})
+	svc := sdb.Service(time.Hour).(*Service)
+
+	// Add three files
+
+	files := []protocol.FileInfo{
+		genFile("test1", 1, 1),
+		genFile("test2", 2, 2),
+		genFile("test3", 3, 3),
+	}
+
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil {
+		t.Fatal(err)
+	}
+
+	// There should exist three blockslists and six blocks
+
+	var count int
+	if err := sdb.sql.Get(&count, `SELECT count(*) FROM blocklists`); err != nil {
+		t.Fatal(err)
+	}
+	if count != 3 {
+		t.Log(count)
+		t.Fatal("expected 3 blocklists")
+	}
+	if err := sdb.sql.Get(&count, `SELECT count(*) FROM blocks`); err != nil {
+		t.Fatal(err)
+	}
+	if count != 6 {
+		t.Log(count)
+		t.Fatal("expected 6 blocks")
+	}
+
+	// Mark test3 as deleted, it's blocks and blocklist are now eligible for collection
+	files = files[2:]
+	files[0].SetDeleted(42)
+	if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil {
+		t.Fatal(err)
+	}
+
+	// Run garbage collection
+	if err := svc.periodic(context.Background()); err != nil {
+		t.Fatal(err)
+	}
+
+	// There should exist two blockslists and four blocks
+
+	if err := sdb.sql.Get(&count, `SELECT count(*) FROM blocklists`); err != nil {
+		t.Fatal(err)
+	}
+	if count != 2 {
+		t.Log(count)
+		t.Error("expected 2 blocklists")
+	}
+	if err := sdb.sql.Get(&count, `SELECT count(*) FROM blocks`); err != nil {
+		t.Fatal(err)
+	}
+	if count != 3 {
+		t.Log(count)
+		t.Error("expected 3 blocks")
+	}
+}
+
+func TestErrorWrap(t *testing.T) {
+	if wrap(nil, "foo") != nil {
+		t.Fatal("nil should wrap to nil")
+	}
+
+	fooErr := errors.New("foo")
+	if err := wrap(fooErr); err.Error() != "testerrorwrap: foo" {
+		t.Fatalf("%q", err)
+	}
+
+	if err := wrap(fooErr, "bar", "baz"); err.Error() != "testerrorwrap (bar, baz): foo" {
+		t.Fatalf("%q", err)
+	}
+}
+
+func mustCollect[T any](t *testing.T) func(it iter.Seq[T], errFn func() error) []T {
+	t.Helper()
+	return func(it iter.Seq[T], errFn func() error) []T {
+		t.Helper()
+		vals, err := itererr.Collect(it, errFn)
+		if err != nil {
+			t.Fatal(err)
+		}
+		return vals
+	}
+}
+
+func fiNames(fs []protocol.FileInfo) []string {
+	names := make([]string, len(fs))
+	for i, fi := range fs {
+		names[i] = fi.Name
+	}
+	return names
+}
+
+func genDir(name string, seq int) protocol.FileInfo {
+	return protocol.FileInfo{
+		Name:        name,
+		Type:        protocol.FileInfoTypeDirectory,
+		ModifiedS:   time.Now().Unix(),
+		ModifiedBy:  1,
+		Sequence:    int64(seq),
+		Version:     protocol.Vector{}.Update(1),
+		Permissions: 0o755,
+		ModifiedNs:  12345678,
+	}
+}
+
+func genFile(name string, numBlocks int, seq int) protocol.FileInfo {
+	ts := timeutil.StrictlyMonotonicNanos()
+	s := ts / 1e9
+	ns := int32(ts % 1e9)
+	return protocol.FileInfo{
+		Name:         name,
+		Size:         int64(numBlocks) * blockSize,
+		ModifiedS:    s,
+		ModifiedBy:   1,
+		Version:      protocol.Vector{}.Update(1),
+		Sequence:     int64(seq),
+		Blocks:       genBlocks(name, 0, numBlocks),
+		Permissions:  0o644,
+		ModifiedNs:   ns,
+		RawBlockSize: blockSize,
+	}
+}
+
+func genBlocks(name string, seed, count int) []protocol.BlockInfo {
+	b := make([]protocol.BlockInfo, count)
+	for i := range b {
+		b[i].Hash = genBlockHash(name, seed, i)
+		b[i].Size = blockSize
+		b[i].Offset = (blockSize) * int64(i)
+	}
+	return b
+}
+
+func genBlockHash(name string, seed, index int) []byte {
+	bs := sha256.Sum256([]byte(name))
+	ebs := binary.LittleEndian.AppendUint64(nil, uint64(seed))
+	for i := range ebs {
+		bs[i] ^= ebs[i]
+	}
+	ebs = binary.LittleEndian.AppendUint64(nil, uint64(index))
+	for i := range ebs {
+		bs[i] ^= ebs[i]
+	}
+	return bs[:]
+}
diff --git a/internal/db/typed_test.go b/internal/db/typed_test.go
new file mode 100644
index 00000000000..9f01b433511
--- /dev/null
+++ b/internal/db/typed_test.go
@@ -0,0 +1,115 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/db/sqlite"
+)
+
+func TestNamespacedInt(t *testing.T) {
+	t.Parallel()
+
+	ldb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		ldb.Close()
+	})
+
+	n1 := db.NewTyped(ldb, "foo")
+	n2 := db.NewTyped(ldb, "bar")
+
+	t.Run("Int", func(t *testing.T) {
+		t.Parallel()
+
+		// Key is missing to start with
+
+		if v, ok, err := n1.Int64("testint"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != 0 || ok {
+			t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
+		}
+
+		if err := n1.PutInt64("testint", 42); err != nil {
+			t.Fatal(err)
+		}
+
+		// It should now exist in n1
+
+		if v, ok, err := n1.Int64("testint"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != 42 || !ok {
+			t.Errorf("Incorrect return v %v != 42 || ok %v != true", v, ok)
+		}
+
+		// ... but not in n2, which is in a different namespace
+
+		if v, ok, err := n2.Int64("testint"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != 0 || ok {
+			t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
+		}
+
+		if err := n1.Delete("testint"); err != nil {
+			t.Fatal(err)
+		}
+
+		// It should no longer exist
+
+		if v, ok, err := n1.Int64("testint"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != 0 || ok {
+			t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
+		}
+	})
+
+	t.Run("Time", func(t *testing.T) {
+		t.Parallel()
+
+		if v, ok, err := n1.Time("testtime"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if !v.IsZero() || ok {
+			t.Errorf("Incorrect return v %v != %v || ok %v != false", v, time.Time{}, ok)
+		}
+
+		now := time.Now()
+		if err := n1.PutTime("testtime", now); err != nil {
+			t.Fatal(err)
+		}
+
+		if v, ok, err := n1.Time("testtime"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if !v.Equal(now) || !ok {
+			t.Errorf("Incorrect return v %v != %v || ok %v != true", v, now, ok)
+		}
+	})
+
+	t.Run("String", func(t *testing.T) {
+		t.Parallel()
+
+		if v, ok, err := n1.String("teststring"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != "" || ok {
+			t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
+		}
+
+		if err := n1.PutString("teststring", "yo"); err != nil {
+			t.Fatal(err)
+		}
+
+		if v, ok, err := n1.String("teststring"); err != nil {
+			t.Error("Unexpected error:", err)
+		} else if v != "yo" || !ok {
+			t.Errorf("Incorrect return v %q != \"yo\" || ok %v != true", v, ok)
+		}
+	})
+}
diff --git a/lib/api/api_auth_test.go b/lib/api/api_auth_test.go
index a2d7146d9dd..ee51031f13f 100644
--- a/lib/api/api_auth_test.go
+++ b/lib/api/api_auth_test.go
@@ -10,10 +10,9 @@ import (
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/db/sqlite"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
 )
 
 var guiCfg config.GUIConfiguration
@@ -131,8 +130,14 @@ func (c *mockClock) wind(t time.Duration) {
 func TestTokenManager(t *testing.T) {
 	t.Parallel()
 
-	mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
-	kdb := db.NewNamespacedKV(mdb, "test")
+	mdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		mdb.Close()
+	})
+	kdb := db.NewMiscDB(mdb)
 	clock := &mockClock{now: time.Now()}
 
 	// Token manager keeps up to three tokens with a validity time of 24 hours.
diff --git a/lib/api/api_test.go b/lib/api/api_test.go
index 96ea1d1cef1..bad4b456c01 100644
--- a/lib/api/api_test.go
+++ b/lib/api/api_test.go
@@ -27,12 +27,12 @@ import (
 	"github.com/d4l3k/messagediff"
 	"github.com/thejerf/suture/v4"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/db/sqlite"
 	"github.com/syncthing/syncthing/lib/assets"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	connmocks "github.com/syncthing/syncthing/lib/connections/mocks"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	discovermocks "github.com/syncthing/syncthing/lib/discover/mocks"
 	"github.com/syncthing/syncthing/lib/events"
 	eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
@@ -84,8 +84,14 @@ func TestStopAfterBrokenConfig(t *testing.T) {
 	}
 	w := config.Wrap("/dev/null", cfg, protocol.LocalDeviceID, events.NoopLogger)
 
-	mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
-	kdb := db.NewMiscDataNamespace(mdb)
+	mdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		mdb.Close()
+	})
+	kdb := db.NewMiscDB(mdb)
 	srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
 
 	srv.started = make(chan string)
@@ -217,11 +223,7 @@ type httpTestCase struct {
 func TestAPIServiceRequests(t *testing.T) {
 	t.Parallel()
 
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(cancel)
+	baseURL := startHTTP(t, apiCfg)
 
 	cases := []httpTestCase{
 		// /rest/db
@@ -598,11 +600,7 @@ func TestHTTPLogin(t *testing.T) {
 			APIKey:              testAPIKey,
 			SendBasicAuthPrompt: sendBasicAuthPrompt,
 		})
-		baseURL, cancel, err := startHTTP(cfg)
-		if err != nil {
-			t.Fatal(err)
-		}
-		t.Cleanup(cancel)
+		baseURL := startHTTP(t, cfg)
 		url := baseURL + path
 
 		t.Run(fmt.Sprintf("%d path", expectedOkStatus), func(t *testing.T) {
@@ -795,13 +793,9 @@ func TestHTTPLogin(t *testing.T) {
 
 			w := initConfig(initialPassword, t)
 			{
-				baseURL, cancel, err := startHTTPWithShutdownTimeout(w, shutdownTimeout)
+				baseURL := startHTTPWithShutdownTimeout(t, w, shutdownTimeout)
 				cfgPath := baseURL + "/rest/config"
 				path := baseURL + "/meta.js"
-				t.Cleanup(cancel)
-				if err != nil {
-					t.Fatal(err)
-				}
 
 				resp := httpGetBasicAuth(path, "user", initialPassword)
 				if resp.StatusCode != http.StatusOK {
@@ -813,12 +807,8 @@ func TestHTTPLogin(t *testing.T) {
 				httpRequest(http.MethodPut, cfgPath, cfg, "", "", testAPIKey, "", "", "", nil, t)
 			}
 			{
-				baseURL, cancel, err := startHTTP(w)
+				baseURL := startHTTP(t, w)
 				path := baseURL + "/meta.js"
-				t.Cleanup(cancel)
-				if err != nil {
-					t.Fatal(err)
-				}
 
 				resp := httpGetBasicAuth(path, "user", initialPassword)
 				if resp.StatusCode != http.StatusForbidden {
@@ -837,13 +827,9 @@ func TestHTTPLogin(t *testing.T) {
 
 			w := initConfig(initialPassword, t)
 			{
-				baseURL, cancel, err := startHTTPWithShutdownTimeout(w, shutdownTimeout)
+				baseURL := startHTTPWithShutdownTimeout(t, w, shutdownTimeout)
 				cfgPath := baseURL + "/rest/config/gui"
 				path := baseURL + "/meta.js"
-				t.Cleanup(cancel)
-				if err != nil {
-					t.Fatal(err)
-				}
 
 				resp := httpGetBasicAuth(path, "user", initialPassword)
 				if resp.StatusCode != http.StatusOK {
@@ -855,12 +841,8 @@ func TestHTTPLogin(t *testing.T) {
 				httpRequest(http.MethodPut, cfgPath, cfg.GUI, "", "", testAPIKey, "", "", "", nil, t)
 			}
 			{
-				baseURL, cancel, err := startHTTP(w)
+				baseURL := startHTTP(t, w)
 				path := baseURL + "/meta.js"
-				t.Cleanup(cancel)
-				if err != nil {
-					t.Fatal(err)
-				}
 
 				resp := httpGetBasicAuth(path, "user", initialPassword)
 				if resp.StatusCode != http.StatusForbidden {
@@ -885,11 +867,7 @@ func TestHtmlFormLogin(t *testing.T) {
 		Password:            "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq", // bcrypt of "räksmörgås" in UTF-8
 		SendBasicAuthPrompt: false,
 	})
-	baseURL, cancel, err := startHTTP(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(cancel)
+	baseURL := startHTTP(t, cfg)
 
 	loginUrl := baseURL + "/rest/noauth/auth/password"
 	resourceUrl := baseURL + "/meta.js"
@@ -1030,11 +1008,7 @@ func TestApiCache(t *testing.T) {
 		RawAddress: "127.0.0.1:0",
 		APIKey:     testAPIKey,
 	})
-	baseURL, cancel, err := startHTTP(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	t.Cleanup(cancel)
+	baseURL := startHTTP(t, cfg)
 
 	httpGet := func(url string, bearer string) *http.Response {
 		return httpGet(url, "", "", "", bearer, nil, t)
@@ -1059,11 +1033,11 @@ func TestApiCache(t *testing.T) {
 	})
 }
 
-func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
-	return startHTTPWithShutdownTimeout(cfg, 0)
+func startHTTP(t *testing.T, cfg config.Wrapper) string {
+	return startHTTPWithShutdownTimeout(t, cfg, 0)
 }
 
-func startHTTPWithShutdownTimeout(cfg config.Wrapper, shutdownTimeout time.Duration) (string, context.CancelFunc, error) {
+func startHTTPWithShutdownTimeout(t *testing.T, cfg config.Wrapper, shutdownTimeout time.Duration) string {
 	m := new(modelmocks.Model)
 	assetDir := "../../gui"
 	eventSub := new(eventmocks.BufferedSubscription)
@@ -1086,12 +1060,18 @@ func startHTTPWithShutdownTimeout(cfg config.Wrapper, shutdownTimeout time.Durat
 
 	// Instantiate the API service
 	urService := ur.New(cfg, m, connections, false)
-	mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
-	kdb := db.NewMiscDataNamespace(mdb)
+	mdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		mdb.Close()
+	})
+	kdb := db.NewMiscDB(mdb)
 	svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, mockedSummary, errorLog, systemLog, false, kdb).(*service)
 	svc.started = addrChan
 
-	if shutdownTimeout > 0*time.Millisecond {
+	if shutdownTimeout > 0 {
 		svc.shutdownTimeout = shutdownTimeout
 	}
 
@@ -1101,14 +1081,14 @@ func startHTTPWithShutdownTimeout(cfg config.Wrapper, shutdownTimeout time.Durat
 	})
 	supervisor.Add(svc)
 	ctx, cancel := context.WithCancel(context.Background())
+	t.Cleanup(cancel)
 	supervisor.ServeBackground(ctx)
 
 	// Make sure the API service is listening, and get the URL to use.
 	addr := <-addrChan
 	tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
 	if err != nil {
-		cancel()
-		return "", cancel, fmt.Errorf("weird address from API service: %w", err)
+		t.Fatal(fmt.Errorf("weird address from API service: %w", err))
 	}
 
 	host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
@@ -1117,17 +1097,13 @@ func startHTTPWithShutdownTimeout(cfg config.Wrapper, shutdownTimeout time.Durat
 	}
 	baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
 
-	return baseURL, cancel, nil
+	return baseURL
 }
 
 func TestCSRFRequired(t *testing.T) {
 	t.Parallel()
 
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		t.Fatal("Unexpected error from getting base URL:", err)
-	}
-	t.Cleanup(cancel)
+	baseURL := startHTTP(t, apiCfg)
 
 	cli := &http.Client{
 		Timeout: time.Minute,
@@ -1245,11 +1221,7 @@ func TestCSRFRequired(t *testing.T) {
 func TestRandomString(t *testing.T) {
 	t.Parallel()
 
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL := startHTTP(t, apiCfg)
 	cli := &http.Client{
 		Timeout: time.Second,
 	}
@@ -1304,7 +1276,7 @@ func TestConfigPostOK(t *testing.T) {
 		]
 	}`))
 
-	resp, err := testConfigPost(cfg)
+	resp, err := testConfigPost(t, cfg)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1325,7 +1297,7 @@ func TestConfigPostDupFolder(t *testing.T) {
 		]
 	}`))
 
-	resp, err := testConfigPost(cfg)
+	resp, err := testConfigPost(t, cfg)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1334,12 +1306,10 @@ func TestConfigPostDupFolder(t *testing.T) {
 	}
 }
 
-func testConfigPost(data io.Reader) (*http.Response, error) {
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		return nil, err
-	}
-	defer cancel()
+func testConfigPost(t *testing.T, data io.Reader) (*http.Response, error) {
+	t.Helper()
+
+	baseURL := startHTTP(t, apiCfg)
 	cli := &http.Client{
 		Timeout: time.Second,
 	}
@@ -1356,11 +1326,7 @@ func TestHostCheck(t *testing.T) {
 
 	cfg := newMockedConfig()
 	cfg.GUIReturns(config.GUIConfiguration{RawAddress: "127.0.0.1:0"})
-	baseURL, cancel, err := startHTTP(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL := startHTTP(t, cfg)
 
 	// A normal HTTP get to the localhost-bound service should succeed
 
@@ -1419,11 +1385,7 @@ func TestHostCheck(t *testing.T) {
 		RawAddress:            "127.0.0.1:0",
 		InsecureSkipHostCheck: true,
 	})
-	baseURL, cancel, err = startHTTP(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL = startHTTP(t, cfg)
 
 	// A request with a suspicious Host header should be allowed
 
@@ -1445,11 +1407,7 @@ func TestHostCheck(t *testing.T) {
 		cfg.GUIReturns(config.GUIConfiguration{
 			RawAddress: "0.0.0.0:0",
 		})
-		baseURL, cancel, err = startHTTP(cfg)
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer cancel()
+		baseURL = startHTTP(t, cfg)
 
 		// A request with a suspicious Host header should be allowed
 
@@ -1476,11 +1434,7 @@ func TestHostCheck(t *testing.T) {
 	cfg.GUIReturns(config.GUIConfiguration{
 		RawAddress: "[::1]:0",
 	})
-	baseURL, cancel, err = startHTTP(cfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL = startHTTP(t, cfg)
 
 	// A normal HTTP get to the localhost-bound service should succeed
 
@@ -1568,11 +1522,7 @@ func TestAddressIsLocalhost(t *testing.T) {
 func TestAccessControlAllowOriginHeader(t *testing.T) {
 	t.Parallel()
 
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL := startHTTP(t, apiCfg)
 	cli := &http.Client{
 		Timeout: time.Second,
 	}
@@ -1596,11 +1546,7 @@ func TestAccessControlAllowOriginHeader(t *testing.T) {
 func TestOptionsRequest(t *testing.T) {
 	t.Parallel()
 
-	baseURL, cancel, err := startHTTP(apiCfg)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cancel()
+	baseURL := startHTTP(t, apiCfg)
 	cli := &http.Client{
 		Timeout: time.Second,
 	}
@@ -1632,8 +1578,14 @@ func TestEventMasks(t *testing.T) {
 	cfg := newMockedConfig()
 	defSub := new(eventmocks.BufferedSubscription)
 	diskSub := new(eventmocks.BufferedSubscription)
-	mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
-	kdb := db.NewMiscDataNamespace(mdb)
+	mdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		mdb.Close()
+	})
+	kdb := db.NewMiscDB(mdb)
 	svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
 
 	if mask := svc.getEventMask(""); mask != DefaultEventMask {
@@ -1780,11 +1732,7 @@ func TestConfigChanges(t *testing.T) {
 	cfgCtx, cfgCancel := context.WithCancel(context.Background())
 	go w.Serve(cfgCtx)
 	defer cfgCancel()
-	baseURL, cancel, err := startHTTP(w)
-	if err != nil {
-		t.Fatal("Unexpected error from getting base URL:", err)
-	}
-	defer cancel()
+	baseURL := startHTTP(t, w)
 
 	cli := &http.Client{
 		Timeout: time.Minute,
diff --git a/lib/config/config_test.go b/lib/config/config_test.go
index 6359f42ab1b..63a817d6516 100644
--- a/lib/config/config_test.go
+++ b/lib/config/config_test.go
@@ -484,7 +484,7 @@ func TestIssue1262(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	actual := cfg.Folders()["test"].Filesystem(nil).URI()
+	actual := cfg.Folders()["test"].Filesystem().URI()
 	expected := `e:\`
 
 	if actual != expected {
@@ -521,7 +521,7 @@ func TestFolderPath(t *testing.T) {
 		Path: "~/tmp",
 	}
 
-	realPath := folder.Filesystem(nil).URI()
+	realPath := folder.Filesystem().URI()
 	if !filepath.IsAbs(realPath) {
 		t.Error(realPath, "should be absolute")
 	}
diff --git a/lib/config/tuning_test.go b/lib/config/tuning_test.go
deleted file mode 100644
index d913492ffce..00000000000
--- a/lib/config/tuning_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2019 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package config_test
-
-import (
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db/backend"
-)
-
-func TestTuningMatches(t *testing.T) {
-	if int(config.TuningAuto) != int(backend.TuningAuto) {
-		t.Error("mismatch for TuningAuto")
-	}
-	if int(config.TuningSmall) != int(backend.TuningSmall) {
-		t.Error("mismatch for TuningSmall")
-	}
-	if int(config.TuningLarge) != int(backend.TuningLarge) {
-		t.Error("mismatch for TuningLarge")
-	}
-}
diff --git a/lib/db/backend/backend_test.go b/lib/db/backend/backend_test.go
deleted file mode 100644
index d755e0cb6c3..00000000000
--- a/lib/db/backend/backend_test.go
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (C) 2019 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package backend
-
-import "testing"
-
-// testBackendBehavior is the generic test suite that must be fulfilled by
-// every backend implementation. It should be called by each implementation
-// as (part of) their test suite.
-func testBackendBehavior(t *testing.T, open func() Backend) {
-	t.Run("WriteIsolation", func(t *testing.T) { testWriteIsolation(t, open) })
-	t.Run("DeleteNonexisten", func(t *testing.T) { testDeleteNonexistent(t, open) })
-	t.Run("IteratorClosedDB", func(t *testing.T) { testIteratorClosedDB(t, open) })
-}
-
-func testWriteIsolation(t *testing.T, open func() Backend) {
-	// Values written during a transaction should not be read back, our
-	// updateGlobal depends on this.
-
-	db := open()
-	defer db.Close()
-
-	// Sanity check
-	_ = db.Put([]byte("a"), []byte("a"))
-	v, _ := db.Get([]byte("a"))
-	if string(v) != "a" {
-		t.Fatal("read back should work")
-	}
-
-	// Now in a transaction we should still see the old value
-	tx, _ := db.NewWriteTransaction()
-	defer tx.Release()
-	_ = tx.Put([]byte("a"), []byte("b"))
-	v, _ = tx.Get([]byte("a"))
-	if string(v) != "a" {
-		t.Fatal("read in transaction should read the old value")
-	}
-}
-
-func testDeleteNonexistent(t *testing.T, open func() Backend) {
-	// Deleting a non-existent key is not an error
-
-	db := open()
-	defer db.Close()
-
-	err := db.Delete([]byte("a"))
-	if err != nil {
-		t.Error(err)
-	}
-}
-
-// Either creating the iterator or the .Error() method of the returned iterator
-// should return an error and IsClosed(err) == true.
-func testIteratorClosedDB(t *testing.T, open func() Backend) {
-	db := open()
-
-	_ = db.Put([]byte("a"), []byte("a"))
-
-	db.Close()
-
-	it, err := db.NewPrefixIterator(nil)
-	if err != nil {
-		if !IsClosed(err) {
-			t.Error("NewPrefixIterator: IsClosed(err) == false:", err)
-		}
-		return
-	}
-	it.Next()
-	if err := it.Error(); !IsClosed(err) {
-		t.Error("Next: IsClosed(err) == false:", err)
-	}
-}
diff --git a/lib/db/backend/leveldb_test.go b/lib/db/backend/leveldb_test.go
deleted file mode 100644
index 3de46ad9004..00000000000
--- a/lib/db/backend/leveldb_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (C) 2019 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package backend
-
-import "testing"
-
-func TestLevelDBBackendBehavior(t *testing.T) {
-	testBackendBehavior(t, OpenLevelDBMemory)
-}
diff --git a/lib/db/benchmark_test.go b/lib/db/benchmark_test.go
deleted file mode 100644
index 6497eb829be..00000000000
--- a/lib/db/benchmark_test.go
+++ /dev/null
@@ -1,344 +0,0 @@
-// Copyright (C) 2015 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db_test
-
-import (
-	"fmt"
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-var files, oneFile, firstHalf, secondHalf, changed100, unchanged100 []protocol.FileInfo
-
-func lazyInitBenchFiles() {
-	if files != nil {
-		return
-	}
-
-	files = make([]protocol.FileInfo, 0, 1000)
-	for i := 0; i < 1000; i++ {
-		files = append(files, protocol.FileInfo{
-			Name:    fmt.Sprintf("file%d", i),
-			Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}},
-			Blocks:  genBlocks(i),
-		})
-	}
-
-	middle := len(files) / 2
-	firstHalf = files[:middle]
-	secondHalf = files[middle:]
-	oneFile = firstHalf[middle-1 : middle]
-
-	unchanged100 := files[100:200]
-	changed100 := append([]protocol.FileInfo{}, unchanged100...)
-	for i := range changed100 {
-		changed100[i].Version = changed100[i].Version.Copy().Update(myID)
-	}
-}
-
-func getBenchFileSet(b testing.TB) (*db.Lowlevel, *db.FileSet) {
-	lazyInitBenchFiles()
-
-	ldb := newLowlevelMemory(b)
-	benchS := newFileSet(b, "test)", ldb)
-	replace(benchS, remoteDevice0, files)
-	replace(benchS, protocol.LocalDeviceID, firstHalf)
-
-	return ldb, benchS
-}
-
-func BenchmarkReplaceAll(b *testing.B) {
-	ldb := newLowlevelMemory(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		m := newFileSet(b, "test)", ldb)
-		replace(m, protocol.LocalDeviceID, files)
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkUpdateOneChanged(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	changed := make([]protocol.FileInfo, 1)
-	changed[0] = oneFile[0]
-	changed[0].Version = changed[0].Version.Copy().Update(myID)
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		if i%2 == 0 {
-			benchS.Update(protocol.LocalDeviceID, changed)
-		} else {
-			benchS.Update(protocol.LocalDeviceID, oneFile)
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkUpdate100Changed(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		if i%2 == 0 {
-			benchS.Update(protocol.LocalDeviceID, changed100)
-		} else {
-			benchS.Update(protocol.LocalDeviceID, unchanged100)
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func setup10Remotes(benchS *db.FileSet) {
-	idBase := remoteDevice1.String()[1:]
-	first := 'J'
-	for i := 0; i < 10; i++ {
-		id, _ := protocol.DeviceIDFromString(fmt.Sprintf("%v%s", first+rune(i), idBase))
-		if i%2 == 0 {
-			benchS.Update(id, changed100)
-		} else {
-			benchS.Update(id, unchanged100)
-		}
-	}
-}
-
-func BenchmarkUpdate100Changed10Remotes(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	setup10Remotes(benchS)
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		if i%2 == 0 {
-			benchS.Update(protocol.LocalDeviceID, changed100)
-		} else {
-			benchS.Update(protocol.LocalDeviceID, unchanged100)
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkUpdate100ChangedRemote(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		if i%2 == 0 {
-			benchS.Update(remoteDevice0, changed100)
-		} else {
-			benchS.Update(remoteDevice0, unchanged100)
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkUpdate100ChangedRemote10Remotes(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		if i%2 == 0 {
-			benchS.Update(remoteDevice0, changed100)
-		} else {
-			benchS.Update(remoteDevice0, unchanged100)
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkUpdateOneUnchanged(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		benchS.Update(protocol.LocalDeviceID, oneFile)
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkNeedHalf(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithNeed(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(secondHalf) {
-			b.Errorf("wrong length %d != %d", count, len(secondHalf))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkNeedHalfRemote(b *testing.B) {
-	ldb := newLowlevelMemory(b)
-	defer ldb.Close()
-	fset := newFileSet(b, "test)", ldb)
-	replace(fset, remoteDevice0, firstHalf)
-	replace(fset, protocol.LocalDeviceID, files)
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, fset)
-		snap.WithNeed(remoteDevice0, func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(secondHalf) {
-			b.Errorf("wrong length %d != %d", count, len(secondHalf))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkHave(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithHave(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(firstHalf) {
-			b.Errorf("wrong length %d != %d", count, len(firstHalf))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkGlobal(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithGlobal(func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(files) {
-			b.Errorf("wrong length %d != %d", count, len(files))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkNeedHalfTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithNeedTruncated(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(secondHalf) {
-			b.Errorf("wrong length %d != %d", count, len(secondHalf))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkHaveTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithHaveTruncated(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(firstHalf) {
-			b.Errorf("wrong length %d != %d", count, len(firstHalf))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkGlobalTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		count := 0
-		snap := snapshot(b, benchS)
-		snap.WithGlobalTruncated(func(fi protocol.FileInfo) bool {
-			count++
-			return true
-		})
-		snap.Release()
-		if count != len(files) {
-			b.Errorf("wrong length %d != %d", count, len(files))
-		}
-	}
-
-	b.ReportAllocs()
-}
-
-func BenchmarkNeedCount(b *testing.B) {
-	ldb, benchS := getBenchFileSet(b)
-	defer ldb.Close()
-
-	benchS.Update(protocol.LocalDeviceID, changed100)
-
-	b.ResetTimer()
-	for i := 0; i < b.N; i++ {
-		snap := snapshot(b, benchS)
-		_ = snap.NeedSize(protocol.LocalDeviceID)
-		snap.Release()
-	}
-
-	b.ReportAllocs()
-}
diff --git a/lib/db/blockmap_test.go b/lib/db/blockmap_test.go
deleted file mode 100644
index e5d2988362b..00000000000
--- a/lib/db/blockmap_test.go
+++ /dev/null
@@ -1,260 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"encoding/binary"
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-var (
-	f1, f2, f3 protocol.FileInfo
-	folders    = []string{"folder1", "folder2"}
-)
-
-func init() {
-	blocks := genBlocks(30)
-
-	f1 = protocol.FileInfo{
-		Name:   "f1",
-		Blocks: blocks[:10],
-	}
-
-	f2 = protocol.FileInfo{
-		Name:   "f2",
-		Blocks: blocks[10:20],
-	}
-
-	f3 = protocol.FileInfo{
-		Name:   "f3",
-		Blocks: blocks[20:],
-	}
-}
-
-func setup(t testing.TB) (*Lowlevel, *BlockFinder) {
-	t.Helper()
-	db := newLowlevelMemory(t)
-	return db, NewBlockFinder(db)
-}
-
-func dbEmpty(db *Lowlevel) bool {
-	iter, err := db.NewPrefixIterator([]byte{KeyTypeBlock})
-	if err != nil {
-		panic(err)
-	}
-	defer iter.Release()
-	return !iter.Next()
-}
-
-func addToBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var keyBuf []byte
-	blockBuf := make([]byte, 4)
-	for _, f := range fs {
-		if !f.IsDirectory() && !f.IsDeleted() && !f.IsInvalid() {
-			name := []byte(f.Name)
-			for i, block := range f.Blocks {
-				binary.BigEndian.PutUint32(blockBuf, uint32(i))
-				keyBuf, err = t.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
-				if err != nil {
-					return err
-				}
-				if err := t.Put(keyBuf, blockBuf); err != nil {
-					return err
-				}
-			}
-		}
-	}
-	return t.Commit()
-}
-
-func discardFromBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) error {
-	t, err := db.newReadWriteTransaction()
-	if err != nil {
-		return err
-	}
-	defer t.close()
-
-	var keyBuf []byte
-	for _, ef := range fs {
-		if !ef.IsDirectory() && !ef.IsDeleted() && !ef.IsInvalid() {
-			name := []byte(ef.Name)
-			for _, block := range ef.Blocks {
-				keyBuf, err = t.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
-				if err != nil {
-					return err
-				}
-				if err := t.Delete(keyBuf); err != nil {
-					return err
-				}
-			}
-		}
-	}
-	return t.Commit()
-}
-
-func TestBlockMapAddUpdateWipe(t *testing.T) {
-	db, f := setup(t)
-	defer db.Close()
-
-	if !dbEmpty(db) {
-		t.Fatal("db not empty")
-	}
-
-	folder := []byte("folder1")
-
-	f3.Type = protocol.FileInfoTypeDirectory
-
-	if err := addToBlockMap(db, folder, []protocol.FileInfo{f1, f2, f3}); err != nil {
-		t.Fatal(err)
-	}
-
-	f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		if folder != "folder1" || file != "f1" || index != 0 {
-			t.Fatal("Mismatch")
-		}
-		return true
-	})
-
-	f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		if folder != "folder1" || file != "f2" || index != 0 {
-			t.Fatal("Mismatch")
-		}
-		return true
-	})
-
-	f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		t.Fatal("Unexpected block")
-		return true
-	})
-
-	if err := discardFromBlockMap(db, folder, []protocol.FileInfo{f1, f2, f3}); err != nil {
-		t.Fatal(err)
-	}
-
-	f1.Deleted = true
-	f2.LocalFlags = protocol.FlagLocalMustRescan // one of the invalid markers
-
-	if err := addToBlockMap(db, folder, []protocol.FileInfo{f1, f2, f3}); err != nil {
-		t.Fatal(err)
-	}
-
-	f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		t.Fatal("Unexpected block")
-		return false
-	})
-
-	f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		t.Fatal("Unexpected block")
-		return false
-	})
-
-	f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		if folder != "folder1" || file != "f3" || index != 0 {
-			t.Fatal("Mismatch")
-		}
-		return true
-	})
-
-	if err := db.dropFolder(folder); err != nil {
-		t.Fatal(err)
-	}
-
-	if !dbEmpty(db) {
-		t.Fatal("db not empty")
-	}
-
-	// Should not add
-	if err := addToBlockMap(db, folder, []protocol.FileInfo{f1, f2}); err != nil {
-		t.Fatal(err)
-	}
-
-	if !dbEmpty(db) {
-		t.Fatal("db not empty")
-	}
-
-	f1.Deleted = false
-	f1.LocalFlags = 0
-	f2.Deleted = false
-	f2.LocalFlags = 0
-	f3.Deleted = false
-	f3.LocalFlags = 0
-}
-
-func TestBlockFinderLookup(t *testing.T) {
-	db, f := setup(t)
-	defer db.Close()
-
-	folder1 := []byte("folder1")
-	folder2 := []byte("folder2")
-
-	if err := addToBlockMap(db, folder1, []protocol.FileInfo{f1}); err != nil {
-		t.Fatal(err)
-	}
-	if err := addToBlockMap(db, folder2, []protocol.FileInfo{f1}); err != nil {
-		t.Fatal(err)
-	}
-
-	counter := 0
-	f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		counter++
-		switch counter {
-		case 1:
-			if folder != "folder1" || file != "f1" || index != 0 {
-				t.Fatal("Mismatch")
-			}
-		case 2:
-			if folder != "folder2" || file != "f1" || index != 0 {
-				t.Fatal("Mismatch")
-			}
-		default:
-			t.Fatal("Unexpected block")
-		}
-		return false
-	})
-
-	if counter != 2 {
-		t.Fatal("Incorrect count", counter)
-	}
-
-	if err := discardFromBlockMap(db, folder1, []protocol.FileInfo{f1}); err != nil {
-		t.Fatal(err)
-	}
-
-	f1.Deleted = true
-
-	if err := addToBlockMap(db, folder1, []protocol.FileInfo{f1}); err != nil {
-		t.Fatal(err)
-	}
-
-	counter = 0
-	f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
-		counter++
-		switch counter {
-		case 1:
-			if folder != "folder2" || file != "f1" || index != 0 {
-				t.Fatal("Mismatch")
-			}
-		default:
-			t.Fatal("Unexpected block")
-		}
-		return false
-	})
-
-	if counter != 1 {
-		t.Fatal("Incorrect count")
-	}
-
-	f1.Deleted = false
-}
diff --git a/lib/db/db_test.go b/lib/db/db_test.go
deleted file mode 100644
index 910e6767c0a..00000000000
--- a/lib/db/db_test.go
+++ /dev/null
@@ -1,701 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"bytes"
-	"context"
-	"fmt"
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-func genBlocks(n int) []protocol.BlockInfo {
-	b := make([]protocol.BlockInfo, n)
-	for i := range b {
-		h := make([]byte, 32)
-		for j := range h {
-			h[j] = byte(i + j)
-		}
-		b[i].Size = i
-		b[i].Hash = h
-	}
-	return b
-}
-
-const myID = 1
-
-var (
-	remoteDevice0, remoteDevice1 protocol.DeviceID
-	invalid                      = "invalid"
-	slashPrefixed                = "/notgood"
-	haveUpdate0to3               map[protocol.DeviceID][]protocol.FileInfo
-)
-
-func init() {
-	remoteDevice0, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
-	remoteDevice1, _ = protocol.DeviceIDFromString("I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU")
-	haveUpdate0to3 = map[protocol.DeviceID][]protocol.FileInfo{
-		protocol.LocalDeviceID: {
-			protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-			protocol.FileInfo{Name: slashPrefixed, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		},
-		remoteDevice0: {
-			protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-			protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), RawInvalid: true},
-			protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
-		},
-		remoteDevice1: {
-			protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(7)},
-			protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(5), RawInvalid: true},
-			protocol.FileInfo{Name: invalid, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1004}}}, Blocks: genBlocks(5), RawInvalid: true},
-		},
-	}
-}
-
-// TestRepairSequence checks that a few hand-crafted messed-up sequence entries get fixed.
-func TestRepairSequence(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folderStr := "test"
-	folder := []byte(folderStr)
-	id := protocol.LocalDeviceID
-	short := protocol.LocalDeviceID.Short()
-
-	files := []protocol.FileInfo{
-		{Name: "fine", Blocks: genBlocks(1)},
-		{Name: "duplicate", Blocks: genBlocks(2)},
-		{Name: "missing", Blocks: genBlocks(3)},
-		{Name: "overwriting", Blocks: genBlocks(4)},
-		{Name: "inconsistent", Blocks: genBlocks(5)},
-		{Name: "inconsistentNotIndirected", Blocks: genBlocks(2)},
-	}
-	for i, f := range files {
-		files[i].Version = f.Version.Update(short)
-	}
-
-	trans, err := db.newReadWriteTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer trans.close()
-
-	addFile := func(f protocol.FileInfo, seq int64) {
-		dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if err := trans.putFile(dk, f); err != nil {
-			t.Fatal(err)
-		}
-		sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if err := trans.Put(sk, dk); err != nil {
-			t.Fatal(err)
-		}
-	}
-
-	// Plain normal entry
-	var seq int64 = 1
-	files[0].Sequence = 1
-	addFile(files[0], seq)
-
-	// Second entry once updated with original sequence still in place
-	f := files[1]
-	f.Sequence = int64(len(files) + 1)
-	addFile(f, f.Sequence)
-	// Original sequence entry
-	seq++
-	sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq)
-	if err != nil {
-		t.Fatal(err)
-	}
-	dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err := trans.Put(sk, dk); err != nil {
-		t.Fatal(err)
-	}
-
-	// File later overwritten thus missing sequence entry
-	seq++
-	files[2].Sequence = seq
-	addFile(files[2], seq)
-
-	// File overwriting previous sequence entry (no seq bump)
-	seq++
-	files[3].Sequence = seq
-	addFile(files[3], seq)
-
-	// Inconistent files
-	seq++
-	files[4].Sequence = 101
-	addFile(files[4], seq)
-	seq++
-	files[5].Sequence = 102
-	addFile(files[5], seq)
-
-	// And a sequence entry pointing at nothing because why not
-	sk, err = trans.keyer.GenerateSequenceKey(nil, folder, 100001)
-	if err != nil {
-		t.Fatal(err)
-	}
-	dk, err = trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte("nonexisting"))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err := trans.Put(sk, dk); err != nil {
-		t.Fatal(err)
-	}
-
-	if err := trans.Commit(); err != nil {
-		t.Fatal(err)
-	}
-
-	// Loading the metadata for the first time means a "re"calculation happens,
-	// along which the sequences get repaired too.
-	db.gcMut.RLock()
-	_, err = db.loadMetadataTracker(folderStr)
-	db.gcMut.RUnlock()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Check the db
-	ro, err := db.newReadOnlyTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer ro.close()
-
-	it, err := ro.NewPrefixIterator([]byte{KeyTypeDevice})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer it.Release()
-	for it.Next() {
-		fi, err := ro.unmarshalTrunc(it.Value(), true)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if sk, err = ro.keyer.GenerateSequenceKey(sk, folder, fi.SequenceNo()); err != nil {
-			t.Fatal(err)
-		}
-		dk, err := ro.Get(sk)
-		if backend.IsNotFound(err) {
-			t.Error("Missing sequence entry for", fi.FileName())
-		} else if err != nil {
-			t.Fatal(err)
-		}
-		if !bytes.Equal(it.Key(), dk) {
-			t.Errorf("Wrong key for %v, expected %s, got %s", f.FileName(), it.Key(), dk)
-		}
-	}
-	if err := it.Error(); err != nil {
-		t.Fatal(err)
-	}
-	it.Release()
-
-	it, err = ro.NewPrefixIterator([]byte{KeyTypeSequence})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer it.Release()
-	for it.Next() {
-		fi, ok, err := ro.getFileTrunc(it.Value(), false)
-		if err != nil {
-			t.Fatal(err)
-		}
-		seq := ro.keyer.SequenceFromSequenceKey(it.Key())
-		if !ok {
-			t.Errorf("Sequence entry %v points at nothing", seq)
-		} else if fi.SequenceNo() != seq {
-			t.Errorf("Inconsistent sequence entry for %v: %v != %v", fi.FileName(), fi.SequenceNo(), seq)
-		}
-		if len(fi.Blocks) == 0 {
-			t.Error("Missing blocks in", fi.FileName())
-		}
-	}
-	if err := it.Error(); err != nil {
-		t.Fatal(err)
-	}
-	it.Release()
-}
-
-func TestDowngrade(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-	// sets the min version etc
-	if err := UpdateSchema(db); err != nil {
-		t.Fatal(err)
-	}
-
-	// Bump the database version to something newer than we actually support
-	miscDB := NewMiscDataNamespace(db)
-	if err := miscDB.PutInt64("dbVersion", dbVersion+1); err != nil {
-		t.Fatal(err)
-	}
-	l.Infoln(dbVersion)
-
-	// Pretend we just opened the DB and attempt to update it again
-	err := UpdateSchema(db)
-
-	if err, ok := err.(*databaseDowngradeError); !ok {
-		t.Fatal("Expected error due to database downgrade, got", err)
-	} else if err.minSyncthingVersion != dbMinSyncthingVersion {
-		t.Fatalf("Error has %v as min Syncthing version, expected %v", err.minSyncthingVersion, dbMinSyncthingVersion)
-	}
-}
-
-func TestCheckGlobals(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	fs := newFileSet(t, "test", db)
-
-	// Add any file
-	name := "foo"
-	fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{
-			Name:    name,
-			Type:    protocol.FileInfoTypeFile,
-			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}},
-		},
-	})
-
-	// Remove just the file entry
-	if err := db.dropPrefix([]byte{KeyTypeDevice}); err != nil {
-		t.Fatal(err)
-	}
-
-	// Clean up global entry of the now missing file
-	if repaired, err := db.checkGlobals(fs.folder); err != nil {
-		t.Fatal(err)
-	} else if repaired != 1 {
-		t.Error("Expected 1 repaired global item, got", repaired)
-	}
-
-	// Check that the global entry is gone
-	gk, err := db.keyer.GenerateGlobalVersionKey(nil, []byte(fs.folder), []byte(name))
-	if err != nil {
-		t.Fatal(err)
-	}
-	_, err = db.Get(gk)
-	if !backend.IsNotFound(err) {
-		t.Error("Expected key missing error, got", err)
-	}
-}
-
-func TestDropDuplicates(t *testing.T) {
-	names := []string{
-		"foo",
-		"bar",
-		"dcxvoijnds",
-		"3d/dsfase/4/ss2",
-	}
-	tcs := []struct{ in, out []int }{
-		{[]int{0}, []int{0}},
-		{[]int{0, 1}, []int{0, 1}},
-		{[]int{0, 1, 0, 1}, []int{0, 1}},
-		{[]int{0, 1, 1, 1, 1}, []int{0, 1}},
-		{[]int{0, 0, 0, 1}, []int{0, 1}},
-		{[]int{0, 1, 2, 3}, []int{0, 1, 2, 3}},
-		{[]int{3, 2, 1, 0, 0, 1, 2, 3}, []int{0, 1, 2, 3}},
-		{[]int{0, 1, 1, 3, 0, 1, 0, 1, 2, 3}, []int{0, 1, 2, 3}},
-	}
-
-	for tci, tc := range tcs {
-		inp := make([]protocol.FileInfo, len(tc.in))
-		expSeq := make(map[string]int)
-		for i, j := range tc.in {
-			inp[i] = protocol.FileInfo{Name: names[j], Sequence: int64(i)}
-			expSeq[names[j]] = i
-		}
-		outp := normalizeFilenamesAndDropDuplicates(inp)
-		if len(outp) != len(tc.out) {
-			t.Errorf("tc %v: Expected %v entries, got %v", tci, len(tc.out), len(outp))
-			continue
-		}
-		for i, f := range outp {
-			if exp := names[tc.out[i]]; exp != f.Name {
-				t.Errorf("tc %v: Got file %v at pos %v, expected %v", tci, f.Name, i, exp)
-			}
-			if exp := int64(expSeq[outp[i].Name]); exp != f.Sequence {
-				t.Errorf("tc %v: Got sequence %v at pos %v, expected %v", tci, f.Sequence, i, exp)
-			}
-		}
-	}
-}
-
-func TestGCIndirect(t *testing.T) {
-	// Verify that the gcIndirect run actually removes block lists.
-
-	db := newLowlevelMemory(t)
-	defer db.Close()
-	meta := newMetadataTracker(db.keyer, events.NoopLogger)
-
-	// Add three files with different block lists
-
-	files := []protocol.FileInfo{
-		{Name: "a", Blocks: genBlocks(100)},
-		{Name: "b", Blocks: genBlocks(200)},
-		{Name: "c", Blocks: genBlocks(300)},
-	}
-
-	db.updateLocalFiles([]byte("folder"), files, meta)
-
-	// Run a GC pass
-
-	db.gcIndirect(context.Background())
-
-	// Verify that we have three different block lists
-
-	n, err := numBlockLists(db)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if n != len(files) {
-		t.Fatal("expected each file to have a block list")
-	}
-
-	// Change the block lists for each file
-
-	for i := range files {
-		files[i].Version = files[i].Version.Update(42)
-		files[i].Blocks = genBlocks(len(files[i].Blocks) + 1)
-	}
-
-	db.updateLocalFiles([]byte("folder"), files, meta)
-
-	// Verify that we now have *six* different block lists
-
-	n, err = numBlockLists(db)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if n != 2*len(files) {
-		t.Fatal("expected both old and new block lists to exist")
-	}
-
-	// Run a GC pass
-
-	db.gcIndirect(context.Background())
-
-	// Verify that we now have just the three we need, again
-
-	n, err = numBlockLists(db)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if n != len(files) {
-		t.Fatal("expected GC to collect all but the needed ones")
-	}
-
-	// Double check the correctness by loading the block lists and comparing with what we stored
-
-	tr, err := db.newReadOnlyTransaction()
-	if err != nil {
-		t.Fatal()
-	}
-	defer tr.Release()
-	for _, f := range files {
-		fi, ok, err := tr.getFile([]byte("folder"), protocol.LocalDeviceID[:], []byte(f.Name))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if !ok {
-			t.Fatal("mysteriously missing")
-		}
-		if len(fi.Blocks) != len(f.Blocks) {
-			t.Fatal("block list mismatch")
-		}
-		for i := range fi.Blocks {
-			if !bytes.Equal(fi.Blocks[i].Hash, f.Blocks[i].Hash) {
-				t.Fatal("hash mismatch")
-			}
-		}
-	}
-}
-
-func TestUpdateTo14(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folderStr := "default"
-	folder := []byte(folderStr)
-	name := []byte("foo")
-	file := protocol.FileInfo{Name: string(name), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(blocksIndirectionCutoff - 1)}
-	file.BlocksHash = protocol.BlocksHash(file.Blocks)
-	fileWOBlocks := file
-	fileWOBlocks.Blocks = nil
-	meta, err := db.loadMetadataTracker(folderStr)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Initially add the correct file the usual way, all good here.
-	if err := db.updateLocalFiles(folder, []protocol.FileInfo{file}, meta); err != nil {
-		t.Fatal(err)
-	}
-
-	// Simulate the previous bug, where .putFile could write a file info without
-	// blocks, even though the file has them (and thus a non-nil BlocksHash).
-	trans, err := db.newReadWriteTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer trans.close()
-	key, err := db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], name)
-	if err != nil {
-		t.Fatal(err)
-	}
-	fiBs := mustMarshal(fileWOBlocks.ToWire(true))
-	if err := trans.Put(key, fiBs); err != nil {
-		t.Fatal(err)
-	}
-	if err := trans.Commit(); err != nil {
-		t.Fatal(err)
-	}
-	trans.close()
-
-	// Run migration, pretending were still on schema 13.
-	if err := (&schemaUpdater{db}).updateSchemaTo14(13); err != nil {
-		t.Fatal(err)
-	}
-
-	// checks
-	ro, err := db.newReadOnlyTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer ro.close()
-	if f, ok, err := ro.getFileByKey(key); err != nil {
-		t.Fatal(err)
-	} else if !ok {
-		t.Error("file missing")
-	} else if !f.MustRescan() {
-		t.Error("file not marked as MustRescan")
-	}
-
-	if vl, err := ro.getGlobalVersions(nil, folder, name); err != nil {
-		t.Fatal(err)
-	} else if fv, ok := vlGetGlobal(vl); !ok {
-		t.Error("missing global")
-	} else if !fvIsInvalid(fv) {
-		t.Error("global not marked as invalid")
-	}
-}
-
-func TestFlushRecursion(t *testing.T) {
-	// Verify that a commit hook can write to the transaction without
-	// causing another flush and thus recursion.
-
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	// A commit hook that writes a small piece of data to the transaction.
-	hookFired := 0
-	hook := func(tx backend.WriteTransaction) error {
-		err := tx.Put([]byte(fmt.Sprintf("hook-key-%d", hookFired)), []byte(fmt.Sprintf("hook-value-%d", hookFired)))
-		if err != nil {
-			t.Fatal(err)
-		}
-		hookFired++
-		return nil
-	}
-
-	// A transaction.
-	tx, err := db.NewWriteTransaction(hook)
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer tx.Release()
-
-	// Write stuff until the transaction flushes, thus firing the hook.
-	i := 0
-	for hookFired == 0 {
-		err := tx.Put([]byte(fmt.Sprintf("key-%d", i)), []byte(fmt.Sprintf("value-%d", i)))
-		if err != nil {
-			t.Fatal(err)
-		}
-		i++
-	}
-
-	// The hook should have fired precisely once.
-	if hookFired != 1 {
-		t.Error("expect one hook fire, not", hookFired)
-	}
-}
-
-func TestCheckLocalNeed(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folderStr := "test"
-	fs := newFileSet(t, folderStr, db)
-
-	// Add files such that we are in sync for a and b, and need c and d.
-	files := []protocol.FileInfo{
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}},
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}},
-		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}},
-	}
-	fs.Update(protocol.LocalDeviceID, files)
-	files[2].Version = files[2].Version.Update(remoteDevice0.Short())
-	files[3].Version = files[2].Version.Update(remoteDevice0.Short())
-	fs.Update(remoteDevice0, files)
-
-	checkNeed := func() {
-		snap := snapshot(t, fs)
-		defer snap.Release()
-		c := snap.NeedSize(protocol.LocalDeviceID)
-		if c.Files != 2 {
-			t.Errorf("Expected 2 needed files locally, got %v in meta", c.Files)
-		}
-		needed := make([]protocol.FileInfo, 0, 2)
-		snap.WithNeed(protocol.LocalDeviceID, func(fi protocol.FileInfo) bool {
-			needed = append(needed, fi)
-			return true
-		})
-		if l := len(needed); l != 2 {
-			t.Errorf("Expected 2 needed files locally, got %v in db", l)
-		} else if needed[0].Name != "c" || needed[1].Name != "d" {
-			t.Errorf("Expected files c and d to be needed, got %v and %v", needed[0].Name, needed[1].Name)
-		}
-	}
-
-	checkNeed()
-
-	trans, err := db.newReadWriteTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer trans.close()
-
-	// Add "b" to needed and remove "d"
-	folder := []byte(folderStr)
-	key, err := trans.keyer.GenerateNeedFileKey(nil, folder, []byte(files[1].Name))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err = trans.Put(key, nil); err != nil {
-		t.Fatal(err)
-	}
-	key, err = trans.keyer.GenerateNeedFileKey(nil, folder, []byte(files[3].Name))
-	if err != nil {
-		t.Fatal(err)
-	}
-	if err = trans.Delete(key); err != nil {
-		t.Fatal(err)
-	}
-	if err := trans.Commit(); err != nil {
-		t.Fatal(err)
-	}
-
-	if repaired, err := db.checkLocalNeed(folder); err != nil {
-		t.Fatal(err)
-	} else if repaired != 2 {
-		t.Error("Expected 2 repaired local need items, got", repaired)
-	}
-
-	checkNeed()
-}
-
-func TestDuplicateNeedCount(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folder := "test"
-
-	fs := newFileSet(t, folder, db)
-	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID), Sequence: 1}}
-	fs.Update(protocol.LocalDeviceID, files)
-	files[0].Version = files[0].Version.Update(remoteDevice0.Short())
-	fs.Update(remoteDevice0, files)
-
-	db.checkRepair()
-
-	fs = newFileSet(t, folder, db)
-	found := false
-	for _, c := range fs.meta.counts.Counts {
-		if protocol.LocalDeviceID == c.DeviceID && c.LocalFlags == needFlag {
-			if found {
-				t.Fatal("second need count for local device encountered")
-			}
-			found = true
-		}
-	}
-	if !found {
-		t.Fatal("no need count for local device encountered")
-	}
-}
-
-func TestNeedAfterDropGlobal(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folder := "test"
-
-	fs := newFileSet(t, folder, db)
-
-	// Initial:
-	// Three devices and a file "test": local has Version 1, remoteDevice0
-	// Version 2 and remoteDevice2 doesn't have it.
-	// All of them have "bar", just so the db knows about remoteDevice2.
-	files := []protocol.FileInfo{
-		{Name: "foo", Version: protocol.Vector{}.Update(myID), Sequence: 1},
-		{Name: "bar", Version: protocol.Vector{}.Update(myID), Sequence: 2},
-	}
-	fs.Update(protocol.LocalDeviceID, files)
-	files[0].Version = files[0].Version.Update(myID)
-	fs.Update(remoteDevice0, files)
-	fs.Update(remoteDevice1, files[1:])
-
-	// remoteDevice1 needs one file: test
-	snap := snapshot(t, fs)
-	c := snap.NeedSize(remoteDevice1)
-	if c.Files != 1 {
-		t.Errorf("Expected 1 needed files initially, got %v", c.Files)
-	}
-	snap.Release()
-
-	// Drop remoteDevice0, i.e. remove all their files from db.
-	// That changes the global file, which is now what local has.
-	fs.Drop(remoteDevice0)
-
-	// remoteDevice1 still needs test.
-	snap = snapshot(t, fs)
-	c = snap.NeedSize(remoteDevice1)
-	if c.Files != 1 {
-		t.Errorf("Expected still 1 needed files, got %v", c.Files)
-	}
-	snap.Release()
-}
-
-func numBlockLists(db *Lowlevel) (int, error) {
-	it, err := db.Backend.NewPrefixIterator([]byte{KeyTypeBlockList})
-	if err != nil {
-		return 0, err
-	}
-	defer it.Release()
-	n := 0
-	for it.Next() {
-		n++
-	}
-	if err := it.Error(); err != nil {
-		return 0, err
-	}
-	return n, nil
-}
diff --git a/lib/db/keyer_test.go b/lib/db/keyer_test.go
deleted file mode 100644
index 6b4372c9c16..00000000000
--- a/lib/db/keyer_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"bytes"
-	"testing"
-)
-
-func TestDeviceKey(t *testing.T) {
-	fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
-	dev := []byte("device67890123456789012345678901")
-	name := []byte("name")
-
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	key, err := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	fld2, ok := db.keyer.FolderFromDeviceFileKey(key)
-	if !ok {
-		t.Fatal("unexpectedly not found")
-	}
-	if !bytes.Equal(fld2, fld) {
-		t.Errorf("wrong folder %q != %q", fld2, fld)
-	}
-	dev2, ok := db.keyer.DeviceFromDeviceFileKey(key)
-	if !ok {
-		t.Fatal("unexpectedly not found")
-	}
-	if !bytes.Equal(dev2, dev) {
-		t.Errorf("wrong device %q != %q", dev2, dev)
-	}
-	name2 := db.keyer.NameFromDeviceFileKey(key)
-	if !bytes.Equal(name2, name) {
-		t.Errorf("wrong name %q != %q", name2, name)
-	}
-}
-
-func TestGlobalKey(t *testing.T) {
-	fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
-	name := []byte("name")
-
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	key, err := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	name2 := db.keyer.NameFromGlobalVersionKey(key)
-	if !bytes.Equal(name2, name) {
-		t.Errorf("wrong name %q != %q", name2, name)
-	}
-}
-
-func TestSequenceKey(t *testing.T) {
-	fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
-
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	const seq = 1234567890
-	key, err := db.keyer.GenerateSequenceKey(nil, fld, seq)
-	if err != nil {
-		t.Fatal(err)
-	}
-	outSeq := db.keyer.SequenceFromSequenceKey(key)
-	if outSeq != seq {
-		t.Errorf("sequence number mangled, %d != %d", outSeq, seq)
-	}
-}
diff --git a/lib/db/meta_test.go b/lib/db/meta_test.go
deleted file mode 100644
index e68257a706e..00000000000
--- a/lib/db/meta_test.go
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"math/bits"
-	"sort"
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-func TestEachFlagBit(t *testing.T) {
-	cases := []struct {
-		flags      uint32
-		iterations int
-	}{
-		{0, 0},
-		{1<<0 | 1<<3, 2},
-		{1 << 0, 1},
-		{1 << 31, 1},
-		{1<<10 | 1<<20 | 1<<30, 3},
-	}
-
-	for _, tc := range cases {
-		var flags uint32
-		iterations := 0
-
-		eachFlagBit(tc.flags, func(f uint32) {
-			iterations++
-			flags |= f
-			if bits.OnesCount32(f) != 1 {
-				t.Error("expected exactly one bit to be set in every call")
-			}
-		})
-
-		if flags != tc.flags {
-			t.Errorf("expected 0x%x flags, got 0x%x", tc.flags, flags)
-		}
-		if iterations != tc.iterations {
-			t.Errorf("expected %d iterations, got %d", tc.iterations, iterations)
-		}
-	}
-}
-
-func TestMetaDevices(t *testing.T) {
-	d1 := protocol.DeviceID{1}
-	d2 := protocol.DeviceID{2}
-	meta := newMetadataTracker(nil, events.NoopLogger)
-
-	meta.addFile(d1, protocol.FileInfo{Sequence: 1})
-	meta.addFile(d1, protocol.FileInfo{Sequence: 2, LocalFlags: 1})
-	meta.addFile(d2, protocol.FileInfo{Sequence: 1})
-	meta.addFile(d2, protocol.FileInfo{Sequence: 2, LocalFlags: 2})
-	meta.addFile(protocol.LocalDeviceID, protocol.FileInfo{Sequence: 1})
-
-	// There are five device/flags combos
-	if l := len(meta.counts.Counts); l < 5 {
-		t.Error("expected at least five buckets, not", l)
-	}
-
-	// There are only two non-local devices
-	devs := meta.devices()
-	if l := len(devs); l != 2 {
-		t.Fatal("expected two devices, not", l)
-	}
-
-	// Check that we got the two devices we expect
-	sort.Slice(devs, func(a, b int) bool {
-		return devs[a].Compare(devs[b]) == -1
-	})
-	if devs[0] != d1 {
-		t.Error("first device should be d1")
-	}
-	if devs[1] != d2 {
-		t.Error("second device should be d2")
-	}
-}
-
-func TestMetaSequences(t *testing.T) {
-	d1 := protocol.DeviceID{1}
-	meta := newMetadataTracker(nil, events.NoopLogger)
-
-	meta.addFile(d1, protocol.FileInfo{Sequence: 1})
-	meta.addFile(d1, protocol.FileInfo{Sequence: 2, RawInvalid: true})
-	meta.addFile(d1, protocol.FileInfo{Sequence: 3})
-	meta.addFile(d1, protocol.FileInfo{Sequence: 4, RawInvalid: true})
-	meta.addFile(protocol.LocalDeviceID, protocol.FileInfo{Sequence: 1})
-	meta.addFile(protocol.LocalDeviceID, protocol.FileInfo{Sequence: 2})
-	meta.addFile(protocol.LocalDeviceID, protocol.FileInfo{Sequence: 3, LocalFlags: 1})
-	meta.addFile(protocol.LocalDeviceID, protocol.FileInfo{Sequence: 4, LocalFlags: 2})
-
-	if seq := meta.Sequence(d1); seq != 4 {
-		t.Error("sequence of first device should be 4, not", seq)
-	}
-	if seq := meta.Sequence(protocol.LocalDeviceID); seq != 4 {
-		t.Error("sequence of first device should be 4, not", seq)
-	}
-}
-
-func TestRecalcMeta(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	// Add some files
-	s1 := newFileSet(t, "test", ldb)
-	files := []protocol.FileInfo{
-		{Name: "a", Size: 1000},
-		{Name: "b", Size: 2000},
-	}
-	s1.Update(protocol.LocalDeviceID, files)
-
-	// Verify local/global size
-	snap := snapshot(t, s1)
-	ls := snap.LocalSize()
-	gs := snap.GlobalSize()
-	snap.Release()
-	if ls.Bytes != 3000 {
-		t.Fatalf("Wrong initial local byte count, %d != 3000", ls.Bytes)
-	}
-	if gs.Bytes != 3000 {
-		t.Fatalf("Wrong initial global byte count, %d != 3000", gs.Bytes)
-	}
-
-	// Reach into the database to make the metadata tracker intentionally
-	// wrong and out of date
-	curSeq := s1.meta.Sequence(protocol.LocalDeviceID)
-	tran, err := ldb.newReadWriteTransaction()
-	if err != nil {
-		t.Fatal(err)
-	}
-	s1.meta.mut.Lock()
-	s1.meta.countsPtr(protocol.LocalDeviceID, 0).Sequence = curSeq - 1 // too low
-	s1.meta.countsPtr(protocol.LocalDeviceID, 0).Bytes = 1234          // wrong
-	s1.meta.countsPtr(protocol.GlobalDeviceID, 0).Bytes = 1234         // wrong
-	s1.meta.dirty = true
-	s1.meta.mut.Unlock()
-	if err := s1.meta.toDB(tran, []byte("test")); err != nil {
-		t.Fatal(err)
-	}
-	if err := tran.Commit(); err != nil {
-		t.Fatal(err)
-	}
-
-	// Verify that our bad data "took"
-	snap = snapshot(t, s1)
-	ls = snap.LocalSize()
-	gs = snap.GlobalSize()
-	snap.Release()
-	if ls.Bytes != 1234 {
-		t.Fatalf("Wrong changed local byte count, %d != 1234", ls.Bytes)
-	}
-	if gs.Bytes != 1234 {
-		t.Fatalf("Wrong changed global byte count, %d != 1234", gs.Bytes)
-	}
-
-	// Create a new fileset, which will realize the inconsistency and recalculate
-	s2 := newFileSet(t, "test", ldb)
-
-	// Verify local/global size
-	snap = snapshot(t, s2)
-	ls = snap.LocalSize()
-	gs = snap.GlobalSize()
-	snap.Release()
-	if ls.Bytes != 3000 {
-		t.Fatalf("Wrong fixed local byte count, %d != 3000", ls.Bytes)
-	}
-	if gs.Bytes != 3000 {
-		t.Fatalf("Wrong fixed global byte count, %d != 3000", gs.Bytes)
-	}
-}
-
-func TestMetaKeyCollisions(t *testing.T) {
-	if protocol.LocalAllFlags&needFlag != 0 {
-		t.Error("Collision between need flag and protocol local file flags")
-	}
-}
diff --git a/lib/db/namespaced_test.go b/lib/db/namespaced_test.go
deleted file mode 100644
index bf012d5f30f..00000000000
--- a/lib/db/namespaced_test.go
+++ /dev/null
@@ -1,177 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"testing"
-	"time"
-)
-
-func TestNamespacedInt(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	n1 := NewNamespacedKV(ldb, "foo")
-	n2 := NewNamespacedKV(ldb, "bar")
-
-	// Key is missing to start with
-
-	if v, ok, err := n1.Int64("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != 0 || ok {
-		t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
-	}
-
-	if err := n1.PutInt64("test", 42); err != nil {
-		t.Fatal(err)
-	}
-
-	// It should now exist in n1
-
-	if v, ok, err := n1.Int64("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != 42 || !ok {
-		t.Errorf("Incorrect return v %v != 42 || ok %v != true", v, ok)
-	}
-
-	// ... but not in n2, which is in a different namespace
-
-	if v, ok, err := n2.Int64("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != 0 || ok {
-		t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
-	}
-
-	if err := n1.Delete("test"); err != nil {
-		t.Fatal(err)
-	}
-
-	// It should no longer exist
-
-	if v, ok, err := n1.Int64("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != 0 || ok {
-		t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
-	}
-}
-
-func TestNamespacedTime(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	n1 := NewNamespacedKV(ldb, "foo")
-
-	if v, ok, err := n1.Time("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if !v.IsZero() || ok {
-		t.Errorf("Incorrect return v %v != %v || ok %v != false", v, time.Time{}, ok)
-	}
-
-	now := time.Now()
-	if err := n1.PutTime("test", now); err != nil {
-		t.Fatal(err)
-	}
-
-	if v, ok, err := n1.Time("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if !v.Equal(now) || !ok {
-		t.Errorf("Incorrect return v %v != %v || ok %v != true", v, now, ok)
-	}
-}
-
-func TestNamespacedString(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	n1 := NewNamespacedKV(ldb, "foo")
-
-	if v, ok, err := n1.String("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "" || ok {
-		t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
-	}
-
-	if err := n1.PutString("test", "yo"); err != nil {
-		t.Fatal(err)
-	}
-
-	if v, ok, err := n1.String("test"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "yo" || !ok {
-		t.Errorf("Incorrect return v %q != \"yo\" || ok %v != true", v, ok)
-	}
-}
-
-func TestNamespacedReset(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	n1 := NewNamespacedKV(ldb, "foo")
-
-	if err := n1.PutString("test1", "yo1"); err != nil {
-		t.Fatal(err)
-	}
-	if err := n1.PutString("test2", "yo2"); err != nil {
-		t.Fatal(err)
-	}
-	if err := n1.PutString("test3", "yo3"); err != nil {
-		t.Fatal(err)
-	}
-
-	if v, ok, err := n1.String("test1"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "yo1" || !ok {
-		t.Errorf("Incorrect return v %q != \"yo1\" || ok %v != true", v, ok)
-	}
-	if v, ok, err := n1.String("test2"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "yo2" || !ok {
-		t.Errorf("Incorrect return v %q != \"yo2\" || ok %v != true", v, ok)
-	}
-	if v, ok, err := n1.String("test3"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "yo3" || !ok {
-		t.Errorf("Incorrect return v %q != \"yo3\" || ok %v != true", v, ok)
-	}
-
-	reset(n1)
-
-	if v, ok, err := n1.String("test1"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "" || ok {
-		t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
-	}
-	if v, ok, err := n1.String("test2"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "" || ok {
-		t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
-	}
-	if v, ok, err := n1.String("test3"); err != nil {
-		t.Error("Unexpected error:", err)
-	} else if v != "" || ok {
-		t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
-	}
-}
-
-// reset removes all entries in this namespace.
-func reset(n *NamespacedKV) {
-	tr, err := n.db.NewWriteTransaction()
-	if err != nil {
-		return
-	}
-	defer tr.Release()
-
-	it, err := tr.NewPrefixIterator([]byte(n.prefix))
-	if err != nil {
-		return
-	}
-	for it.Next() {
-		_ = tr.Delete(it.Key())
-	}
-	it.Release()
-	_ = tr.Commit()
-}
diff --git a/lib/db/set_test.go b/lib/db/set_test.go
deleted file mode 100644
index 459d9531e89..00000000000
--- a/lib/db/set_test.go
+++ /dev/null
@@ -1,1901 +0,0 @@
-// Copyright (C) 2014 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db_test
-
-import (
-	"bytes"
-	"fmt"
-	"os"
-	"path/filepath"
-	"sort"
-	"testing"
-	"time"
-
-	"github.com/d4l3k/messagediff"
-
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-var remoteDevice0, remoteDevice1 protocol.DeviceID
-
-func init() {
-	remoteDevice0, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
-	remoteDevice1, _ = protocol.DeviceIDFromString("I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU")
-}
-
-const myID = 1
-
-func genBlocks(n int) []protocol.BlockInfo {
-	b := make([]protocol.BlockInfo, n)
-	for i := range b {
-		h := make([]byte, 32)
-		for j := range h {
-			h[j] = byte(i + j)
-		}
-		b[i].Size = i
-		b[i].Hash = h
-	}
-	return b
-}
-
-func globalList(t testing.TB, s *db.FileSet) []protocol.FileInfo {
-	var fs []protocol.FileInfo
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithGlobal(func(fi protocol.FileInfo) bool {
-		fs = append(fs, fi)
-		return true
-	})
-	return fs
-}
-
-func globalListPrefixed(t testing.TB, s *db.FileSet, prefix string) []protocol.FileInfo {
-	var fs []protocol.FileInfo
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithPrefixedGlobalTruncated(prefix, func(fi protocol.FileInfo) bool {
-		fs = append(fs, fi)
-		return true
-	})
-	return fs
-}
-
-func haveList(t testing.TB, s *db.FileSet, n protocol.DeviceID) []protocol.FileInfo {
-	var fs []protocol.FileInfo
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithHave(n, func(fi protocol.FileInfo) bool {
-		fs = append(fs, fi)
-		return true
-	})
-	return fs
-}
-
-func haveListPrefixed(t testing.TB, s *db.FileSet, n protocol.DeviceID, prefix string) []protocol.FileInfo {
-	var fs []protocol.FileInfo
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithPrefixedHaveTruncated(n, prefix, func(fi protocol.FileInfo) bool {
-		fs = append(fs, fi)
-		return true
-	})
-	return fs
-}
-
-func needList(t testing.TB, s *db.FileSet, n protocol.DeviceID) []protocol.FileInfo {
-	var fs []protocol.FileInfo
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithNeed(n, func(fi protocol.FileInfo) bool {
-		fs = append(fs, fi)
-		return true
-	})
-	return fs
-}
-
-type fileList []protocol.FileInfo
-
-func (l fileList) Len() int {
-	return len(l)
-}
-
-func (l fileList) Less(a, b int) bool {
-	return l[a].Name < l[b].Name
-}
-
-func (l fileList) Swap(a, b int) {
-	l[a], l[b] = l[b], l[a]
-}
-
-func (l fileList) String() string {
-	var b bytes.Buffer
-	b.WriteString("[]protocol.FileList{\n")
-	for _, f := range l {
-		fmt.Fprintf(&b, "  %q: #%v, %d bytes, %d blocks, perms=%o\n", f.Name, f.Version, f.Size, len(f.Blocks), f.Permissions)
-	}
-	b.WriteString("}")
-	return b.String()
-}
-
-func setSequence(seq int64, files fileList) int64 {
-	for i := range files {
-		seq++
-		files[i].Sequence = seq
-	}
-	return seq
-}
-
-func setBlocksHash(files fileList) {
-	for i, f := range files {
-		files[i].BlocksHash = protocol.BlocksHash(f.Blocks)
-	}
-}
-
-func TestGlobalSet(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	m := newFileSet(t, "test", ldb)
-
-	local0 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "z", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
-	}
-	localSeq := setSequence(0, local0)
-	setBlocksHash(local0)
-	local1 := fileList{
-		protocol.FileInfo{Name: "a", Sequence: 6, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Sequence: 7, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Sequence: 8, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
-		protocol.FileInfo{Name: "d", Sequence: 9, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "z", Sequence: 10, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Deleted: true},
-	}
-	setSequence(localSeq, local1)
-	setBlocksHash(local1)
-	localTot := fileList{
-		local1[0],
-		local1[1],
-		local1[2],
-		local1[3],
-		protocol.FileInfo{Name: "z", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Deleted: true},
-	}
-
-	remote0 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(5)},
-	}
-	remoteSeq := setSequence(0, remote0)
-	setBlocksHash(remote0)
-	remote1 := fileList{
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(6)},
-		protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(7)},
-	}
-	setSequence(remoteSeq, remote1)
-	setBlocksHash(remote1)
-	remoteTot := fileList{
-		remote0[0],
-		remote1[0],
-		remote0[2],
-		remote1[1],
-	}
-
-	expectedGlobal := fileList{
-		remote0[0],  // a
-		remote1[0],  // b
-		remote0[2],  // c
-		localTot[3], // d
-		remote1[1],  // e
-		localTot[4], // z
-	}
-
-	expectedLocalNeed := fileList{
-		remote1[0],
-		remote0[2],
-		remote1[1],
-	}
-
-	expectedRemoteNeed := fileList{
-		local0[3],
-	}
-
-	replace(m, protocol.LocalDeviceID, local0)
-	replace(m, protocol.LocalDeviceID, local1)
-	replace(m, remoteDevice0, remote0)
-	m.Update(remoteDevice0, remote1)
-
-	check := func() {
-		t.Helper()
-
-		g := fileList(globalList(t, m))
-		sort.Sort(g)
-
-		if fmt.Sprint(g) != fmt.Sprint(expectedGlobal) {
-			t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
-		}
-
-		var globalFiles, globalDirectories, globalDeleted int
-		var globalBytes int64
-		for _, f := range g {
-			if f.IsInvalid() {
-				continue
-			}
-			switch {
-			case f.IsDeleted():
-				globalDeleted++
-			case f.IsDirectory():
-				globalDirectories++
-			default:
-				globalFiles++
-			}
-			globalBytes += f.FileSize()
-		}
-		gs := globalSize(t, m)
-		if gs.Files != globalFiles {
-			t.Errorf("Incorrect GlobalSize files; %d != %d", gs.Files, globalFiles)
-		}
-		if gs.Directories != globalDirectories {
-			t.Errorf("Incorrect GlobalSize directories; %d != %d", gs.Directories, globalDirectories)
-		}
-		if gs.Deleted != globalDeleted {
-			t.Errorf("Incorrect GlobalSize deleted; %d != %d", gs.Deleted, globalDeleted)
-		}
-		if gs.Bytes != globalBytes {
-			t.Errorf("Incorrect GlobalSize bytes; %d != %d", gs.Bytes, globalBytes)
-		}
-
-		h := fileList(haveList(t, m, protocol.LocalDeviceID))
-		sort.Sort(h)
-
-		if fmt.Sprint(h) != fmt.Sprint(localTot) {
-			t.Errorf("Have incorrect (local);\n A: %v !=\n E: %v", h, localTot)
-		}
-
-		var haveFiles, haveDirectories, haveDeleted int
-		var haveBytes int64
-		for _, f := range h {
-			if f.IsInvalid() {
-				continue
-			}
-			switch {
-			case f.IsDeleted():
-				haveDeleted++
-			case f.IsDirectory():
-				haveDirectories++
-			default:
-				haveFiles++
-			}
-			haveBytes += f.FileSize()
-		}
-		ls := localSize(t, m)
-		if ls.Files != haveFiles {
-			t.Errorf("Incorrect LocalSize files; %d != %d", ls.Files, haveFiles)
-		}
-		if ls.Directories != haveDirectories {
-			t.Errorf("Incorrect LocalSize directories; %d != %d", ls.Directories, haveDirectories)
-		}
-		if ls.Deleted != haveDeleted {
-			t.Errorf("Incorrect LocalSize deleted; %d != %d", ls.Deleted, haveDeleted)
-		}
-		if ls.Bytes != haveBytes {
-			t.Errorf("Incorrect LocalSize bytes; %d != %d", ls.Bytes, haveBytes)
-		}
-
-		h = fileList(haveList(t, m, remoteDevice0))
-		sort.Sort(h)
-
-		if fmt.Sprint(h) != fmt.Sprint(remoteTot) {
-			t.Errorf("Have incorrect (remote);\n A: %v !=\n E: %v", h, remoteTot)
-		}
-
-		n := fileList(needList(t, m, protocol.LocalDeviceID))
-		sort.Sort(n)
-
-		if fmt.Sprint(n) != fmt.Sprint(expectedLocalNeed) {
-			t.Errorf("Need incorrect (local);\n A: %v !=\n E: %v", n, expectedLocalNeed)
-		}
-
-		checkNeed(t, m, protocol.LocalDeviceID, expectedLocalNeed)
-
-		n = fileList(needList(t, m, remoteDevice0))
-		sort.Sort(n)
-
-		if fmt.Sprint(n) != fmt.Sprint(expectedRemoteNeed) {
-			t.Errorf("Need incorrect (remote);\n A: %v !=\n E: %v", n, expectedRemoteNeed)
-		}
-
-		checkNeed(t, m, remoteDevice0, expectedRemoteNeed)
-
-		snap := snapshot(t, m)
-		defer snap.Release()
-		f, ok := snap.Get(protocol.LocalDeviceID, "b")
-		if !ok {
-			t.Error("Unexpectedly not OK")
-		}
-		if fmt.Sprint(f) != fmt.Sprint(localTot[1]) {
-			t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, localTot[1])
-		}
-
-		f, ok = snap.Get(remoteDevice0, "b")
-		if !ok {
-			t.Error("Unexpectedly not OK")
-		}
-		if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
-			t.Errorf("Get incorrect (remote);\n A: %v !=\n E: %v", f, remote1[0])
-		}
-
-		f, ok = snap.GetGlobal("b")
-		if !ok {
-			t.Error("Unexpectedly not OK")
-		}
-		if fmt.Sprint(f) != fmt.Sprint(expectedGlobal[1]) {
-			t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, remote1[0])
-		}
-
-		f, ok = snap.Get(protocol.LocalDeviceID, "zz")
-		if ok {
-			t.Error("Unexpectedly OK")
-		}
-		if f.Name != "" {
-			t.Errorf("Get incorrect (local);\n A: %v !=\n E: %v", f, protocol.FileInfo{})
-		}
-
-		f, ok = snap.GetGlobal("zz")
-		if ok {
-			t.Error("Unexpectedly OK")
-		}
-		if f.Name != "" {
-			t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
-		}
-	}
-
-	check()
-
-	snap := snapshot(t, m)
-
-	av := []protocol.DeviceID{protocol.LocalDeviceID, remoteDevice0}
-	a := snap.Availability("a")
-	if !(len(a) == 2 && (a[0] == av[0] && a[1] == av[1] || a[0] == av[1] && a[1] == av[0])) {
-		t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, av)
-	}
-	a = snap.Availability("b")
-	if len(a) != 1 || a[0] != remoteDevice0 {
-		t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, remoteDevice0)
-	}
-	a = snap.Availability("d")
-	if len(a) != 1 || a[0] != protocol.LocalDeviceID {
-		t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, protocol.LocalDeviceID)
-	}
-
-	snap.Release()
-
-	// Now bring another remote into play
-
-	secRemote := fileList{
-		local1[0],  // a
-		remote1[0], // b
-		local1[3],  // d
-		remote1[1], // e
-		local1[4],  // z
-	}
-	secRemote[0].Version = secRemote[0].Version.Update(remoteDevice1.Short())
-	secRemote[1].Version = secRemote[1].Version.Update(remoteDevice1.Short())
-	secRemote[4].Version = secRemote[4].Version.Update(remoteDevice1.Short())
-	secRemote[4].Deleted = false
-	secRemote[4].Blocks = genBlocks(1)
-	setSequence(0, secRemote)
-
-	expectedGlobal = fileList{
-		secRemote[0], // a
-		secRemote[1], // b
-		remote0[2],   // c
-		localTot[3],  // d
-		secRemote[3], // e
-		secRemote[4], // z
-	}
-
-	expectedLocalNeed = fileList{
-		secRemote[0], // a
-		secRemote[1], // b
-		remote0[2],   // c
-		secRemote[3], // e
-		secRemote[4], // z
-	}
-
-	expectedRemoteNeed = fileList{
-		secRemote[0], // a
-		secRemote[1], // b
-		local0[3],    // d
-		secRemote[4], // z
-	}
-
-	expectedSecRemoteNeed := fileList{
-		remote0[2], // c
-	}
-
-	m.Update(remoteDevice1, secRemote)
-
-	check()
-
-	h := fileList(haveList(t, m, remoteDevice1))
-	sort.Sort(h)
-
-	if fmt.Sprint(h) != fmt.Sprint(secRemote) {
-		t.Errorf("Have incorrect (secRemote);\n A: %v !=\n E: %v", h, secRemote)
-	}
-
-	n := fileList(needList(t, m, remoteDevice1))
-	sort.Sort(n)
-
-	if fmt.Sprint(n) != fmt.Sprint(expectedSecRemoteNeed) {
-		t.Errorf("Need incorrect (secRemote);\n A: %v !=\n E: %v", n, expectedSecRemoteNeed)
-	}
-
-	checkNeed(t, m, remoteDevice1, expectedSecRemoteNeed)
-}
-
-func TestNeedWithInvalid(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	localHave := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-	}
-	remote0Have := fileList{
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), RawInvalid: true},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
-	}
-	remote1Have := fileList{
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(7)},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(5), RawInvalid: true},
-		protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1004}}}, Blocks: genBlocks(5), RawInvalid: true},
-	}
-
-	expectedNeed := fileList{
-		remote0Have[0],
-		remote1Have[0],
-		remote0Have[2],
-	}
-
-	replace(s, protocol.LocalDeviceID, localHave)
-	replace(s, remoteDevice0, remote0Have)
-	replace(s, remoteDevice1, remote1Have)
-
-	need := fileList(needList(t, s, protocol.LocalDeviceID))
-	sort.Sort(need)
-
-	if fmt.Sprint(need) != fmt.Sprint(expectedNeed) {
-		t.Errorf("Need incorrect;\n A: %v !=\n E: %v", need, expectedNeed)
-	}
-
-	checkNeed(t, s, protocol.LocalDeviceID, expectedNeed)
-}
-
-func TestUpdateToInvalid(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	s := newFileSet(t, folder, ldb)
-	f := db.NewBlockFinder(ldb)
-
-	localHave := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1), Size: 1},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2), Size: 1},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), LocalFlags: protocol.FlagLocalIgnored, Size: 1},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7), Size: 1},
-		protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, LocalFlags: protocol.FlagLocalIgnored, Size: 1},
-	}
-
-	replace(s, protocol.LocalDeviceID, localHave)
-
-	have := fileList(haveList(t, s, protocol.LocalDeviceID))
-	sort.Sort(have)
-
-	if fmt.Sprint(have) != fmt.Sprint(localHave) {
-		t.Errorf("Have incorrect before invalidation;\n A: %v !=\n E: %v", have, localHave)
-	}
-
-	oldBlockHash := localHave[1].Blocks[0].Hash
-
-	localHave[1].LocalFlags = protocol.FlagLocalIgnored
-	localHave[1].Blocks = nil
-
-	localHave[4].LocalFlags = 0
-	localHave[4].Blocks = genBlocks(3)
-
-	s.Update(protocol.LocalDeviceID, append(fileList{}, localHave[1], localHave[4]))
-
-	have = fileList(haveList(t, s, protocol.LocalDeviceID))
-	sort.Sort(have)
-
-	if fmt.Sprint(have) != fmt.Sprint(localHave) {
-		t.Errorf("Have incorrect after invalidation;\n A: %v !=\n E: %v", have, localHave)
-	}
-
-	f.Iterate([]string{folder}, oldBlockHash, func(folder, file string, index int32) bool {
-		if file == localHave[1].Name {
-			t.Errorf("Found unexpected block in blockmap for invalidated file")
-			return true
-		}
-		return false
-	})
-
-	if !f.Iterate([]string{folder}, localHave[4].Blocks[0].Hash, func(folder, file string, index int32) bool {
-		return file == localHave[4].Name
-	}) {
-		t.Errorf("First block of un-invalidated file is missing from blockmap")
-	}
-}
-
-func TestInvalidAvailability(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	remote0Have := fileList{
-		protocol.FileInfo{Name: "both", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "r1only", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), RawInvalid: true},
-		protocol.FileInfo{Name: "r0only", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
-		protocol.FileInfo{Name: "none", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1004}}}, Blocks: genBlocks(5), RawInvalid: true},
-	}
-	remote1Have := fileList{
-		protocol.FileInfo{Name: "both", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "r1only", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(7)},
-		protocol.FileInfo{Name: "r0only", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(5), RawInvalid: true},
-		protocol.FileInfo{Name: "none", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1004}}}, Blocks: genBlocks(5), RawInvalid: true},
-	}
-
-	replace(s, remoteDevice0, remote0Have)
-	replace(s, remoteDevice1, remote1Have)
-
-	snap := snapshot(t, s)
-	defer snap.Release()
-
-	if av := snap.Availability("both"); len(av) != 2 {
-		t.Error("Incorrect availability for 'both':", av)
-	}
-
-	if av := snap.Availability("r0only"); len(av) != 1 || av[0] != remoteDevice0 {
-		t.Error("Incorrect availability for 'r0only':", av)
-	}
-
-	if av := snap.Availability("r1only"); len(av) != 1 || av[0] != remoteDevice1 {
-		t.Error("Incorrect availability for 'r1only':", av)
-	}
-
-	if av := snap.Availability("none"); len(av) != 0 {
-		t.Error("Incorrect availability for 'none':", av)
-	}
-}
-
-func TestGlobalReset(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	m := newFileSet(t, "test", ldb)
-
-	local := []protocol.FileInfo{
-		{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "b", Sequence: 2, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "c", Sequence: 3, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "d", Sequence: 4, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	remote := []protocol.FileInfo{
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	replace(m, protocol.LocalDeviceID, local)
-	g := globalList(t, m)
-	sort.Sort(fileList(g))
-
-	if diff, equal := messagediff.PrettyDiff(local, g); !equal {
-		t.Errorf("Global incorrect;\nglobal: %v\n!=\nlocal: %v\ndiff:\n%s", g, local, diff)
-	}
-
-	replace(m, remoteDevice0, remote)
-	replace(m, remoteDevice0, nil)
-
-	g = globalList(t, m)
-	sort.Sort(fileList(g))
-
-	if diff, equal := messagediff.PrettyDiff(local, g); !equal {
-		t.Errorf("Global incorrect;\nglobal: %v\n!=\nlocal: %v\ndiff:\n%s", g, local, diff)
-	}
-}
-
-func TestNeed(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	m := newFileSet(t, "test", ldb)
-
-	local := []protocol.FileInfo{
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	remote := []protocol.FileInfo{
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	shouldNeed := []protocol.FileInfo{
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	replace(m, protocol.LocalDeviceID, local)
-	replace(m, remoteDevice0, remote)
-
-	need := needList(t, m, protocol.LocalDeviceID)
-
-	sort.Sort(fileList(need))
-	sort.Sort(fileList(shouldNeed))
-
-	if fmt.Sprint(need) != fmt.Sprint(shouldNeed) {
-		t.Errorf("Need incorrect;\n%v !=\n%v", need, shouldNeed)
-	}
-
-	checkNeed(t, m, protocol.LocalDeviceID, shouldNeed)
-}
-
-func TestSequence(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	m := newFileSet(t, "test", ldb)
-
-	local1 := []protocol.FileInfo{
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	local2 := []protocol.FileInfo{
-		local1[0],
-		// [1] deleted
-		local1[2],
-		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	replace(m, protocol.LocalDeviceID, local1)
-	c0 := m.Sequence(protocol.LocalDeviceID)
-
-	replace(m, protocol.LocalDeviceID, local2)
-	c1 := m.Sequence(protocol.LocalDeviceID)
-	if !(c1 > c0) {
-		t.Fatal("Local version number should have incremented")
-	}
-}
-
-func TestListDropFolder(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s0 := newFileSet(t, "test0", ldb)
-	local1 := []protocol.FileInfo{
-		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-	replace(s0, protocol.LocalDeviceID, local1)
-
-	s1 := newFileSet(t, "test1", ldb)
-	local2 := []protocol.FileInfo{
-		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-		{Name: "f", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
-	}
-	replace(s1, remoteDevice0, local2)
-
-	// Check that we have both folders and their data is in the global list
-
-	expectedFolderList := []string{"test0", "test1"}
-	actualFolderList := ldb.ListFolders()
-	if diff, equal := messagediff.PrettyDiff(expectedFolderList, actualFolderList); !equal {
-		t.Fatalf("FolderList mismatch. Diff:\n%s", diff)
-	}
-	if l := len(globalList(t, s0)); l != 3 {
-		t.Errorf("Incorrect global length %d != 3 for s0", l)
-	}
-	if l := len(globalList(t, s1)); l != 3 {
-		t.Errorf("Incorrect global length %d != 3 for s1", l)
-	}
-
-	// Drop one of them and check that it's gone.
-
-	db.DropFolder(ldb, "test1")
-
-	expectedFolderList = []string{"test0"}
-	actualFolderList = ldb.ListFolders()
-	if diff, equal := messagediff.PrettyDiff(expectedFolderList, actualFolderList); !equal {
-		t.Fatalf("FolderList mismatch. Diff:\n%s", diff)
-	}
-	if l := len(globalList(t, s0)); l != 3 {
-		t.Errorf("Incorrect global length %d != 3 for s0", l)
-	}
-	if l := len(globalList(t, s1)); l != 0 {
-		t.Errorf("Incorrect global length %d != 0 for s1", l)
-	}
-}
-
-func TestGlobalNeedWithInvalid(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test1", ldb)
-
-	rem0 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, RawInvalid: true},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: remoteDevice0.Short(), Value: 1002}}}},
-	}
-	replace(s, remoteDevice0, rem0)
-
-	rem1 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, RawInvalid: true},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, RawInvalid: true, ModifiedS: 10},
-	}
-	replace(s, remoteDevice1, rem1)
-
-	total := fileList{
-		// There's a valid copy of each file, so it should be merged
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
-		// in conflict and older, but still wins as the other is invalid
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: remoteDevice0.Short(), Value: 1002}}}},
-	}
-
-	need := fileList(needList(t, s, protocol.LocalDeviceID))
-	if fmt.Sprint(need) != fmt.Sprint(total) {
-		t.Errorf("Need incorrect;\n A: %v !=\n E: %v", need, total)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, total)
-
-	global := fileList(globalList(t, s))
-	if fmt.Sprint(global) != fmt.Sprint(total) {
-		t.Errorf("Global incorrect;\n A: %v !=\n E: %v", global, total)
-	}
-}
-
-func TestLongPath(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	var b bytes.Buffer
-	for i := 0; i < 100; i++ {
-		b.WriteString("012345678901234567890123456789012345678901234567890")
-	}
-	name := b.String() // 5000 characters
-
-	local := []protocol.FileInfo{
-		{Name: name, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	replace(s, protocol.LocalDeviceID, local)
-
-	gf := globalList(t, s)
-	if l := len(gf); l != 1 {
-		t.Fatalf("Incorrect len %d != 1 for global list", l)
-	}
-	if gf[0].Name != local[0].Name {
-		t.Errorf("Incorrect long filename;\n%q !=\n%q",
-			gf[0].Name, local[0].Name)
-	}
-}
-
-func BenchmarkUpdateOneFile(b *testing.B) {
-	local0 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
-		// A longer name is more realistic and causes more allocations
-		protocol.FileInfo{Name: "zajksdhaskjdh/askjdhaskjdashkajshd/kasjdhaskjdhaskdjhaskdjash/dkjashdaksjdhaskdjahskdjh", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
-	}
-
-	be, err := backend.Open("testdata/benchmarkupdate.db", backend.TuningAuto)
-	if err != nil {
-		b.Fatal(err)
-	}
-	ldb := newLowlevel(b, be)
-	defer func() {
-		ldb.Close()
-		os.RemoveAll("testdata/benchmarkupdate.db")
-	}()
-
-	m := newFileSet(b, "test", ldb)
-	replace(m, protocol.LocalDeviceID, local0)
-	l := local0[4:5]
-
-	for i := 0; i < b.N; i++ {
-		l[0].Version = l[0].Version.Update(myID)
-		m.Update(protocol.LocalDeviceID, local0)
-	}
-
-	b.ReportAllocs()
-}
-
-func TestIndexID(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	// The Index ID for some random device is zero by default.
-	id := s.IndexID(remoteDevice0)
-	if id != 0 {
-		t.Errorf("index ID for remote device should default to zero, not %d", id)
-	}
-
-	// The Index ID for someone else should be settable
-	s.SetIndexID(remoteDevice0, 42)
-	id = s.IndexID(remoteDevice0)
-	if id != 42 {
-		t.Errorf("index ID for remote device should be remembered; got %d, expected %d", id, 42)
-	}
-
-	// Our own index ID should be generated randomly.
-	id = s.IndexID(protocol.LocalDeviceID)
-	if id == 0 {
-		t.Errorf("index ID for local device should be random, not zero")
-	}
-	t.Logf("random index ID is 0x%016x", id)
-
-	// But of course always the same after that.
-	again := s.IndexID(protocol.LocalDeviceID)
-	if again != id {
-		t.Errorf("index ID changed; %d != %d", again, id)
-	}
-}
-
-func TestDropFiles(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-
-	m := newFileSet(t, "test", ldb)
-
-	local0 := fileList{
-		protocol.FileInfo{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Sequence: 2, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Sequence: 3, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
-		protocol.FileInfo{Name: "d", Sequence: 4, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
-		protocol.FileInfo{Name: "z", Sequence: 5, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
-	}
-
-	remote0 := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(5)},
-	}
-
-	// Insert files
-
-	m.Update(protocol.LocalDeviceID, local0)
-	m.Update(remoteDevice0, remote0)
-
-	// Check that they're there
-
-	h := haveList(t, m, protocol.LocalDeviceID)
-	if len(h) != len(local0) {
-		t.Errorf("Incorrect number of files after update, %d != %d", len(h), len(local0))
-	}
-
-	h = haveList(t, m, remoteDevice0)
-	if len(h) != len(remote0) {
-		t.Errorf("Incorrect number of files after update, %d != %d", len(h), len(local0))
-	}
-
-	g := globalList(t, m)
-	if len(g) != len(local0) {
-		// local0 covers all files
-		t.Errorf("Incorrect global files after update, %d != %d", len(g), len(local0))
-	}
-
-	// Drop the local files and recheck
-
-	m.Drop(protocol.LocalDeviceID)
-
-	h = haveList(t, m, protocol.LocalDeviceID)
-	if len(h) != 0 {
-		t.Errorf("Incorrect number of files after drop, %d != %d", len(h), 0)
-	}
-
-	h = haveList(t, m, remoteDevice0)
-	if len(h) != len(remote0) {
-		t.Errorf("Incorrect number of files after update, %d != %d", len(h), len(local0))
-	}
-
-	g = globalList(t, m)
-	if len(g) != len(remote0) {
-		// the ones in remote0 remain
-		t.Errorf("Incorrect global files after update, %d != %d", len(g), len(remote0))
-	}
-}
-
-func TestIssue4701(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	localHave := fileList{
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, LocalFlags: protocol.FlagLocalIgnored},
-	}
-
-	s.Update(protocol.LocalDeviceID, localHave)
-
-	if c := localSize(t, s); c.Files != 1 {
-		t.Errorf("Expected 1 local file, got %v", c.Files)
-	}
-	if c := globalSize(t, s); c.Files != 1 {
-		t.Errorf("Expected 1 global file, got %v", c.Files)
-	}
-
-	localHave[1].LocalFlags = 0
-	s.Update(protocol.LocalDeviceID, localHave)
-
-	if c := localSize(t, s); c.Files != 2 {
-		t.Errorf("Expected 2 local files, got %v", c.Files)
-	}
-	if c := globalSize(t, s); c.Files != 2 {
-		t.Errorf("Expected 2 global files, got %v", c.Files)
-	}
-
-	localHave[0].LocalFlags = protocol.FlagLocalIgnored
-	localHave[1].LocalFlags = protocol.FlagLocalIgnored
-	s.Update(protocol.LocalDeviceID, localHave)
-
-	if c := localSize(t, s); c.Files != 0 {
-		t.Errorf("Expected 0 local files, got %v", c.Files)
-	}
-	if c := globalSize(t, s); c.Files != 0 {
-		t.Errorf("Expected 0 global files, got %v", c.Files)
-	}
-}
-
-func TestWithHaveSequence(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	s := newFileSet(t, folder, ldb)
-
-	// The files must not be in alphabetical order
-	localHave := fileList{
-		protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, RawInvalid: true},
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-		protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
-		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
-		protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), RawInvalid: true},
-	}
-
-	replace(s, protocol.LocalDeviceID, localHave)
-
-	i := 2
-	snap := snapshot(t, s)
-	defer snap.Release()
-	snap.WithHaveSequence(int64(i), func(fi protocol.FileInfo) bool {
-		if !fi.IsEquivalent(localHave[i-1], 0) {
-			t.Fatalf("Got %v\nExpected %v", fi, localHave[i-1])
-		}
-		i++
-		return true
-	})
-}
-
-func TestStressWithHaveSequence(t *testing.T) {
-	// This races two loops against each other: one that continuously does
-	// updates, and one that continuously does sequence walks. The test fails
-	// if the sequence walker sees a discontinuity.
-
-	if testing.Short() {
-		t.Skip("Takes a long time")
-	}
-
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	s := newFileSet(t, folder, ldb)
-
-	var localHave []protocol.FileInfo
-	for i := 0; i < 100; i++ {
-		localHave = append(localHave, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Blocks: genBlocks(i * 10)})
-	}
-
-	done := make(chan struct{})
-	t0 := time.Now()
-	go func() {
-		for time.Since(t0) < 10*time.Second {
-			for j, f := range localHave {
-				localHave[j].Version = f.Version.Update(42)
-			}
-
-			s.Update(protocol.LocalDeviceID, localHave)
-		}
-		close(done)
-	}()
-
-	var prevSeq int64
-loop:
-	for {
-		select {
-		case <-done:
-			break loop
-		default:
-		}
-		snap := snapshot(t, s)
-		snap.WithHaveSequence(prevSeq+1, func(fi protocol.FileInfo) bool {
-			if fi.SequenceNo() < prevSeq+1 {
-				t.Fatal("Skipped ", prevSeq+1, fi.SequenceNo())
-			}
-			prevSeq = fi.SequenceNo()
-			return true
-		})
-		snap.Release()
-	}
-}
-
-func TestIssue4925(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	s := newFileSet(t, folder, ldb)
-
-	localHave := fileList{
-		protocol.FileInfo{Name: "dir"},
-		protocol.FileInfo{Name: "dir.file"},
-		protocol.FileInfo{Name: "dir/file"},
-	}
-
-	replace(s, protocol.LocalDeviceID, localHave)
-
-	for _, prefix := range []string{"dir", "dir/"} {
-		pl := haveListPrefixed(t, s, protocol.LocalDeviceID, prefix)
-		if l := len(pl); l != 2 {
-			t.Errorf("Expected 2, got %v local items below %v", l, prefix)
-		}
-		pl = globalListPrefixed(t, s, prefix)
-		if l := len(pl); l != 2 {
-			t.Errorf("Expected 2, got %v global items below %v", l, prefix)
-		}
-	}
-}
-
-func TestMoveGlobalBack(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	file := "foo"
-	s := newFileSet(t, folder, ldb)
-
-	localHave := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}, Blocks: genBlocks(1), ModifiedS: 10, Size: 1}}
-	remote0Have := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}, {ID: remoteDevice0.Short(), Value: 1}}}, Blocks: genBlocks(2), ModifiedS: 0, Size: 2}}
-
-	s.Update(protocol.LocalDeviceID, localHave)
-	s.Update(remoteDevice0, remote0Have)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 1 {
-		t.Error("Expected 1 local need, got", need)
-	} else if !need[0].IsEquivalent(remote0Have[0], 0) {
-		t.Errorf("Local need incorrect;\n A: %v !=\n E: %v", need[0], remote0Have[0])
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, remote0Have[:1])
-
-	if need := needList(t, s, remoteDevice0); len(need) != 0 {
-		t.Error("Expected no need for remote 0, got", need)
-	}
-	checkNeed(t, s, remoteDevice0, nil)
-
-	ls := localSize(t, s)
-	if haveBytes := localHave[0].Size; ls.Bytes != haveBytes {
-		t.Errorf("Incorrect LocalSize bytes; %d != %d", ls.Bytes, haveBytes)
-	}
-
-	gs := globalSize(t, s)
-	if globalBytes := remote0Have[0].Size; gs.Bytes != globalBytes {
-		t.Errorf("Incorrect GlobalSize bytes; %d != %d", gs.Bytes, globalBytes)
-	}
-
-	// That's what happens when something becomes unignored or something.
-	// In any case it will be moved back from first spot in the global list
-	// which is the scenario to be tested here.
-	remote0Have[0].Version = remote0Have[0].Version.Update(remoteDevice0.Short()).DropOthers(remoteDevice0.Short())
-	s.Update(remoteDevice0, remote0Have)
-
-	if need := needList(t, s, remoteDevice0); len(need) != 1 {
-		t.Error("Expected 1 need for remote 0, got", need)
-	} else if !need[0].IsEquivalent(localHave[0], 0) {
-		t.Errorf("Need for remote 0 incorrect;\n A: %v !=\n E: %v", need[0], localHave[0])
-	}
-	checkNeed(t, s, remoteDevice0, localHave[:1])
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 0 {
-		t.Error("Expected no local need, got", need)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, nil)
-
-	ls = localSize(t, s)
-	if haveBytes := localHave[0].Size; ls.Bytes != haveBytes {
-		t.Errorf("Incorrect LocalSize bytes; %d != %d", ls.Bytes, haveBytes)
-	}
-
-	gs = globalSize(t, s)
-	if globalBytes := localHave[0].Size; gs.Bytes != globalBytes {
-		t.Errorf("Incorrect GlobalSize bytes; %d != %d", gs.Bytes, globalBytes)
-	}
-}
-
-// TestIssue5007 checks, that updating the local device with an invalid file
-// info with the newest version does indeed remove that file from the list of
-// needed files.
-// https://github.com/syncthing/syncthing/issues/5007
-func TestIssue5007(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	file := "foo"
-	s := newFileSet(t, folder, ldb)
-
-	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}}
-
-	s.Update(remoteDevice0, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 1 {
-		t.Fatal("Expected 1 local need, got", need)
-	} else if !need[0].IsEquivalent(fs[0], 0) {
-		t.Fatalf("Local need incorrect;\n A: %v !=\n E: %v", need[0], fs[0])
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, fs[:1])
-
-	fs[0].LocalFlags = protocol.FlagLocalIgnored
-	s.Update(protocol.LocalDeviceID, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 0 {
-		t.Fatal("Expected no local need, got", need)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, nil)
-}
-
-// TestNeedDeleted checks that a file that doesn't exist locally isn't needed
-// when the global file is deleted.
-func TestNeedDeleted(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	file := "foo"
-	s := newFileSet(t, folder, ldb)
-
-	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}, Deleted: true}}
-
-	s.Update(remoteDevice0, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 0 {
-		t.Fatal("Expected no local need, got", need)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, nil)
-
-	fs[0].Deleted = false
-	fs[0].Version = fs[0].Version.Update(remoteDevice0.Short())
-	s.Update(remoteDevice0, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 1 {
-		t.Fatal("Expected 1 local need, got", need)
-	} else if !need[0].IsEquivalent(fs[0], 0) {
-		t.Fatalf("Local need incorrect;\n A: %v !=\n E: %v", need[0], fs[0])
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, fs[:1])
-
-	fs[0].Deleted = true
-	fs[0].Version = fs[0].Version.Update(remoteDevice0.Short())
-	s.Update(remoteDevice0, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 0 {
-		t.Fatal("Expected no local need, got", need)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, nil)
-}
-
-func TestReceiveOnlyAccounting(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	s := newFileSet(t, folder, ldb)
-
-	local := protocol.DeviceID{1}
-	remote := protocol.DeviceID{2}
-
-	// Three files that have been created by the remote device
-
-	version := protocol.Vector{Counters: []protocol.Counter{{ID: remote.Short(), Value: 1}}}
-	files := fileList{
-		protocol.FileInfo{Name: "f1", Size: 10, Sequence: 1, Version: version},
-		protocol.FileInfo{Name: "f2", Size: 10, Sequence: 1, Version: version},
-		protocol.FileInfo{Name: "f3", Size: 10, Sequence: 1, Version: version},
-	}
-
-	// We have synced them locally
-
-	replace(s, protocol.LocalDeviceID, files)
-	replace(s, remote, files)
-
-	if n := localSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 local files initially, not", n)
-	}
-	if n := localSize(t, s).Bytes; n != 30 {
-		t.Fatal("expected 30 local bytes initially, not", n)
-	}
-	if n := globalSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 global files initially, not", n)
-	}
-	if n := globalSize(t, s).Bytes; n != 30 {
-		t.Fatal("expected 30 global bytes initially, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Files; n != 0 {
-		t.Fatal("expected 0 receive only changed files initially, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Bytes; n != 0 {
-		t.Fatal("expected 0 receive only changed bytes initially, not", n)
-	}
-
-	// Detected a local change in a receive only folder
-
-	changed := files[0]
-	changed.Version = changed.Version.Update(local.Short())
-	changed.Size = 100
-	changed.ModifiedBy = local.Short()
-	changed.LocalFlags = protocol.FlagLocalReceiveOnly
-	s.Update(protocol.LocalDeviceID, []protocol.FileInfo{changed})
-
-	// Check that we see the files
-
-	if n := localSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 local files after local change, not", n)
-	}
-	if n := localSize(t, s).Bytes; n != 120 {
-		t.Fatal("expected 120 local bytes after local change, not", n)
-	}
-	if n := globalSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 global files after local change, not", n)
-	}
-	if n := globalSize(t, s).Bytes; n != 30 {
-		t.Fatal("expected 30 global files after local change, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Files; n != 1 {
-		t.Fatal("expected 1 receive only changed file after local change, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Bytes; n != 100 {
-		t.Fatal("expected 100 receive only changed bytes after local change, not", n)
-	}
-
-	// Fake a revert. That's a two step process, first converting our
-	// changed file into a less preferred variant, then pulling down the old
-	// version.
-
-	changed.Version = protocol.Vector{}
-	changed.LocalFlags &^= protocol.FlagLocalReceiveOnly
-	s.Update(protocol.LocalDeviceID, []protocol.FileInfo{changed})
-
-	s.Update(protocol.LocalDeviceID, []protocol.FileInfo{files[0]})
-
-	// Check that we see the files, same data as initially
-
-	if n := localSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 local files after revert, not", n)
-	}
-	if n := localSize(t, s).Bytes; n != 30 {
-		t.Fatal("expected 30 local bytes after revert, not", n)
-	}
-	if n := globalSize(t, s).Files; n != 3 {
-		t.Fatal("expected 3 global files after revert, not", n)
-	}
-	if n := globalSize(t, s).Bytes; n != 30 {
-		t.Fatal("expected 30 global bytes after revert, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Files; n != 0 {
-		t.Fatal("expected 0 receive only changed files after revert, not", n)
-	}
-	if n := receiveOnlyChangedSize(t, s).Bytes; n != 0 {
-		t.Fatal("expected 0 receive only changed bytes after revert, not", n)
-	}
-}
-
-func TestNeedAfterUnignore(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	folder := "test"
-	file := "foo"
-	s := newFileSet(t, folder, ldb)
-
-	remID := remoteDevice0.Short()
-
-	// Initial state: Devices in sync, locally ignored
-	local := protocol.FileInfo{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: remID, Value: 1}, {ID: myID, Value: 1}}}, ModifiedS: 10}
-	local.SetIgnored()
-	remote := protocol.FileInfo{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: remID, Value: 1}, {ID: myID, Value: 1}}}, ModifiedS: 10}
-	s.Update(protocol.LocalDeviceID, fileList{local})
-	s.Update(remoteDevice0, fileList{remote})
-
-	// Unignore locally -> conflicting changes. Remote is newer, thus winning.
-	local.Version = local.Version.Update(myID)
-	local.Version = local.Version.DropOthers(myID)
-	local.LocalFlags = 0
-	local.ModifiedS = 0
-	s.Update(protocol.LocalDeviceID, fileList{local})
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 1 {
-		t.Fatal("Expected one local need, got", need)
-	} else if !need[0].IsEquivalent(remote, 0) {
-		t.Fatalf("Got %v, expected %v", need[0], remote)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, []protocol.FileInfo{remote})
-}
-
-func TestRemoteInvalidNotAccounted(t *testing.T) {
-	// Remote files with the invalid bit should not count.
-
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-	s := newFileSet(t, "test", ldb)
-
-	files := []protocol.FileInfo{
-		{Name: "a", Size: 1234, Sequence: 42, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}},                   // valid, should count
-		{Name: "b", Size: 1234, Sequence: 43, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, RawInvalid: true}, // invalid, doesn't count
-	}
-	s.Update(remoteDevice0, files)
-
-	global := globalSize(t, s)
-	if global.Files != 1 {
-		t.Error("Expected one file in global size, not", global.Files)
-	}
-	if global.Bytes != 1234 {
-		t.Error("Expected 1234 bytes in global size, not", global.Bytes)
-	}
-}
-
-func TestNeedWithNewerInvalid(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "default", ldb)
-
-	rem0ID := remoteDevice0.Short()
-	rem1ID := remoteDevice1.Short()
-
-	// Initial state: file present on rem0 and rem1, but not locally.
-	file := protocol.FileInfo{Name: "foo"}
-	file.Version = file.Version.Update(rem0ID)
-	s.Update(remoteDevice0, fileList{file})
-	s.Update(remoteDevice1, fileList{file})
-
-	need := needList(t, s, protocol.LocalDeviceID)
-	if len(need) != 1 {
-		t.Fatal("Locally missing file should be needed")
-	}
-	if !need[0].IsEquivalent(file, 0) {
-		t.Fatalf("Got needed file %v, expected %v", need[0], file)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, []protocol.FileInfo{file})
-
-	// rem1 sends an invalid file with increased version
-	inv := file
-	inv.Version = inv.Version.Update(rem1ID)
-	inv.RawInvalid = true
-	s.Update(remoteDevice1, fileList{inv})
-
-	// We still have an old file, we need the newest valid file
-	need = needList(t, s, protocol.LocalDeviceID)
-	if len(need) != 1 {
-		t.Fatal("Locally missing file should be needed regardless of invalid files")
-	}
-	if !need[0].IsEquivalent(file, 0) {
-		t.Fatalf("Got needed file %v, expected %v", need[0], file)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, []protocol.FileInfo{file})
-}
-
-func TestNeedAfterDeviceRemove(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	file := "foo"
-	s := newFileSet(t, "test", ldb)
-
-	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}}
-
-	s.Update(protocol.LocalDeviceID, fs)
-
-	fs[0].Version = fs[0].Version.Update(myID)
-
-	s.Update(remoteDevice0, fs)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 1 {
-		t.Fatal("Expected one local need, got", need)
-	}
-
-	s.Drop(remoteDevice0)
-
-	if need := needList(t, s, protocol.LocalDeviceID); len(need) != 0 {
-		t.Fatal("Expected no local need, got", need)
-	}
-	checkNeed(t, s, protocol.LocalDeviceID, nil)
-}
-
-func TestCaseSensitive(t *testing.T) {
-	// Normal case sensitive lookup should work
-
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-	s := newFileSet(t, "test", ldb)
-
-	local := []protocol.FileInfo{
-		{Name: filepath.FromSlash("D1/f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("F1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("d1/F1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("d1/f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	replace(s, protocol.LocalDeviceID, local)
-
-	gf := globalList(t, s)
-	if l := len(gf); l != len(local) {
-		t.Fatalf("Incorrect len %d != %d for global list", l, len(local))
-	}
-	for i := range local {
-		if gf[i].Name != local[i].Name {
-			t.Errorf("Incorrect  filename;\n%q !=\n%q",
-				gf[i].Name, local[i].Name)
-		}
-	}
-}
-
-func TestSequenceIndex(t *testing.T) {
-	// This test attempts to verify correct operation of the sequence index.
-
-	// It's a stress test and needs to run for a long time, but we don't
-	// really have time for that in normal builds.
-	runtime := time.Minute
-	if testing.Short() {
-		runtime = time.Second
-	}
-
-	// Set up a db and a few files that we will manipulate.
-
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-	s := newFileSet(t, "test", ldb)
-
-	local := []protocol.FileInfo{
-		{Name: filepath.FromSlash("banana"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("pineapple"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("orange"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("apple"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-		{Name: filepath.FromSlash("jackfruit"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
-	}
-
-	// Start a  background routine that makes updates to these files as fast
-	// as it can. We always update the same files in the same order.
-
-	done := make(chan struct{})
-	defer close(done)
-
-	go func() {
-		for {
-			select {
-			case <-done:
-				return
-			default:
-			}
-
-			for i := range local {
-				local[i].Version = local[i].Version.Update(42)
-			}
-			s.Update(protocol.LocalDeviceID, local)
-		}
-	}()
-
-	// Start a routine to walk the sequence index and inspect the result.
-
-	seen := make(map[string]protocol.FileInfo)
-	latest := make([]protocol.FileInfo, 0, len(local))
-	var seq int64
-	t0 := time.Now()
-
-	for time.Since(t0) < runtime {
-		// Walk the changes since our last iteration. This should give is
-		// one instance each of the files that are changed all the time, or
-		// a subset of those files if we manage to run before a complete
-		// update has happened since our last iteration.
-		latest = latest[:0]
-		snap := snapshot(t, s)
-		snap.WithHaveSequence(seq+1, func(f protocol.FileInfo) bool {
-			seen[f.FileName()] = f
-			latest = append(latest, f)
-			seq = f.SequenceNo()
-			return true
-		})
-		snap.Release()
-
-		// Calculate the spread in sequence number.
-		var max, min int64
-		for _, v := range seen {
-			s := v.SequenceNo()
-			if max == 0 || max < s {
-				max = s
-			}
-			if min == 0 || min > s {
-				min = s
-			}
-		}
-
-		// We shouldn't see a spread larger than the number of files, as
-		// that would mean we have missed updates. For example, if we were
-		// to see the following:
-		//
-		// banana    N
-		// pineapple N+1
-		// orange    N+2
-		// apple     N+10
-		// jackfruit N+11
-		//
-		// that would mean that there have been updates to banana, pineapple
-		// and orange that we didn't see in this pass. If those files aren't
-		// updated again, those updates are permanently lost.
-		if max-min > int64(len(local)) {
-			for _, v := range seen {
-				t.Log("seen", v.FileName(), v.SequenceNo())
-			}
-			for _, v := range latest {
-				t.Log("latest", v.FileName(), v.SequenceNo())
-			}
-			t.Fatal("large spread")
-		}
-		time.Sleep(time.Millisecond)
-	}
-}
-
-func TestIgnoreAfterReceiveOnly(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	file := "foo"
-	s := newFileSet(t, "test", ldb)
-
-	fs := fileList{{
-		Name:       file,
-		Version:    protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}},
-		LocalFlags: protocol.FlagLocalReceiveOnly,
-	}}
-
-	s.Update(protocol.LocalDeviceID, fs)
-
-	fs[0].LocalFlags = protocol.FlagLocalIgnored
-
-	s.Update(protocol.LocalDeviceID, fs)
-
-	snap := snapshot(t, s)
-	defer snap.Release()
-	if f, ok := snap.Get(protocol.LocalDeviceID, file); !ok {
-		t.Error("File missing in db")
-	} else if f.IsReceiveOnlyChanged() {
-		t.Error("File is still receive-only changed")
-	} else if !f.IsIgnored() {
-		t.Error("File is not ignored")
-	}
-}
-
-// https://github.com/syncthing/syncthing/issues/6650
-func TestUpdateWithOneFileTwice(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	file := "foo"
-	s := newFileSet(t, "test", ldb)
-
-	fs := fileList{{
-		Name:     file,
-		Version:  protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}},
-		Sequence: 1,
-	}}
-
-	s.Update(protocol.LocalDeviceID, fs)
-
-	fs = append(fs, fs[0])
-	for i := range fs {
-		fs[i].Sequence++
-		fs[i].Version = fs[i].Version.Update(myID)
-	}
-	fs[1].Sequence++
-	fs[1].Version = fs[1].Version.Update(myID)
-
-	s.Update(protocol.LocalDeviceID, fs)
-
-	snap := snapshot(t, s)
-	defer snap.Release()
-	count := 0
-	snap.WithHaveSequence(0, func(_ protocol.FileInfo) bool {
-		count++
-		return true
-	})
-	if count != 1 {
-		t.Error("Expected to have one file, got", count)
-	}
-}
-
-// https://github.com/syncthing/syncthing/issues/6668
-func TestNeedRemoteOnly(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	remote0Have := fileList{
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-	}
-	s.Update(remoteDevice0, remote0Have)
-
-	need := needSize(t, s, remoteDevice0)
-	if !need.Equal(db.Counts{}) {
-		t.Error("Expected nothing needed, got", need)
-	}
-}
-
-// https://github.com/syncthing/syncthing/issues/6784
-func TestNeedRemoteAfterReset(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	files := fileList{
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
-	}
-	s.Update(protocol.LocalDeviceID, files)
-	s.Update(remoteDevice0, files)
-
-	need := needSize(t, s, remoteDevice0)
-	if !need.Equal(db.Counts{}) {
-		t.Error("Expected nothing needed, got", need)
-	}
-
-	s.Drop(remoteDevice0)
-
-	need = needSize(t, s, remoteDevice0)
-	if exp := (db.Counts{Files: 1}); !need.Equal(exp) {
-		t.Errorf("Expected %v, got %v", exp, need)
-	}
-}
-
-// https://github.com/syncthing/syncthing/issues/6850
-func TestIgnoreLocalChanged(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	// Add locally changed file
-	files := fileList{
-		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2), LocalFlags: protocol.FlagLocalReceiveOnly},
-	}
-	s.Update(protocol.LocalDeviceID, files)
-
-	if c := globalSize(t, s).Files; c != 0 {
-		t.Error("Expected no global file, got", c)
-	}
-	if c := localSize(t, s).Files; c != 1 {
-		t.Error("Expected one local file, got", c)
-	}
-
-	// Change file to ignored
-	files[0].LocalFlags = protocol.FlagLocalIgnored
-	s.Update(protocol.LocalDeviceID, files)
-
-	if c := globalSize(t, s).Files; c != 0 {
-		t.Error("Expected no global file, got", c)
-	}
-	if c := localSize(t, s).Files; c != 0 {
-		t.Error("Expected no local file, got", c)
-	}
-}
-
-// Dropping the index ID on Drop is bad, because Drop gets called when receiving
-// an Index (as opposed to an IndexUpdate), and we don't want to loose the index
-// ID when that happens.
-func TestNoIndexIDResetOnDrop(t *testing.T) {
-	ldb := newLowlevelMemory(t)
-	defer ldb.Close()
-
-	s := newFileSet(t, "test", ldb)
-
-	s.SetIndexID(remoteDevice0, 1)
-	s.Drop(remoteDevice0)
-	if got := s.IndexID(remoteDevice0); got != 1 {
-		t.Errorf("Expected unchanged (%v), got %v", 1, got)
-	}
-}
-
-func TestConcurrentIndexID(t *testing.T) {
-	done := make(chan struct{})
-	var ids [2]protocol.IndexID
-	setID := func(s *db.FileSet, i int) {
-		ids[i] = s.IndexID(protocol.LocalDeviceID)
-		done <- struct{}{}
-	}
-
-	max := 100
-	if testing.Short() {
-		max = 10
-	}
-	for i := 0; i < max; i++ {
-		ldb := newLowlevelMemory(t)
-		s := newFileSet(t, "test", ldb)
-		go setID(s, 0)
-		go setID(s, 1)
-		<-done
-		<-done
-		ldb.Close()
-		if ids[0] != ids[1] {
-			t.Fatalf("IDs differ after %v rounds", i)
-		}
-	}
-}
-
-func TestNeedRemoveLastValid(t *testing.T) {
-	db := newLowlevelMemory(t)
-	defer db.Close()
-
-	folder := "test"
-
-	fs := newFileSet(t, folder, db)
-
-	files := []protocol.FileInfo{
-		{Name: "foo", Version: protocol.Vector{}.Update(myID), Sequence: 1},
-	}
-	fs.Update(remoteDevice0, files)
-	files[0].Version = files[0].Version.Update(myID)
-	fs.Update(remoteDevice1, files)
-	files[0].LocalFlags = protocol.FlagLocalIgnored
-	fs.Update(protocol.LocalDeviceID, files)
-
-	snap := snapshot(t, fs)
-	c := snap.NeedSize(remoteDevice0)
-	if c.Files != 1 {
-		t.Errorf("Expected 1 needed files initially, got %v", c.Files)
-	}
-	snap.Release()
-
-	fs.Drop(remoteDevice1)
-
-	snap = snapshot(t, fs)
-	c = snap.NeedSize(remoteDevice0)
-	if c.Files != 0 {
-		t.Errorf("Expected no needed files, got %v", c.Files)
-	}
-	snap.Release()
-}
-
-func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
-	fs.Drop(device)
-	fs.Update(device, files)
-}
-
-func localSize(t testing.TB, fs *db.FileSet) db.Counts {
-	snap := snapshot(t, fs)
-	defer snap.Release()
-	return snap.LocalSize()
-}
-
-func globalSize(t testing.TB, fs *db.FileSet) db.Counts {
-	snap := snapshot(t, fs)
-	defer snap.Release()
-	return snap.GlobalSize()
-}
-
-func needSize(t testing.TB, fs *db.FileSet, id protocol.DeviceID) db.Counts {
-	snap := snapshot(t, fs)
-	defer snap.Release()
-	return snap.NeedSize(id)
-}
-
-func receiveOnlyChangedSize(t testing.TB, fs *db.FileSet) db.Counts {
-	snap := snapshot(t, fs)
-	defer snap.Release()
-	return snap.ReceiveOnlyChangedSize()
-}
-
-func filesToCounts(files []protocol.FileInfo) db.Counts {
-	cp := db.Counts{}
-	for _, f := range files {
-		switch {
-		case f.IsDeleted():
-			cp.Deleted++
-		case f.IsDirectory() && !f.IsSymlink():
-			cp.Directories++
-		case f.IsSymlink():
-			cp.Symlinks++
-		default:
-			cp.Files++
-		}
-		cp.Bytes += f.FileSize()
-	}
-	return cp
-}
-
-func checkNeed(t testing.TB, s *db.FileSet, dev protocol.DeviceID, expected []protocol.FileInfo) {
-	t.Helper()
-	counts := needSize(t, s, dev)
-	if exp := filesToCounts(expected); !exp.Equal(counts) {
-		t.Errorf("Count incorrect (%v): expected %v, got %v", dev, exp, counts)
-	}
-}
-
-func newLowlevel(t testing.TB, backend backend.Backend) *db.Lowlevel {
-	t.Helper()
-	ll, err := db.NewLowlevel(backend, events.NoopLogger)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return ll
-}
-
-func newLowlevelMemory(t testing.TB) *db.Lowlevel {
-	return newLowlevel(t, backend.OpenMemory())
-}
-
-func newFileSet(t testing.TB, folder string, ll *db.Lowlevel) *db.FileSet {
-	t.Helper()
-	fset, err := db.NewFileSet(folder, ll)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return fset
-}
-
-func snapshot(t testing.TB, fset *db.FileSet) *db.Snapshot {
-	t.Helper()
-	snap, err := fset.Snapshot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	return snap
-}
diff --git a/lib/db/smallindex_test.go b/lib/db/smallindex_test.go
deleted file mode 100644
index c47a685a0a1..00000000000
--- a/lib/db/smallindex_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"testing"
-)
-
-func TestSmallIndex(t *testing.T) {
-	db := newLowlevelMemory(t)
-	idx := newSmallIndex(db, []byte{12, 34})
-
-	// ID zero should be unallocated
-	if val, ok := idx.Val(0); ok || val != nil {
-		t.Fatal("Unexpected return for nonexistent ID 0")
-	}
-
-	// A new key should get ID zero
-	if id, err := idx.ID([]byte("hello")); err != nil {
-		t.Fatal(err)
-	} else if id != 0 {
-		t.Fatal("Expected 0, not", id)
-	}
-	// Looking up ID zero should work
-	if val, ok := idx.Val(0); !ok || string(val) != "hello" {
-		t.Fatalf(`Expected true, "hello", not %v, %q`, ok, val)
-	}
-
-	// Delete the key
-	idx.Delete([]byte("hello"))
-
-	// Next ID should be one
-	if id, err := idx.ID([]byte("key2")); err != nil {
-		t.Fatal(err)
-	} else if id != 1 {
-		t.Fatal("Expected 1, not", id)
-	}
-
-	// Now lets create a new index instance based on what's actually serialized to the database.
-	idx = newSmallIndex(db, []byte{12, 34})
-
-	// Status should be about the same as before.
-	if val, ok := idx.Val(0); ok || val != nil {
-		t.Fatal("Unexpected return for deleted ID 0")
-	}
-	if id, err := idx.ID([]byte("key2")); err != nil {
-		t.Fatal(err)
-	} else if id != 1 {
-		t.Fatal("Expected 1, not", id)
-	}
-
-	// Setting "hello" again should get us ID 2, not 0 as it was originally.
-	if id, err := idx.ID([]byte("hello")); err != nil {
-		t.Fatal(err)
-	} else if id != 2 {
-		t.Fatal("Expected 2, not", id)
-	}
-}
diff --git a/lib/db/util_test.go b/lib/db/util_test.go
deleted file mode 100644
index cc7d7d6ce58..00000000000
--- a/lib/db/util_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright (C) 2018 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at https://mozilla.org/MPL/2.0/.
-
-package db
-
-import (
-	"encoding/json"
-	"errors"
-	"io"
-	"os"
-	"testing"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/events"
-	"github.com/syncthing/syncthing/lib/protocol"
-)
-
-// writeJSONS serializes the database to a JSON stream that can be checked
-// in to the repo and used for tests.
-func writeJSONS(w io.Writer, db backend.Backend) {
-	it, err := db.NewPrefixIterator(nil)
-	if err != nil {
-		panic(err)
-	}
-	defer it.Release()
-	enc := json.NewEncoder(w)
-	for it.Next() {
-		err := enc.Encode(map[string][]byte{
-			"k": it.Key(),
-			"v": it.Value(),
-		})
-		if err != nil {
-			panic(err)
-		}
-	}
-}
-
-// we know this function isn't generally used, nonetheless we want it in
-// here and the linter to not complain.
-var _ = writeJSONS
-
-// openJSONS reads a JSON stream file into a backend DB
-func openJSONS(file string) (backend.Backend, error) {
-	fd, err := os.Open(file)
-	if err != nil {
-		return nil, err
-	}
-	dec := json.NewDecoder(fd)
-
-	db := backend.OpenMemory()
-
-	for {
-		var row map[string][]byte
-
-		err := dec.Decode(&row)
-		if err == io.EOF {
-			break
-		} else if err != nil {
-			return nil, err
-		}
-
-		if err := db.Put(row["k"], row["v"]); err != nil {
-			return nil, err
-		}
-	}
-
-	return db, nil
-}
-
-func newLowlevel(t testing.TB, backend backend.Backend) *Lowlevel {
-	t.Helper()
-	ll, err := NewLowlevel(backend, events.NoopLogger)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return ll
-}
-
-func newLowlevelMemory(t testing.TB) *Lowlevel {
-	return newLowlevel(t, backend.OpenMemory())
-}
-
-func newFileSet(t testing.TB, folder string, db *Lowlevel) *FileSet {
-	t.Helper()
-	fset, err := NewFileSet(folder, db)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return fset
-}
-
-func snapshot(t testing.TB, fset *FileSet) *Snapshot {
-	t.Helper()
-	snap, err := fset.Snapshot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	return snap
-}
-
-// The following commented tests were used to generate jsons files to stdout for
-// future tests and are kept here for reference (reuse).
-
-// TestGenerateIgnoredFilesDB generates a database with files with invalid flags,
-// local and remote, in the format used in 0.14.48.
-// func TestGenerateIgnoredFilesDB(t *testing.T) {
-// 	db := OpenMemory()
-// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
-// 	fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-// 		{ // invalid (ignored) file
-// 			Name:    "foo",
-// 			Type:    protocol.FileInfoTypeFile,
-// 			Invalid: true,
-// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1000}}},
-// 		},
-// 		{ // regular file
-// 			Name:    "bar",
-// 			Type:    protocol.FileInfoTypeFile,
-// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}},
-// 		},
-// 	})
-// 	fs.Update(protocol.DeviceID{42}, []protocol.FileInfo{
-// 		{ // invalid file
-// 			Name:    "baz",
-// 			Type:    protocol.FileInfoTypeFile,
-// 			Invalid: true,
-// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1000}}},
-// 		},
-// 		{ // regular file
-// 			Name:    "quux",
-// 			Type:    protocol.FileInfoTypeFile,
-// 			Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1002}}},
-// 		},
-// 	})
-// 	writeJSONS(os.Stdout, db.DB)
-// }
-
-// TestGenerateUpdate0to3DB generates a database with files with invalid flags, prefixed
-// by a slash and other files to test database migration from version 0 to 3, in the
-// format used in 0.14.45.
-// func TestGenerateUpdate0to3DB(t *testing.T) {
-// 	db := OpenMemory()
-// 	fs := newFileSet(t, update0to3Folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
-// 	for devID, files := range haveUpdate0to3 {
-// 		fs.Update(devID, files)
-// 	}
-// 	writeJSONS(os.Stdout, db.DB)
-// }
-
-// func TestGenerateUpdateTo10(t *testing.T) {
-// 	db := newLowlevelMemory(t)
-// 	defer db.Close()
-
-// 	if err := UpdateSchema(db); err != nil {
-// 		t.Fatal(err)
-// 	}
-
-// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
-
-// 	files := []protocol.FileInfo{
-// 		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Deleted: true, Sequence: 1},
-// 		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2), Sequence: 2},
-// 		{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Deleted: true, Sequence: 3},
-// 	}
-// 	fs.Update(protocol.LocalDeviceID, files)
-// 	files[1].Version = files[1].Version.Update(remoteDevice0.Short())
-// 	files[1].Deleted = true
-// 	files[2].Version = files[2].Version.Update(remoteDevice0.Short())
-// 	files[2].Blocks = genBlocks(1)
-// 	files[2].Deleted = false
-// 	fs.Update(remoteDevice0, files)
-
-// 	fd, err := os.Create("./testdata/v1.4.0-updateTo10.json")
-// 	if err != nil {
-// 		panic(err)
-// 	}
-// 	defer fd.Close()
-// 	writeJSONS(fd, db)
-// }
-
-func TestFileInfoBatchError(t *testing.T) {
-	// Verify behaviour of the flush function returning an error.
-
-	var errReturn error
-	var called int
-	b := NewFileInfoBatch(func([]protocol.FileInfo) error {
-		called += 1
-		return errReturn
-	})
-
-	// Flush should work when the flush function error is nil
-	b.Append(protocol.FileInfo{Name: "test"})
-	if err := b.Flush(); err != nil {
-		t.Fatalf("expected nil, got %v", err)
-	}
-	if called != 1 {
-		t.Fatalf("expected 1, got %d", called)
-	}
-
-	// Flush should fail with an error retur
-	errReturn = errors.New("problem")
-	b.Append(protocol.FileInfo{Name: "test"})
-	if err := b.Flush(); err != errReturn {
-		t.Fatalf("expected %v, got %v", errReturn, err)
-	}
-	if called != 2 {
-		t.Fatalf("expected 2, got %d", called)
-	}
-
-	// Flush function should not be called again when it's already errored,
-	// same error should be returned by Flush()
-	if err := b.Flush(); err != errReturn {
-		t.Fatalf("expected %v, got %v", errReturn, err)
-	}
-	if called != 2 {
-		t.Fatalf("expected 2, got %d", called)
-	}
-
-	// Reset should clear the error (and the file list)
-	errReturn = nil
-	b.Reset()
-	b.Append(protocol.FileInfo{Name: "test"})
-	if err := b.Flush(); err != nil {
-		t.Fatalf("expected nil, got %v", err)
-	}
-	if called != 3 {
-		t.Fatalf("expected 3, got %d", called)
-	}
-}
diff --git a/lib/fs/filesystem_test.go b/lib/fs/filesystem_test.go
index b0b239a5b93..08e45ff50c1 100644
--- a/lib/fs/filesystem_test.go
+++ b/lib/fs/filesystem_test.go
@@ -189,7 +189,7 @@ func TestRepro9677MissingMtimeFS(t *testing.T) {
 	testTime := time.Unix(1723491493, 123456789)
 
 	// Create a file with an mtime FS entry
-	firstFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
+	firstFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB, ""))
 
 	// Create a file, set its mtime and check that we get the expected mtime when stat-ing.
 	file, err := firstFS.Create(name)
@@ -231,6 +231,6 @@ func TestRepro9677MissingMtimeFS(t *testing.T) {
 	// be without mtime, even if requested:
 	NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{})
 
-	newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
+	newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB, ""))
 	checkMtime(newFS)
 }
diff --git a/lib/fs/mtimefs_test.go b/lib/fs/mtimefs_test.go
index 22f02b3838d..cce2acef669 100644
--- a/lib/fs/mtimefs_test.go
+++ b/lib/fs/mtimefs_test.go
@@ -226,20 +226,20 @@ func TestMtimeFSInsensitive(t *testing.T) {
 
 // The mapStore is a simple database
 
-type mapStore map[string][]byte
+type mapStore map[string][2]time.Time
 
-func (s mapStore) PutBytes(key string, data []byte) error {
-	s[key] = data
+func (s mapStore) PutMtime(_, name string, real, virtual time.Time) error {
+	s[name] = [2]time.Time{real, virtual}
 	return nil
 }
 
-func (s mapStore) Bytes(key string) (data []byte, ok bool, err error) {
-	data, ok = s[key]
-	return
+func (s mapStore) GetMtime(_, name string) (real, virtual time.Time) {
+	v := s[name]
+	return v[0], v[1]
 }
 
-func (s mapStore) Delete(key string) error {
-	delete(s, key)
+func (s mapStore) DeleteMtime(_, name string) error {
+	delete(s, name)
 	return nil
 }
 
@@ -260,7 +260,7 @@ func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
 }
 
 func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
-	fs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, options...))
+	fs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, "", options...))
 	wfs, _ := unwrapFilesystem(fs, filesystemWrapperTypeWalk)
 	mfs, _ := unwrapFilesystem(fs, filesystemWrapperTypeMtime)
 	return mfs.(*mtimeFS), wfs.(*walkFilesystem)
diff --git a/lib/model/fakeconns_test.go b/lib/model/fakeconns_test.go
index 8311982760d..524aa62d57d 100644
--- a/lib/model/fakeconns_test.go
+++ b/lib/model/fakeconns_test.go
@@ -12,6 +12,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/timeutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 	protocolmocks "github.com/syncthing/syncthing/lib/protocol/mocks"
 	"github.com/syncthing/syncthing/lib/rand"
@@ -81,7 +82,7 @@ func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol
 		Name:       name,
 		Type:       ftype,
 		Version:    version,
-		Sequence:   time.Now().UnixNano(),
+		Sequence:   timeutil.StrictlyMonotonicNanos(),
 		LocalFlags: localFlags,
 	}
 	switch ftype {
@@ -108,15 +109,6 @@ func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol
 	f.fileData[name] = data
 }
 
-func (f *fakeConnection) addFileWithLocalFlags(name string, ftype protocol.FileInfoType, localFlags uint32) {
-	f.mut.Lock()
-	defer f.mut.Unlock()
-
-	var version protocol.Vector
-	version = version.Update(f.id.Short())
-	f.addFileLocked(name, 0, ftype, nil, version, localFlags)
-}
-
 func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
 	f.mut.Lock()
 	defer f.mut.Unlock()
@@ -148,7 +140,7 @@ func (f *fakeConnection) deleteFile(name string) {
 			fi.Deleted = true
 			fi.ModifiedS = time.Now().Unix()
 			fi.Version = fi.Version.Update(f.id.Short())
-			fi.Sequence = time.Now().UnixNano()
+			fi.Sequence = timeutil.StrictlyMonotonicNanos()
 			fi.Blocks = nil
 
 			f.files = append(append(f.files[:i], f.files[i+1:]...), fi)
diff --git a/lib/model/fileinfobatch_test.go b/lib/model/fileinfobatch_test.go
new file mode 100644
index 00000000000..2ed7d9752c8
--- /dev/null
+++ b/lib/model/fileinfobatch_test.go
@@ -0,0 +1,64 @@
+// Copyright (C) 2018 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package model
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func TestFileInfoBatchError(t *testing.T) {
+	// Verify behaviour of the flush function returning an error.
+
+	var errReturn error
+	var called int
+	b := NewFileInfoBatch(func([]protocol.FileInfo) error {
+		called += 1
+		return errReturn
+	})
+
+	// Flush should work when the flush function error is nil
+	b.Append(protocol.FileInfo{Name: "test"})
+	if err := b.Flush(); err != nil {
+		t.Fatalf("expected nil, got %v", err)
+	}
+	if called != 1 {
+		t.Fatalf("expected 1, got %d", called)
+	}
+
+	// Flush should fail with an error retur
+	errReturn = errors.New("problem")
+	b.Append(protocol.FileInfo{Name: "test"})
+	if err := b.Flush(); err != errReturn {
+		t.Fatalf("expected %v, got %v", errReturn, err)
+	}
+	if called != 2 {
+		t.Fatalf("expected 2, got %d", called)
+	}
+
+	// Flush function should not be called again when it's already errored,
+	// same error should be returned by Flush()
+	if err := b.Flush(); err != errReturn {
+		t.Fatalf("expected %v, got %v", errReturn, err)
+	}
+	if called != 2 {
+		t.Fatalf("expected 2, got %d", called)
+	}
+
+	// Reset should clear the error (and the file list)
+	errReturn = nil
+	b.Reset()
+	b.Append(protocol.FileInfo{Name: "test"})
+	if err := b.Flush(); err != nil {
+		t.Fatalf("expected nil, got %v", err)
+	}
+	if called != 3 {
+		t.Fatalf("expected 3, got %d", called)
+	}
+}
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
index 729b68ebb14..3abf6bf08a0 100644
--- a/lib/model/folder_recvonly_test.go
+++ b/lib/model/folder_recvonly_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
@@ -28,7 +29,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -46,9 +47,11 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 	// Send and index update for the known stuff
 
 	must(t, m.Index(conn, &protocol.Index{Folder: "ro", Files: knownFiles}))
-	f.updateLocalsFromScanning(knownFiles)
+	if err := f.updateLocalsFromScanning(knownFiles); err != nil {
+		t.Fatal(err)
+	}
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
@@ -59,15 +62,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 
 	// We should now have two files and two directories, with global state unchanged.
 
-	size = globalSize(t, m, "ro")
+	size = mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
-		t.Fatalf("Global: expected 2 files and 2 directories: %+v", size)
+		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 2 || size.Directories != 2 {
 		t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories == 0 {
 		t.Fatalf("ROChanged: expected something: %+v", size)
 	}
@@ -92,11 +95,11 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
 
 	// We should now have one file and directory again.
 
-	size = globalSize(t, m, "ro")
+	size = mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 files and 1 directories: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 files and 1 directories: %+v", size)
 	}
@@ -110,7 +113,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -131,19 +134,19 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	// Everything should be in sync.
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("ROChanged: expected nothing: %+v", size)
 	}
@@ -159,20 +162,20 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	// We now have a newer file than the rest of the cluster. Global state should reflect this.
 
-	size = globalSize(t, m, "ro")
+	size = mustV(m.GlobalSize("ro"))
 	const sizeOfDir = 128
 	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
 		t.Fatalf("Global: expected no change due to the new file: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
 		t.Fatalf("Local: expected the new file to be reflected: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories == 0 {
 		t.Fatalf("ROChanged: expected something: %+v", size)
 	}
@@ -181,15 +184,15 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
 
 	m.Revert("ro")
 
-	size = globalSize(t, m, "ro")
+	size = mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
 		t.Fatalf("Global: expected the global size to revert: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
 		t.Fatalf("Local: expected the local size to remain: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Bytes != int64(len(oldData)) {
 		t.Fatalf("Local: expected to need the old file data: %+v", size)
 	}
@@ -200,7 +203,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -221,19 +224,19 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	// Everything should be in sync.
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("ROChanged: expected nothing: %+v", size)
 	}
@@ -246,7 +249,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files != 2 {
 		t.Fatalf("Receive only: expected 2 files: %+v", size)
 	}
@@ -259,7 +262,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories+size.Deleted != 0 {
 		t.Fatalf("Receive only: expected all zero: %+v", size)
 	}
@@ -270,7 +273,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -291,19 +294,19 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
 
 	// Everything should be in sync.
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("ROChanged: expected nothing: %+v", size)
 	}
@@ -314,17 +317,17 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Deleted != 1 {
 		t.Fatalf("Receive only: expected 1 deleted: %+v", size)
 	}
 
 	// Drop the remote
 
-	f.fset.Drop(device1)
+	f.db.DropAllFiles("ro", device1)
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Deleted != 0 {
 		t.Fatalf("Receive only: expected no deleted: %+v", size)
 	}
@@ -335,7 +338,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -356,19 +359,19 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	// Everything should be in sync.
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("ROChanged: expected nothing: %+v", size)
 	}
@@ -382,7 +385,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files != 2 {
 		t.Fatalf("Receive only: expected 2 files: %+v", size)
 	}
@@ -390,17 +393,17 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 	// Do the same changes on the remote
 
 	files := make([]protocol.FileInfo, 0, 2)
-	snap := fsetSnapshot(t, f.fset)
-	snap.WithHave(protocol.LocalDeviceID, func(f protocol.FileInfo) bool {
+	for f, err := range itererr.Zip(f.db.AllLocalFiles("ro", protocol.LocalDeviceID)) {
+		if err != nil {
+			t.Fatal(err)
+		}
 		if f.Name != file && f.Name != knownFile {
-			return true
+			continue
 		}
 		f.LocalFlags = 0
 		f.Version = protocol.Vector{}.Update(device1.Short())
 		files = append(files, f)
-		return true
-	})
-	snap.Release()
+	}
 	must(t, m.IndexUpdate(conn, &protocol.IndexUpdate{Folder: "ro", Files: files}))
 
 	// Ensure the pull to resolve conflicts (content identical) happened
@@ -409,7 +412,10 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
 		return nil
 	}))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size, err := m.ReceiveOnlySize("ro")
+	if err != nil {
+		t.Fatal(err)
+	}
 	if size.Files+size.Directories+size.Deleted != 0 {
 		t.Fatalf("Receive only: expected all zero: %+v", size)
 	}
@@ -424,7 +430,7 @@ func TestRecvOnlyRevertOwnID(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -484,7 +490,7 @@ func TestRecvOnlyLocalChangeDoesNotCauseConflict(t *testing.T) {
 
 	m, f, wcfgCancel := setupROFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	defer cleanupModel(m)
 	conn := addFakeConn(m, device1, f.ID)
 
@@ -505,19 +511,19 @@ func TestRecvOnlyLocalChangeDoesNotCauseConflict(t *testing.T) {
 
 	// Everything should be in sync.
 
-	size := globalSize(t, m, "ro")
+	size := mustV(m.GlobalSize("ro"))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
 	}
-	size = localSize(t, m, "ro")
+	size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
 	if size.Files != 1 || size.Directories != 1 {
 		t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
 	}
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files+size.Directories > 0 {
 		t.Fatalf("ROChanged: expected nothing: %+v", size)
 	}
@@ -528,7 +534,7 @@ func TestRecvOnlyLocalChangeDoesNotCauseConflict(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = receiveOnlyChangedSize(t, m, "ro")
+	size = mustV(m.ReceiveOnlySize("ro"))
 	if size.Files != 1 {
 		t.Fatalf("Receive only: expected 1 file: %+v", size)
 	}
@@ -541,7 +547,7 @@ func TestRecvOnlyLocalChangeDoesNotCauseConflict(t *testing.T) {
 
 	must(t, m.ScanFolder("ro"))
 
-	size = needSizeLocal(t, m, "ro")
+	size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
 	if size.Files != 0 {
 		t.Fatalf("Need: expected nothing: %+v", size)
 	}
@@ -577,7 +583,7 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
 			ModifiedS:   fi.ModTime().Unix(),
 			ModifiedNs:  int32(fi.ModTime().Nanosecond()),
 			Version:     protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 42}}},
-			Sequence:    42,
+			Sequence:    43,
 			Blocks:      blocks,
 		},
 	}
diff --git a/lib/model/folder_sendrecv_test.go b/lib/model/folder_sendrecv_test.go
index ee013467bf7..bd13762624c 100644
--- a/lib/model/folder_sendrecv_test.go
+++ b/lib/model/folder_sendrecv_test.go
@@ -19,6 +19,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/events"
@@ -149,7 +150,7 @@ func TestHandleFile(t *testing.T) {
 
 	copyChan := make(chan copyBlocksState, 1)
 
-	f.handleFile(requiredFile, fsetSnapshot(t, f.fset), copyChan)
+	f.handleFile(requiredFile, copyChan)
 
 	// Receive the results
 	toCopy := <-copyChan
@@ -189,13 +190,13 @@ func TestHandleFileWithTemp(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
 	defer wcfgCancel()
 
-	if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
+	if _, err := prepareTmpFile(f.Filesystem()); err != nil {
 		t.Fatal(err)
 	}
 
 	copyChan := make(chan copyBlocksState, 1)
 
-	f.handleFile(requiredFile, fsetSnapshot(t, f.fset), copyChan)
+	f.handleFile(requiredFile, copyChan)
 
 	// Receive the results
 	toCopy := <-copyChan
@@ -239,7 +240,7 @@ func TestCopierFinder(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
 	defer wcfgCancel()
 
-	if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
+	if _, err := prepareTmpFile(f.Filesystem()); err != nil {
 		t.Fatal(err)
 	}
 
@@ -251,7 +252,7 @@ func TestCopierFinder(t *testing.T) {
 	go f.copierRoutine(copyChan, pullChan, finisherChan)
 	defer close(copyChan)
 
-	f.handleFile(requiredFile, fsetSnapshot(t, f.fset), copyChan)
+	f.handleFile(requiredFile, copyChan)
 
 	timeout := time.After(10 * time.Second)
 	pulls := make([]pullBlockState, 4)
@@ -272,8 +273,9 @@ func TestCopierFinder(t *testing.T) {
 	defer cleanupSharedPullerState(finish)
 
 	select {
-	case <-pullChan:
-		t.Fatal("Pull channel has data to be read")
+	case v := <-pullChan:
+		t.Logf("%+v\n", v)
+		t.Fatal("Pull channel had data to be read")
 	case <-finisherChan:
 		t.Fatal("Finisher channel has data to be read")
 	default:
@@ -299,7 +301,7 @@ func TestCopierFinder(t *testing.T) {
 	}
 
 	// Verify that the fetched blocks have actually been written to the temp file
-	blks, err := scanner.HashFile(context.TODO(), f.ID, f.Filesystem(nil), tempFile, protocol.MinBlockSize, nil)
+	blks, err := scanner.HashFile(context.TODO(), f.ID, f.Filesystem(), tempFile, protocol.MinBlockSize, nil)
 	if err != nil {
 		t.Log(err)
 	}
@@ -313,10 +315,6 @@ func TestCopierFinder(t *testing.T) {
 
 // Test that updating a file removes its old blocks from the blockmap
 func TestCopierCleanup(t *testing.T) {
-	iterFn := func(folder, file string, index int32) bool {
-		return true
-	}
-
 	// Create a file
 	file := setupFile("test", []int{0})
 	file.Size = 1
@@ -328,11 +326,11 @@ func TestCopierCleanup(t *testing.T) {
 	// Update index (removing old blocks)
 	f.updateLocalsFromScanning([]protocol.FileInfo{file})
 
-	if m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
+	if vals, err := itererr.Collect(m.sdb.AllLocalBlocksWithHash(blocks[0].Hash)); err != nil || len(vals) > 0 {
 		t.Error("Unexpected block found")
 	}
 
-	if !m.finder.Iterate(folders, blocks[1].Hash, iterFn) {
+	if vals, err := itererr.Collect(m.sdb.AllLocalBlocksWithHash(blocks[1].Hash)); err != nil || len(vals) == 0 {
 		t.Error("Expected block not found")
 	}
 
@@ -341,11 +339,11 @@ func TestCopierCleanup(t *testing.T) {
 	// Update index (removing old blocks)
 	f.updateLocalsFromScanning([]protocol.FileInfo{file})
 
-	if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
+	if vals, err := itererr.Collect(m.sdb.AllLocalBlocksWithHash(blocks[0].Hash)); err != nil || len(vals) == 0 {
 		t.Error("Unexpected block found")
 	}
 
-	if m.finder.Iterate(folders, blocks[1].Hash, iterFn) {
+	if vals, err := itererr.Collect(m.sdb.AllLocalBlocksWithHash(blocks[1].Hash)); err != nil || len(vals) > 0 {
 		t.Error("Expected block not found")
 	}
 }
@@ -371,10 +369,9 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
 	finisherBufferChan := make(chan *sharedPullerState, 1)
 	finisherChan := make(chan *sharedPullerState)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
-	snap := fsetSnapshot(t, f.fset)
 
 	copyChan, copyWg := startCopier(f, pullChan, finisherBufferChan)
-	go f.finisherRoutine(snap, finisherChan, dbUpdateChan, make(chan string))
+	go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
 
 	defer func() {
 		close(copyChan)
@@ -384,7 +381,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
 		close(finisherChan)
 	}()
 
-	f.handleFile(file, snap, copyChan)
+	f.handleFile(file, copyChan)
 
 	// Receive a block at puller, to indicate that at least a single copier
 	// loop has been performed.
@@ -471,16 +468,15 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 	finisherBufferChan := make(chan *sharedPullerState)
 	finisherChan := make(chan *sharedPullerState)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
-	snap := fsetSnapshot(t, f.fset)
 
 	copyChan, copyWg := startCopier(f, pullChan, finisherBufferChan)
 	pullWg := sync.NewWaitGroup()
 	pullWg.Add(1)
 	go func() {
-		f.pullerRoutine(snap, pullChan, finisherBufferChan)
+		f.pullerRoutine(pullChan, finisherBufferChan)
 		pullWg.Done()
 	}()
-	go f.finisherRoutine(snap, finisherChan, dbUpdateChan, make(chan string))
+	go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
 	defer func() {
 		// Unblock copier and puller
 		go func() {
@@ -495,7 +491,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 		close(finisherChan)
 	}()
 
-	f.handleFile(file, snap, copyChan)
+	f.handleFile(file, copyChan)
 
 	// Receive at finisher, we should error out as puller has nowhere to pull
 	// from.
@@ -558,7 +554,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 func TestIssue3164(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	ignDir := filepath.Join("issue3164", "oktodelete")
 	subDir := filepath.Join(ignDir, "foobar")
@@ -577,7 +573,7 @@ func TestIssue3164(t *testing.T) {
 
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 
-	f.deleteDir(file, fsetSnapshot(t, f.fset), dbUpdateChan, make(chan string))
+	f.deleteDir(file, dbUpdateChan, make(chan string))
 
 	if _, err := ffs.Stat("issue3164"); !fs.IsNotExist(err) {
 		t.Fatal(err)
@@ -648,7 +644,7 @@ func TestDiffEmpty(t *testing.T) {
 func TestDeleteIgnorePerms(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 	f.IgnorePerms = true
 
 	name := "deleteIgnorePerms"
@@ -692,9 +688,6 @@ func TestCopyOwner(t *testing.T) {
 	f.folder.FolderConfiguration = newFolderConfiguration(m.cfg, f.ID, f.Label, config.FilesystemTypeFake, "/TestCopyOwner")
 	f.folder.FolderConfiguration.CopyOwnershipFromParent = true
 
-	f.fset = newFileSet(t, f.ID, m.db)
-	f.mtimefs = f.Filesystem(f.fset)
-
 	// Create a parent dir with a certain owner/group.
 
 	f.mtimefs.Mkdir("foo", 0o755)
@@ -712,7 +705,7 @@ func TestCopyOwner(t *testing.T) {
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 	scanChan := make(chan string)
 	defer close(dbUpdateChan)
-	f.handleDir(dir, fsetSnapshot(t, f.fset), dbUpdateChan, scanChan)
+	f.handleDir(dir, dbUpdateChan, scanChan)
 	select {
 	case <-dbUpdateChan: // empty the channel for later
 	case toScan := <-scanChan:
@@ -742,17 +735,16 @@ func TestCopyOwner(t *testing.T) {
 	// but it's the way data is passed around. When the database update
 	// comes the finisher is done.
 
-	snap := fsetSnapshot(t, f.fset)
 	finisherChan := make(chan *sharedPullerState)
 	copierChan, copyWg := startCopier(f, nil, finisherChan)
-	go f.finisherRoutine(snap, finisherChan, dbUpdateChan, nil)
+	go f.finisherRoutine(finisherChan, dbUpdateChan, nil)
 	defer func() {
 		close(copierChan)
 		copyWg.Wait()
 		close(finisherChan)
 	}()
 
-	f.handleFile(file, snap, copierChan)
+	f.handleFile(file, copierChan)
 	<-dbUpdateChan
 
 	info, err = f.mtimefs.Lstat("foo/bar/baz")
@@ -771,7 +763,7 @@ func TestCopyOwner(t *testing.T) {
 		SymlinkTarget: []byte("over the rainbow"),
 	}
 
-	f.handleSymlink(symlink, snap, dbUpdateChan, scanChan)
+	f.handleSymlink(symlink, dbUpdateChan, scanChan)
 	select {
 	case <-dbUpdateChan:
 	case toScan := <-scanChan:
@@ -792,7 +784,7 @@ func TestCopyOwner(t *testing.T) {
 func TestSRConflictReplaceFileByDir(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	name := "foo"
 
@@ -810,7 +802,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 	scanChan := make(chan string, 1)
 
-	f.handleDir(file, fsetSnapshot(t, f.fset), dbUpdateChan, scanChan)
+	f.handleDir(file, dbUpdateChan, scanChan)
 
 	if confls := existingConflicts(name, ffs); len(confls) != 1 {
 		t.Fatal("Expected one conflict, got", len(confls))
@@ -824,7 +816,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 func TestSRConflictReplaceFileByLink(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	name := "foo"
 
@@ -843,7 +835,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 	scanChan := make(chan string, 1)
 
-	f.handleSymlink(file, fsetSnapshot(t, f.fset), dbUpdateChan, scanChan)
+	f.handleSymlink(file, dbUpdateChan, scanChan)
 
 	if confls := existingConflicts(name, ffs); len(confls) != 1 {
 		t.Fatal("Expected one conflict, got", len(confls))
@@ -857,7 +849,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 func TestDeleteBehindSymlink(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	link := "link"
 	linkFile := filepath.Join(link, "file")
@@ -873,7 +865,7 @@ func TestDeleteBehindSymlink(t *testing.T) {
 	fi.Version = fi.Version.Update(device1.Short())
 	scanChan := make(chan string, 1)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
-	f.deleteFile(fi, fsetSnapshot(t, f.fset), dbUpdateChan, scanChan)
+	f.deleteFile(fi, dbUpdateChan, scanChan)
 	select {
 	case f := <-scanChan:
 		t.Fatalf("Received %v on scanChan", f)
@@ -903,7 +895,7 @@ func TestPullCtxCancel(t *testing.T) {
 	var cancel context.CancelFunc
 	f.ctx, cancel = context.WithCancel(context.Background())
 
-	go f.pullerRoutine(fsetSnapshot(t, f.fset), pullChan, finisherChan)
+	go f.pullerRoutine(pullChan, finisherChan)
 	defer close(pullChan)
 
 	emptyState := func() pullBlockState {
@@ -938,7 +930,7 @@ func TestPullCtxCancel(t *testing.T) {
 func TestPullDeleteUnscannedDir(t *testing.T) {
 	_, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	dir := "foobar"
 	must(t, ffs.MkdirAll(dir, 0o777))
@@ -949,7 +941,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
 	scanChan := make(chan string, 1)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 
-	f.deleteDir(fi, fsetSnapshot(t, f.fset), dbUpdateChan, scanChan)
+	f.deleteDir(fi, dbUpdateChan, scanChan)
 
 	if _, err := ffs.Stat(dir); fs.IsNotExist(err) {
 		t.Error("directory has been deleted")
@@ -967,7 +959,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
 func TestPullCaseOnlyPerformFinish(t *testing.T) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	name := "foo"
 	contents := []byte("contents")
@@ -976,16 +968,17 @@ func TestPullCaseOnlyPerformFinish(t *testing.T) {
 
 	var cur protocol.FileInfo
 	hasCur := false
-	snap := dbSnapshot(t, m, f.ID)
-	defer snap.Release()
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
+	it, errFn := m.LocalFiles(f.ID, protocol.LocalDeviceID)
+	for i := range it {
 		if hasCur {
 			t.Fatal("got more than one file")
 		}
 		cur = i
 		hasCur = true
-		return true
-	})
+	}
+	if err := errFn(); err != nil {
+		t.Fatal(err)
+	}
 	if !hasCur {
 		t.Fatal("file is missing")
 	}
@@ -999,7 +992,7 @@ func TestPullCaseOnlyPerformFinish(t *testing.T) {
 	scanChan := make(chan string, 1)
 	dbUpdateChan := make(chan dbUpdateJob, 1)
 
-	err := f.performFinish(remote, cur, hasCur, temp, snap, dbUpdateChan, scanChan)
+	err := f.performFinish(remote, cur, hasCur, temp, dbUpdateChan, scanChan)
 
 	select {
 	case <-dbUpdateChan: // boring case sensitive filesystem
@@ -1029,7 +1022,7 @@ func TestPullCaseOnlySymlink(t *testing.T) {
 func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 	m, f, wcfgCancel := setupSendReceiveFolder(t)
 	defer wcfgCancel()
-	ffs := f.Filesystem(nil)
+	ffs := f.Filesystem()
 
 	name := "foo"
 	if dir {
@@ -1041,16 +1034,17 @@ func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 	must(t, f.scanSubdirs(nil))
 	var cur protocol.FileInfo
 	hasCur := false
-	snap := dbSnapshot(t, m, f.ID)
-	defer snap.Release()
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
+	it, errFn := m.LocalFiles(f.ID, protocol.LocalDeviceID)
+	for i := range it {
 		if hasCur {
 			t.Fatal("got more than one file")
 		}
 		cur = i
 		hasCur = true
-		return true
-	})
+	}
+	if err := errFn(); err != nil {
+		t.Fatal(err)
+	}
 	if !hasCur {
 		t.Fatal("file is missing")
 	}
@@ -1063,9 +1057,9 @@ func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 	remote.Name = strings.ToUpper(cur.Name)
 
 	if dir {
-		f.handleDir(remote, snap, dbUpdateChan, scanChan)
+		f.handleDir(remote, dbUpdateChan, scanChan)
 	} else {
-		f.handleSymlink(remote, snap, dbUpdateChan, scanChan)
+		f.handleSymlink(remote, dbUpdateChan, scanChan)
 	}
 
 	select {
@@ -1100,7 +1094,7 @@ func TestPullTempFileCaseConflict(t *testing.T) {
 		fd.Close()
 	}
 
-	f.handleFile(file, fsetSnapshot(t, f.fset), copyChan)
+	f.handleFile(file, copyChan)
 
 	cs := <-copyChan
 	if _, err := cs.tempFile(); err != nil {
@@ -1142,9 +1136,7 @@ func TestPullCaseOnlyRename(t *testing.T) {
 
 	dbUpdateChan := make(chan dbUpdateJob, 2)
 	scanChan := make(chan string, 2)
-	snap := fsetSnapshot(t, f.fset)
-	defer snap.Release()
-	if err := f.renameFile(cur, deleted, confl, snap, dbUpdateChan, scanChan); err != nil {
+	if err := f.renameFile(cur, deleted, confl, dbUpdateChan, scanChan); err != nil {
 		t.Error(err)
 	}
 }
@@ -1219,9 +1211,7 @@ func TestPullDeleteCaseConflict(t *testing.T) {
 		t.Error("Missing db update for file")
 	}
 
-	snap := fsetSnapshot(t, f.fset)
-	defer snap.Release()
-	f.deleteDir(fi, snap, dbUpdateChan, scanChan)
+	f.deleteDir(fi, dbUpdateChan, scanChan)
 	select {
 	case <-dbUpdateChan:
 	default:
@@ -1249,7 +1239,7 @@ func TestPullDeleteIgnoreChildDir(t *testing.T) {
 
 	scanChan := make(chan string, 2)
 
-	err := f.deleteDirOnDisk(parent, fsetSnapshot(t, f.fset), scanChan)
+	err := f.deleteDirOnDisk(parent, scanChan)
 	if err == nil {
 		t.Error("no error")
 	}
diff --git a/lib/model/indexhandler_test.go b/lib/model/indexhandler_test.go
index e8602e235bc..4e3981f63ec 100644
--- a/lib/model/indexhandler_test.go
+++ b/lib/model/indexhandler_test.go
@@ -13,7 +13,7 @@ import (
 	"sync"
 	"testing"
 
-	"github.com/syncthing/syncthing/lib/db"
+	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/model/mocks"
 	"github.com/syncthing/syncthing/lib/protocol"
 	protomock "github.com/syncthing/syncthing/lib/protocol/mocks"
@@ -63,7 +63,7 @@ func TestIndexhandlerConcurrency(t *testing.T) {
 		return nil
 	})
 
-	b1 := db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
+	b1 := model.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
 		return c1.IndexUpdate(ctx, &protocol.IndexUpdate{Folder: "foo", Files: fs})
 	})
 	sentEntries := 0
diff --git a/lib/model/model_test.go b/lib/model/model_test.go
index 0c62742485f..a1cd98f15cc 100644
--- a/lib/model/model_test.go
+++ b/lib/model/model_test.go
@@ -13,6 +13,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"iter"
 	mrand "math/rand"
 	"os"
 	"path/filepath"
@@ -24,10 +25,10 @@ import (
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/itererr"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -77,7 +78,7 @@ func addFolderDevicesToClusterConfig(cc *protocol.ClusterConfig, remote protocol
 
 func TestRequest(t *testing.T) {
 	wrapper, fcfg, cancel := newDefaultCfgWrapper()
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	defer cancel()
 	m := setupModel(t, wrapper)
 	defer cleanupModel(m)
@@ -165,7 +166,7 @@ func BenchmarkIndex_100(b *testing.B) {
 func benchmarkIndex(b *testing.B, nfiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	files := genFiles(nfiles)
 	must(b, m.Index(device1Conn, &protocol.Index{Folder: fcfg.ID, Files: files}))
@@ -192,7 +193,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
 func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	files := genFiles(nfiles)
 	ufiles := genFiles(nufiles)
@@ -235,7 +236,7 @@ func BenchmarkRequestOut(b *testing.B) {
 func BenchmarkRequestInSingleFile(b *testing.B) {
 	w, cancel := newConfigWrapper(defaultCfg)
 	defer cancel()
-	ffs := w.FolderList()[0].Filesystem(nil)
+	ffs := w.FolderList()[0].Filesystem()
 	m := setupModel(b, w)
 	defer cleanupModel(m)
 
@@ -1195,7 +1196,7 @@ func TestAutoAcceptPrefersLabel(t *testing.T) {
 func TestAutoAcceptFallsBackToID(t *testing.T) {
 	// Prefers label, falls back to ID.
 	m, cancel := newState(t, defaultAutoAcceptCfg)
-	ffs := defaultFolderConfig.Filesystem(nil)
+	ffs := defaultFolderConfig.Filesystem()
 	id := srand.String(8)
 	label := srand.String(8)
 	if err := ffs.MkdirAll(label, 0o777); err != nil {
@@ -1488,7 +1489,7 @@ func changeIgnores(t *testing.T, m *testModel, expected []string) {
 func TestIgnores(t *testing.T) {
 	w, cancel := newConfigWrapper(defaultCfg)
 	defer cancel()
-	ffs := w.FolderList()[0].Filesystem(nil)
+	ffs := w.FolderList()[0].Filesystem()
 	m := setupModel(t, w)
 	defer cleanupModel(m)
 
@@ -1523,7 +1524,7 @@ func TestIgnores(t *testing.T) {
 		ID: "fresh", Path: "XXX",
 		FilesystemType: config.FilesystemTypeFake,
 	}
-	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
+	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
 	m.mut.Lock()
 	m.folderCfgs[fcfg.ID] = fcfg
 	m.folderIgnores[fcfg.ID] = ignores
@@ -1555,7 +1556,7 @@ func TestIgnores(t *testing.T) {
 func TestEmptyIgnores(t *testing.T) {
 	w, cancel := newConfigWrapper(defaultCfg)
 	defer cancel()
-	ffs := w.FolderList()[0].Filesystem(nil)
+	ffs := w.FolderList()[0].Filesystem()
 	m := setupModel(t, w)
 	defer cleanupModel(m)
 
@@ -1628,12 +1629,11 @@ func TestROScanRecovery(t *testing.T) {
 	defer cancel()
 	m := newModel(t, cfg, myID, nil)
 
-	set := newFileSet(t, "default", m.db)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+	m.sdb.Update("default", protocol.LocalDeviceID, []protocol.FileInfo{
 		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
 	})
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	// Remove marker to generate an error
 	ffs.Remove(fcfg.MarkerName)
@@ -1675,12 +1675,11 @@ func TestRWScanRecovery(t *testing.T) {
 	defer cancel()
 	m := newModel(t, cfg, myID, nil)
 
-	set := newFileSet(t, "default", m.db)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+	m.sdb.Update("default", protocol.LocalDeviceID, []protocol.FileInfo{
 		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
 	})
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	// Generate error
 	if err := ffs.Remove(config.DefaultMarkerName); err != nil {
@@ -1706,8 +1705,9 @@ func TestRWScanRecovery(t *testing.T) {
 func TestGlobalDirectoryTree(t *testing.T) {
 	m, conn, fcfg, wCancel := setupModelWithConnection(t)
 	defer wCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
+	var seq int64
 	b := func(isfile bool, path ...string) protocol.FileInfo {
 		typ := protocol.FileInfoTypeDirectory
 		var blocks []protocol.BlockInfo
@@ -1716,12 +1716,14 @@ func TestGlobalDirectoryTree(t *testing.T) {
 			typ = protocol.FileInfoTypeFile
 			blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
 		}
+		seq++
 		return protocol.FileInfo{
 			Name:      filepath.Join(path...),
 			Type:      typ,
 			ModifiedS: 0x666,
 			Blocks:    blocks,
 			Size:      0xa,
+			Sequence:  seq,
 		}
 	}
 	f := func(name string) *TreeEntry {
@@ -1813,13 +1815,13 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	result, _ := m.GlobalDirectoryTree("default", "", -1, false)
 
 	if mm(result) != mm(expectedResult) {
-		t.Errorf("Does not match:\n%s\n============\n%s", mm(result), mm(expectedResult))
+		t.Fatalf("Does not match:\n%s\n============\n%s", mm(result), mm(expectedResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "another", -1, false)
 
 	if mm(result) != mm(findByName(expectedResult, "another").Children) {
-		t.Errorf("Does not match:\n%s\n============\n%s", mm(result), mm(findByName(expectedResult, "another").Children))
+		t.Fatalf("Does not match:\n%s\n============\n%s", mm(result), mm(findByName(expectedResult, "another").Children))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "", 0, false)
@@ -1831,7 +1833,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n============\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n============\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "", 1, false)
@@ -1852,7 +1854,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "", -1, true)
@@ -1882,7 +1884,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "", 1, true)
@@ -1901,7 +1903,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "another", 0, false)
@@ -1911,7 +1913,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "some/directory", 0, false)
@@ -1920,7 +1922,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "some/directory", 1, false)
@@ -1931,7 +1933,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "some/directory", 2, false)
@@ -1944,7 +1946,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	result, _ = m.GlobalDirectoryTree("default", "another", -1, true)
@@ -1957,7 +1959,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 
 	// No prefix matching!
@@ -1965,7 +1967,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 	currentResult = []*TreeEntry{}
 
 	if mm(result) != mm(currentResult) {
-		t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
+		t.Fatalf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
 	}
 }
 
@@ -2010,7 +2012,7 @@ func BenchmarkTree_100_10(b *testing.B) {
 func benchmarkTree(b *testing.B, n1, n2 int) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	m.ScanFolder(fcfg.ID)
 	files := genDeepFiles(n1, n2)
@@ -2027,7 +2029,7 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
 func TestIssue3028(t *testing.T) {
 	w, cancel := newConfigWrapper(defaultCfg)
 	defer cancel()
-	ffs := w.FolderList()[0].Filesystem(nil)
+	ffs := w.FolderList()[0].Filesystem()
 	m := setupModel(t, w)
 	defer cleanupModel(m)
 
@@ -2039,8 +2041,8 @@ func TestIssue3028(t *testing.T) {
 	// Scan, and get a count of how many files are there now
 
 	m.ScanFolderSubdirs("default", []string{"testrm", "testrm2"})
-	locorigfiles := localSize(t, m, "default").Files
-	globorigfiles := globalSize(t, m, "default").Files
+	locorigfiles := mustV(m.LocalSize("default", protocol.LocalDeviceID)).Files
+	globorigfiles := mustV(m.GlobalSize("default")).Files
 
 	// Delete
 
@@ -2051,8 +2053,8 @@ func TestIssue3028(t *testing.T) {
 	// deleted files increases by two
 
 	m.ScanFolderSubdirs("default", []string{"testrm", "testrm2"})
-	loc := localSize(t, m, "default")
-	glob := globalSize(t, m, "default")
+	loc := mustV(m.LocalSize("default", protocol.LocalDeviceID))
+	glob := mustV(m.GlobalSize("default"))
 
 	if loc.Files != locorigfiles-2 {
 		t.Errorf("Incorrect local accounting; got %d current files, expected %d", loc.Files, locorigfiles-2)
@@ -2127,24 +2129,22 @@ func TestIssue4357(t *testing.T) {
 func TestIndexesForUnknownDevicesDropped(t *testing.T) {
 	m := newModel(t, defaultCfgWrapper, myID, nil)
 
-	files := newFileSet(t, "default", m.db)
-	files.Drop(device1)
-	files.Update(device1, genFiles(1))
-	files.Drop(device2)
-	files.Update(device2, genFiles(1))
+	m.sdb.DropAllFiles("default", device1)
+	m.sdb.Update("default", device1, genFiles(1))
+	m.sdb.DropAllFiles("default", device2)
+	m.sdb.Update("default", device2, genFiles(1))
 
-	if len(files.ListDevices()) != 2 {
+	if devs, err := m.sdb.ListDevicesForFolder("default"); err != nil || len(devs) != 2 {
+		t.Log(devs, err)
 		t.Error("expected two devices")
 	}
 
 	m.newFolder(defaultFolderConfig, false)
 	defer cleanupModel(m)
 
-	// Remote sequence is cached, hence need to recreated.
-	files = newFileSet(t, "default", m.db)
-
-	if l := len(files.ListDevices()); l != 1 {
-		t.Errorf("Expected one device got %v", l)
+	if devs, err := m.sdb.ListDevicesForFolder("default"); err != nil || len(devs) != 1 {
+		t.Log(devs, err)
+		t.Error("expected one device")
 	}
 }
 
@@ -2270,7 +2270,7 @@ func TestIssue3829(t *testing.T) {
 func TestIssue4573(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem(nil)
+	testFs := fcfg.Filesystem()
 	defer os.RemoveAll(testFs.URI())
 
 	must(t, testFs.MkdirAll("inaccessible", 0o755))
@@ -2300,7 +2300,7 @@ func TestIssue4573(t *testing.T) {
 func TestInternalScan(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	testFs := fcfg.Filesystem(nil)
+	testFs := fcfg.Filesystem()
 	defer os.RemoveAll(testFs.URI())
 
 	testCases := map[string]func(protocol.FileInfo) bool{
@@ -2372,12 +2372,11 @@ func TestCustomMarkerName(t *testing.T) {
 	})
 	defer cancel()
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	m := newModel(t, cfg, myID, nil)
 
-	set := newFileSet(t, "default", m.db)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+	m.sdb.Update("default", protocol.LocalDeviceID, []protocol.FileInfo{
 		{Name: "dummyfile"},
 	})
 
@@ -2401,7 +2400,7 @@ func TestCustomMarkerName(t *testing.T) {
 func TestRemoveDirWithContent(t *testing.T) {
 	m, conn, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	tfs.MkdirAll("dirwith", 0o755)
@@ -2463,7 +2462,7 @@ func TestIssue4475(t *testing.T) {
 	m, conn, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
 	defer cleanupModel(m)
-	testFs := fcfg.Filesystem(nil)
+	testFs := fcfg.Filesystem()
 
 	// Scenario: Dir is deleted locally and before syncing/index exchange
 	// happens, a file is create in that dir on the remote.
@@ -2525,7 +2524,7 @@ func TestVersionRestore(t *testing.T) {
 	fcfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", config.FilesystemTypeFake, srand.String(32))
 	fcfg.Versioning.Type = "simple"
 	fcfg.FSWatcherEnabled = false
-	filesystem := fcfg.Filesystem(nil)
+	filesystem := fcfg.Filesystem()
 
 	rawConfig := config.Configuration{
 		Version: config.CurrentVersion,
@@ -2759,7 +2758,7 @@ func TestIssue4094(t *testing.T) {
 		t.Fatalf("failed setting ignores: %v", err)
 	}
 
-	if _, err := fcfg.Filesystem(nil).Lstat(".stignore"); err != nil {
+	if _, err := fcfg.Filesystem().Lstat(".stignore"); err != nil {
 		t.Fatalf("failed stating .stignore: %v", err)
 	}
 }
@@ -2788,7 +2787,7 @@ func TestIssue4903(t *testing.T) {
 		t.Fatalf("expected path missing error, got: %v, debug: %s", err, fcfg.CheckPath())
 	}
 
-	if _, err := fcfg.Filesystem(nil).Lstat("."); !fs.IsNotExist(err) {
+	if _, err := fcfg.Filesystem().Lstat("."); !fs.IsNotExist(err) {
 		t.Fatalf("Expected missing path error, got: %v", err)
 	}
 }
@@ -2798,7 +2797,7 @@ func TestIssue5002(t *testing.T) {
 
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	fd, err := ffs.Create("foo")
 	must(t, err)
@@ -2827,7 +2826,7 @@ func TestIssue5002(t *testing.T) {
 func TestParentOfUnignored(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	must(t, ffs.Mkdir("bar", 0o755))
 	must(t, ffs.Mkdir("baz", 0o755))
@@ -2906,7 +2905,7 @@ func TestFolderRestartZombies(t *testing.T) {
 
 func TestRequestLimit(t *testing.T) {
 	wrapper, fcfg, cancel := newDefaultCfgWrapper()
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 
 	file := "tmpfile"
 	fd, err := ffs.Create(file)
@@ -2966,7 +2965,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	br := &testutil.BlockingRW{}
 	nw := &testutil.NoopRW{}
@@ -3009,7 +3008,7 @@ func TestModTimeWindow(t *testing.T) {
 	defer wCancel()
 	tfs := modtimeTruncatingFS{
 		trunc:      0,
-		Filesystem: fcfg.Filesystem(nil),
+		Filesystem: fcfg.Filesystem(),
 	}
 	// fcfg.RawModTimeWindowS = 2
 	setFolder(t, w, fcfg)
@@ -3069,7 +3068,7 @@ func TestModTimeWindow(t *testing.T) {
 func TestDevicePause(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	sub := m.evLogger.Subscribe(events.DevicePaused)
 	defer sub.Unsubscribe()
@@ -3099,7 +3098,7 @@ func TestDevicePause(t *testing.T) {
 func TestDeviceWasSeen(t *testing.T) {
 	m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	m.deviceWasSeen(device1)
 
@@ -3194,7 +3193,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 
 	numFiles := 20
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	for i := 0; i < numFiles; i++ {
 		v := fmt.Sprintf("%d", i)
 		writeFile(t, ffs, v, []byte(v))
@@ -3202,14 +3201,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 
 	m.ScanFolders()
 
-	count := 0
-	snap := dbSnapshot(t, m, "default")
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
-		count++
-		return true
-	})
-	snap.Release()
-
+	count := countIterator[protocol.FileInfo](t)(m.LocalFiles("default", protocol.LocalDeviceID))
 	if count != numFiles {
 		t.Errorf("Unexpected count: %d != %d", count, numFiles)
 	}
@@ -3229,14 +3221,11 @@ func TestRenameSequenceOrder(t *testing.T) {
 	// Scan
 	m.ScanFolders()
 
-	// Verify sequence of a appearing is followed by c disappearing.
-	snap = dbSnapshot(t, m, "default")
-	defer snap.Release()
-
 	var firstExpectedSequence int64
 	var secondExpectedSequence int64
 	failed := false
-	snap.WithHaveSequence(0, func(i protocol.FileInfo) bool {
+	it, errFn := m.LocalFilesSequenced("default", protocol.LocalDeviceID, 0)
+	for i := range it {
 		t.Log(i)
 		if i.FileName() == "17" {
 			firstExpectedSequence = i.SequenceNo() + 1
@@ -3250,8 +3239,10 @@ func TestRenameSequenceOrder(t *testing.T) {
 		if i.FileName() == "16" {
 			failed = i.SequenceNo() != secondExpectedSequence || failed
 		}
-		return true
-	})
+	}
+	if err := errFn(); err != nil {
+		t.Fatal(err)
+	}
 	if failed {
 		t.Fail()
 	}
@@ -3263,19 +3254,12 @@ func TestRenameSameFile(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	writeFile(t, ffs, "file", []byte("file"))
 
 	m.ScanFolders()
 
-	count := 0
-	snap := dbSnapshot(t, m, "default")
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
-		count++
-		return true
-	})
-	snap.Release()
-
+	count := countIterator[protocol.FileInfo](t)(m.LocalFiles("default", protocol.LocalDeviceID))
 	if count != 1 {
 		t.Errorf("Unexpected count: %d != %d", count, 1)
 	}
@@ -3288,12 +3272,10 @@ func TestRenameSameFile(t *testing.T) {
 
 	m.ScanFolders()
 
-	snap = dbSnapshot(t, m, "default")
-	defer snap.Release()
-
 	prevSeq := int64(0)
 	seen := false
-	snap.WithHaveSequence(0, func(i protocol.FileInfo) bool {
+	it, errFn := m.LocalFilesSequenced("default", protocol.LocalDeviceID, 0)
+	for i := range it {
 		if i.SequenceNo() <= prevSeq {
 			t.Fatalf("non-increasing sequences: %d <= %d", i.SequenceNo(), prevSeq)
 		}
@@ -3304,84 +3286,9 @@ func TestRenameSameFile(t *testing.T) {
 			seen = true
 		}
 		prevSeq = i.SequenceNo()
-		return true
-	})
-}
-
-func TestRenameEmptyFile(t *testing.T) {
-	wcfg, fcfg, wcfgCancel := newDefaultCfgWrapper()
-	defer wcfgCancel()
-	m := setupModel(t, wcfg)
-	defer cleanupModel(m)
-
-	ffs := fcfg.Filesystem(nil)
-
-	writeFile(t, ffs, "file", []byte("data"))
-	writeFile(t, ffs, "empty", nil)
-
-	m.ScanFolders()
-
-	snap := dbSnapshot(t, m, "default")
-	defer snap.Release()
-	empty, eok := snap.Get(protocol.LocalDeviceID, "empty")
-	if !eok {
-		t.Fatal("failed to find empty file")
-	}
-	file, fok := snap.Get(protocol.LocalDeviceID, "file")
-	if !fok {
-		t.Fatal("failed to find non-empty file")
-	}
-
-	count := 0
-	snap.WithBlocksHash(empty.BlocksHash, func(_ protocol.FileInfo) bool {
-		count++
-		return true
-	})
-
-	if count != 0 {
-		t.Fatalf("Found %d entries for empty file, expected 0", count)
-	}
-
-	count = 0
-	snap.WithBlocksHash(file.BlocksHash, func(_ protocol.FileInfo) bool {
-		count++
-		return true
-	})
-
-	if count != 1 {
-		t.Fatalf("Found %d entries for non-empty file, expected 1", count)
-	}
-
-	must(t, ffs.Rename("file", "new-file"))
-	must(t, ffs.Rename("empty", "new-empty"))
-
-	// Scan
-	m.ScanFolders()
-
-	snap = dbSnapshot(t, m, "default")
-	defer snap.Release()
-
-	count = 0
-	snap.WithBlocksHash(empty.BlocksHash, func(_ protocol.FileInfo) bool {
-		count++
-		return true
-	})
-
-	if count != 0 {
-		t.Fatalf("Found %d entries for empty file, expected 0", count)
 	}
-
-	count = 0
-	snap.WithBlocksHash(file.BlocksHash, func(i protocol.FileInfo) bool {
-		count++
-		if i.FileName() != "new-file" {
-			t.Fatalf("unexpected file name %s, expected new-file", i.FileName())
-		}
-		return true
-	})
-
-	if count != 1 {
-		t.Fatalf("Found %d entries for non-empty file, expected 1", count)
+	if err := errFn(); err != nil {
+		t.Fatal(err)
 	}
 }
 
@@ -3391,7 +3298,7 @@ func TestBlockListMap(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	writeFile(t, ffs, "one", []byte("content"))
 	writeFile(t, ffs, "two", []byte("content"))
 	writeFile(t, ffs, "three", []byte("content"))
@@ -3400,23 +3307,25 @@ func TestBlockListMap(t *testing.T) {
 
 	m.ScanFolders()
 
-	snap := dbSnapshot(t, m, "default")
-	defer snap.Release()
-	fi, ok := snap.Get(protocol.LocalDeviceID, "one")
+	fi, ok, err := m.model.CurrentFolderFile("default", "one")
+	if err != nil {
+		t.Fatal(err)
+	}
 	if !ok {
 		t.Error("failed to find existing file")
 	}
 	var paths []string
 
-	snap.WithBlocksHash(fi.BlocksHash, func(fi protocol.FileInfo) bool {
-		paths = append(paths, fi.FileName())
-		return true
-	})
-	snap.Release()
+	for fi, err := range itererr.Zip(m.model.AllForBlocksHash(fcfg.ID, fi.BlocksHash)) {
+		if err != nil {
+			t.Fatal(err)
+		}
+		paths = append(paths, fi.Name)
+	}
 
 	expected := []string{"one", "two", "three", "four", "five"}
 	if !equalStringsInAnyOrder(paths, expected) {
-		t.Errorf("expected %q got %q", expected, paths)
+		t.Fatalf("expected %q got %q", expected, paths)
 	}
 
 	// Fudge the files around
@@ -3437,19 +3346,18 @@ func TestBlockListMap(t *testing.T) {
 	m.ScanFolders()
 
 	// Check we're left with 2 of the 5
-	snap = dbSnapshot(t, m, "default")
-	defer snap.Release()
 
 	paths = paths[:0]
-	snap.WithBlocksHash(fi.BlocksHash, func(fi protocol.FileInfo) bool {
-		paths = append(paths, fi.FileName())
-		return true
-	})
-	snap.Release()
+	for fi, err := range itererr.Zip(m.model.AllForBlocksHash(fcfg.ID, fi.BlocksHash)) {
+		if err != nil {
+			t.Fatal(err)
+		}
+		paths = append(paths, fi.Name)
+	}
 
 	expected = []string{"new-three", "five"}
 	if !equalStringsInAnyOrder(paths, expected) {
-		t.Errorf("expected %q got %q", expected, paths)
+		t.Fatalf("expected %q got %q", expected, paths)
 	}
 }
 
@@ -3459,16 +3367,17 @@ func TestScanRenameCaseOnly(t *testing.T) {
 	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	name := "foo"
 	writeFile(t, ffs, name, []byte("contents"))
 
 	m.ScanFolders()
 
-	snap := dbSnapshot(t, m, fcfg.ID)
-	defer snap.Release()
 	found := false
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
+	for i, err := range itererr.Zip(m.LocalFiles(fcfg.ID, protocol.LocalDeviceID)) {
+		if err != nil {
+			t.Fatal(err)
+		}
 		if found {
 			t.Fatal("got more than one file")
 		}
@@ -3476,21 +3385,20 @@ func TestScanRenameCaseOnly(t *testing.T) {
 			t.Fatalf("got file %v, expected %v", i.FileName(), name)
 		}
 		found = true
-		return true
-	})
-	snap.Release()
+	}
 
 	upper := strings.ToUpper(name)
 	must(t, ffs.Rename(name, upper))
 	m.ScanFolders()
 
-	snap = dbSnapshot(t, m, fcfg.ID)
-	defer snap.Release()
 	found = false
-	snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileInfo) bool {
+	for i, err := range itererr.Zip(m.LocalFiles(fcfg.ID, protocol.LocalDeviceID)) {
+		if err != nil {
+			t.Fatal(err)
+		}
 		if i.FileName() == name {
 			if i.IsDeleted() {
-				return true
+				continue
 			}
 			t.Fatal("renamed file not deleted")
 		}
@@ -3501,8 +3409,7 @@ func TestScanRenameCaseOnly(t *testing.T) {
 			t.Fatal("got more than the expected files")
 		}
 		found = true
-		return true
-	})
+	}
 }
 
 func TestClusterConfigOnFolderAdd(t *testing.T) {
@@ -3577,7 +3484,7 @@ func TestAddFolderCompletion(t *testing.T) {
 
 func TestScanDeletedROChangedOnSR(t *testing.T) {
 	m, conn, fcfg, wCancel := setupModelWithConnection(t)
-	ffs := fcfg.Filesystem(nil)
+	ffs := fcfg.Filesystem()
 	defer wCancel()
 	defer cleanupModelAndRemoveDir(m, ffs.URI())
 	fcfg.Type = config.FolderTypeReceiveOnly
@@ -3599,7 +3506,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
 	must(t, ffs.Remove(name))
 	m.ScanFolders()
 
-	if receiveOnlyChangedSize(t, m, fcfg.ID).Deleted != 1 {
+	if mustV(m.ReceiveOnlySize(fcfg.ID)).Deleted != 1 {
 		t.Fatal("expected one receive only changed deleted item")
 	}
 
@@ -3607,10 +3514,10 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
 	setFolder(t, m.cfg, fcfg)
 	m.ScanFolders()
 
-	if receiveOnlyChangedSize(t, m, fcfg.ID).Deleted != 0 {
+	if mustV(m.ReceiveOnlySize(fcfg.ID)).Deleted != 0 {
 		t.Fatal("expected no receive only changed deleted item")
 	}
-	if localSize(t, m, fcfg.ID).Deleted != 1 {
+	if mustV(m.LocalSize(fcfg.ID, protocol.LocalDeviceID)).Deleted != 1 {
 		t.Fatal("expected one local deleted item")
 	}
 }
@@ -3682,7 +3589,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
 func TestIssue6961(t *testing.T) {
 	wcfg, fcfg, wcfgCancel := newDefaultCfgWrapper()
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	waiter, err := wcfg.Modify(func(cfg *config.Configuration) {
 		cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2"))
 		fcfg.Type = config.FolderTypeReceiveOnly
@@ -3693,11 +3600,6 @@ func TestIssue6961(t *testing.T) {
 	waiter.Wait()
 	// Always recalc/repair when opening a fileset.
 	m := newModel(t, wcfg, myID, nil)
-	m.db.Close()
-	m.db, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond))
-	if err != nil {
-		t.Fatal(err)
-	}
 	m.ServeBackground()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 	conn1 := addFakeConn(m, device1, fcfg.ID)
@@ -3752,11 +3654,9 @@ func TestIssue6961(t *testing.T) {
 func TestCompletionEmptyGlobal(t *testing.T) {
 	m, conn, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID.Short()), Sequence: 1}}
-	m.mut.Lock()
-	m.folderFiles[fcfg.ID].Update(protocol.LocalDeviceID, files)
-	m.mut.Unlock()
+	m.sdb.Update(fcfg.ID, protocol.LocalDeviceID, files)
 	files[0].Deleted = true
 	files[0].Version = files[0].Version.Update(device1.Short())
 	must(t, m.IndexUpdate(conn, &protocol.IndexUpdate{Folder: fcfg.ID, Files: files}))
@@ -3953,7 +3853,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 	// Create the folder, but don't start it.
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	m := newModel(t, w, myID, nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -3990,7 +3890,7 @@ func TestPendingFolder(t *testing.T) {
 		Time:  time.Now().Truncate(time.Second),
 		Label: pfolder,
 	}
-	if err := m.db.AddOrUpdatePendingFolder(pfolder, of, device2); err != nil {
+	if err := m.observed.AddOrUpdatePendingFolder(pfolder, of, device2); err != nil {
 		t.Fatal(err)
 	}
 	deviceFolders, err := m.PendingFolders(protocol.EmptyDeviceID)
@@ -4009,7 +3909,7 @@ func TestPendingFolder(t *testing.T) {
 		t.Fatal(err)
 	}
 	setDevice(t, w, config.DeviceConfiguration{DeviceID: device3})
-	if err := m.db.AddOrUpdatePendingFolder(pfolder, of, device3); err != nil {
+	if err := m.observed.AddOrUpdatePendingFolder(pfolder, of, device3); err != nil {
 		t.Fatal(err)
 	}
 	deviceFolders, err = m.PendingFolders(device2)
@@ -4060,7 +3960,7 @@ func TestDeletedNotLocallyChangedReceiveEncrypted(t *testing.T) {
 
 func deletedNotLocallyChanged(t *testing.T, ft config.FolderType) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	defer wCancel()
@@ -4141,3 +4041,17 @@ type modtimeTruncatingFileInfo struct {
 func (fi modtimeTruncatingFileInfo) ModTime() time.Time {
 	return fi.FileInfo.ModTime().Truncate(fi.trunc)
 }
+
+func countIterator[T any](t *testing.T) func(it iter.Seq[T], errFn func() error) int {
+	return func(it iter.Seq[T], errFn func() error) int {
+		t.Helper()
+		count := 0
+		for range it {
+			count++
+		}
+		if err := errFn(); err != nil {
+			t.Fatal(err)
+		}
+		return count
+	}
+}
diff --git a/lib/model/queue_test.go b/lib/model/queue_test.go
index a49f2c70f6d..d8467ff4d12 100644
--- a/lib/model/queue_test.go
+++ b/lib/model/queue_test.go
@@ -163,95 +163,6 @@ func TestBringToFront(t *testing.T) {
 	}
 }
 
-func TestShuffle(t *testing.T) {
-	q := newJobQueue()
-	q.Push("f1", 0, time.Time{})
-	q.Push("f2", 0, time.Time{})
-	q.Push("f3", 0, time.Time{})
-	q.Push("f4", 0, time.Time{})
-
-	// This test will fail once in eight million times (1 / (4!)^5) :)
-	for i := 0; i < 5; i++ {
-		q.Shuffle()
-		_, queued, _ := q.Jobs(1, 100)
-		if l := len(queued); l != 4 {
-			t.Fatalf("Weird length %d returned from jobs(1, 100)", l)
-		}
-
-		t.Logf("%v", queued)
-		if _, equal := messagediff.PrettyDiff([]string{"f1", "f2", "f3", "f4"}, queued); !equal {
-			// The queue was shuffled
-			return
-		}
-	}
-
-	t.Error("Queue was not shuffled after five attempts.")
-}
-
-func TestSortBySize(t *testing.T) {
-	q := newJobQueue()
-	q.Push("f1", 20, time.Time{})
-	q.Push("f2", 40, time.Time{})
-	q.Push("f3", 30, time.Time{})
-	q.Push("f4", 10, time.Time{})
-
-	q.SortSmallestFirst()
-
-	_, actual, _ := q.Jobs(1, 100)
-	if l := len(actual); l != 4 {
-		t.Fatalf("Weird length %d returned from jobs(1, 100)", l)
-	}
-	expected := []string{"f4", "f1", "f3", "f2"}
-
-	if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
-		t.Errorf("SortSmallestFirst() diff:\n%s", diff)
-	}
-
-	q.SortLargestFirst()
-
-	_, actual, _ = q.Jobs(1, 100)
-	if l := len(actual); l != 4 {
-		t.Fatalf("Weird length %d returned from jobs(1, 100)", l)
-	}
-	expected = []string{"f2", "f3", "f1", "f4"}
-
-	if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
-		t.Errorf("SortLargestFirst() diff:\n%s", diff)
-	}
-}
-
-func TestSortByAge(t *testing.T) {
-	q := newJobQueue()
-	q.Push("f1", 0, time.Unix(20, 0))
-	q.Push("f2", 0, time.Unix(40, 0))
-	q.Push("f3", 0, time.Unix(30, 0))
-	q.Push("f4", 0, time.Unix(10, 0))
-
-	q.SortOldestFirst()
-
-	_, actual, _ := q.Jobs(1, 100)
-	if l := len(actual); l != 4 {
-		t.Fatalf("Weird length %d returned from jobs(1, 100)", l)
-	}
-	expected := []string{"f4", "f1", "f3", "f2"}
-
-	if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
-		t.Errorf("SortOldestFirst() diff:\n%s", diff)
-	}
-
-	q.SortNewestFirst()
-
-	_, actual, _ = q.Jobs(1, 100)
-	if l := len(actual); l != 4 {
-		t.Fatalf("Weird length %d returned from jobs(1, 100)", l)
-	}
-	expected = []string{"f2", "f3", "f1", "f4"}
-
-	if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
-		t.Errorf("SortNewestFirst() diff:\n%s", diff)
-	}
-}
-
 func BenchmarkJobQueueBump(b *testing.B) {
 	files := genFiles(10000)
 
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
index 991656ed31e..3e437ebfbcb 100644
--- a/lib/model/requests_test.go
+++ b/lib/model/requests_test.go
@@ -32,7 +32,7 @@ func TestRequestSimple(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -80,7 +80,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -123,7 +123,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected names.
@@ -182,7 +182,7 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
 	// the expected test file.
@@ -229,7 +229,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 	w, wCancel := newConfigWrapper(defaultCfgWrapper.RawCopy())
 	defer wCancel()
 	fcfg := w.FolderList()[0]
-	fss := fcfg.Filesystem(nil)
+	fss := fcfg.Filesystem()
 	fcfg.Type = ft
 	setFolder(t, w, fcfg)
 	m := setupModel(t, w)
@@ -358,7 +358,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 func TestIssue4841(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, _ string, fs []protocol.FileInfo) error {
@@ -407,7 +407,7 @@ func TestIssue4841(t *testing.T) {
 func TestRescanIfHaveInvalidContent(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	payload := []byte("hello")
@@ -465,9 +465,11 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 }
 
 func TestParentDeletion(t *testing.T) {
+	t.Skip("flaky")
+
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	testFs := fcfg.Filesystem(nil)
+	testFs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, testFs.URI())
 
 	parent := "foo"
@@ -546,7 +548,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem(nil).URI())
+	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	received := make(chan []protocol.FileInfo)
 	fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
@@ -623,7 +625,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModel(m)
 
 	received := make(chan []protocol.FileInfo)
@@ -756,7 +758,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
 func TestRequestRemoteRenameConflict(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModel(m)
 
 	recv := make(chan int)
@@ -846,7 +848,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
 func TestRequestDeleteChanged(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -960,7 +962,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
 	m := setupModel(t, w)
-	fss := fcfg.Filesystem(nil)
+	fss := fcfg.Filesystem()
 	defer cleanupModel(m)
 
 	folderIgnoresAlwaysReload(t, m, fcfg)
@@ -1054,7 +1056,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 func TestRequestLastFileProgress(t *testing.T) {
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	done := make(chan struct{})
@@ -1089,7 +1091,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 
 	m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
 	defer wcfgCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo)
@@ -1202,7 +1204,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	dir1 := "foo"
 	dir2 := "bar"
 
@@ -1217,7 +1219,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 
 	// Add connection (sends incoming cluster config) before starting the new model
 	m = &testModel{
-		model:    NewModel(m.cfg, m.id, m.db, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model),
+		model:    NewModel(m.cfg, m.id, m.sdb, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model),
 		evCancel: m.evCancel,
 		stopped:  make(chan struct{}),
 	}
@@ -1269,7 +1271,7 @@ func TestRequestReceiveEncrypted(t *testing.T) {
 
 	w, fcfg, wCancel := newDefaultCfgWrapper()
 	defer wCancel()
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	fcfg.Type = config.FolderTypeReceiveEncrypted
 	setFolder(t, w, fcfg)
 
@@ -1281,10 +1283,7 @@ func TestRequestReceiveEncrypted(t *testing.T) {
 
 	files := genFiles(2)
 	files[1].LocalFlags = protocol.FlagLocalReceiveOnly
-	m.mut.RLock()
-	fset := m.folderFiles[fcfg.ID]
-	m.mut.RUnlock()
-	fset.Update(protocol.LocalDeviceID, files)
+	m.sdb.Update(fcfg.ID, protocol.LocalDeviceID, files)
 
 	indexChan := make(chan []protocol.FileInfo, 10)
 	done := make(chan struct{})
@@ -1376,7 +1375,7 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
 	must(t, err)
 	waiter.Wait()
 	conn := addFakeConn(m, device2, fcfg.ID)
-	tfs := fcfg.Filesystem(nil)
+	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	indexChan := make(chan []protocol.FileInfo, 1)
@@ -1402,7 +1401,7 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
 	file.SetIgnored()
 	m.IndexUpdate(conn, &protocol.IndexUpdate{Folder: fcfg.ID, Files: []protocol.FileInfo{prepareFileInfoForIndex(file)}})
 
-	// Wait for the ignored file to be received and possible pulled
+	// Wait for the ignored file to be received and possibly pulled
 	timeout := time.After(10 * time.Second)
 	globalUpdated := false
 	for {
@@ -1422,13 +1421,9 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
 			}
 			globalUpdated = true
 		}
-		snap, err := m.DBSnapshot(fcfg.ID)
-		if err != nil {
+		if s, err := m.NeedSize(fcfg.ID, protocol.LocalDeviceID); err != nil {
 			t.Fatal(err)
-		}
-		need := snap.NeedSize(protocol.LocalDeviceID)
-		snap.Release()
-		if need.Files == 0 {
+		} else if s.Files == 0 {
 			break
 		}
 	}
diff --git a/lib/model/testos_test.go b/lib/model/testos_test.go
index 2c56265b635..b7110ef7571 100644
--- a/lib/model/testos_test.go
+++ b/lib/model/testos_test.go
@@ -6,10 +6,6 @@
 
 package model
 
-import (
-	"github.com/syncthing/syncthing/lib/fs"
-)
-
 // fatal is the required common interface between *testing.B and *testing.T
 type fatal interface {
 	Fatal(...interface{})
@@ -23,9 +19,9 @@ func must(f fatal, err error) {
 	}
 }
 
-func mustRemove(f fatal, err error) {
-	f.Helper()
-	if err != nil && !fs.IsNotExist(err) {
-		f.Fatal(err)
+func mustV[T any](v T, err error) T {
+	if err != nil {
+		panic(err)
 	}
+	return v
 }
diff --git a/lib/model/testutils_test.go b/lib/model/testutils_test.go
index 1e203f9fd23..7949bc61712 100644
--- a/lib/model/testutils_test.go
+++ b/lib/model/testutils_test.go
@@ -12,9 +12,8 @@ import (
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db/sqlite"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/ignore"
@@ -149,11 +148,14 @@ type testModel struct {
 func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, protectedFiles []string) *testModel {
 	t.Helper()
 	evLogger := events.NewLogger()
-	ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger)
+	mdb, err := sqlite.OpenTemp()
 	if err != nil {
 		t.Fatal(err)
 	}
-	m := NewModel(cfg, id, ldb, protectedFiles, evLogger, protocol.NewKeyGenerator()).(*model)
+	t.Cleanup(func() {
+		mdb.Close()
+	})
+	m := NewModel(cfg, id, mdb, protectedFiles, evLogger, protocol.NewKeyGenerator()).(*model)
 	ctx, cancel := context.WithCancel(context.Background())
 	go evLogger.Serve(ctx)
 	return &testModel{
@@ -174,12 +176,6 @@ func (m *testModel) ServeBackground() {
 	<-m.started
 }
 
-func (m *testModel) testAvailability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
-	av, err := m.model.Availability(folder, file, block)
-	must(m.t, err)
-	return av
-}
-
 func (m *testModel) testCurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
 	f, ok, err := m.model.CurrentFolderFile(folder, file)
 	must(m.t, err)
@@ -198,7 +194,7 @@ func cleanupModel(m *testModel) {
 		<-m.stopped
 	}
 	m.evCancel()
-	m.db.Close()
+	m.sdb.Close()
 	os.Remove(m.cfg.ConfigPath())
 }
 
@@ -240,52 +236,6 @@ func (*alwaysChanged) Changed() bool {
 	return true
 }
 
-func localSize(t *testing.T, m Model, folder string) db.Counts {
-	t.Helper()
-	snap := dbSnapshot(t, m, folder)
-	defer snap.Release()
-	return snap.LocalSize()
-}
-
-func globalSize(t *testing.T, m Model, folder string) db.Counts {
-	t.Helper()
-	snap := dbSnapshot(t, m, folder)
-	defer snap.Release()
-	return snap.GlobalSize()
-}
-
-func receiveOnlyChangedSize(t *testing.T, m Model, folder string) db.Counts {
-	t.Helper()
-	snap := dbSnapshot(t, m, folder)
-	defer snap.Release()
-	return snap.ReceiveOnlyChangedSize()
-}
-
-func needSizeLocal(t *testing.T, m Model, folder string) db.Counts {
-	t.Helper()
-	snap := dbSnapshot(t, m, folder)
-	defer snap.Release()
-	return snap.NeedSize(protocol.LocalDeviceID)
-}
-
-func dbSnapshot(t *testing.T, m Model, folder string) *db.Snapshot {
-	t.Helper()
-	snap, err := m.DBSnapshot(folder)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return snap
-}
-
-func fsetSnapshot(t *testing.T, fset *db.FileSet) *db.Snapshot {
-	t.Helper()
-	snap, err := fset.Snapshot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	return snap
-}
-
 // Reach in and update the ignore matcher to one that always does
 // reloads when asked to, instead of checking file mtimes. This is
 // because we will be changing the files on disk often enough that the
@@ -293,10 +243,9 @@ func fsetSnapshot(t *testing.T, fset *db.FileSet) *db.Snapshot {
 func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderConfiguration) {
 	t.Helper()
 	m.removeFolder(fcfg)
-	fset := newFileSet(t, fcfg.ID, m.db)
-	ignores := ignore.New(fcfg.Filesystem(nil), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
+	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
 	m.mut.Lock()
-	m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores)
+	m.addAndStartFolderLockedWithIgnores(fcfg, ignores)
 	m.mut.Unlock()
 }
 
@@ -319,12 +268,11 @@ func basicClusterConfig(local, remote protocol.DeviceID, folders ...string) *pro
 }
 
 func localIndexUpdate(m *testModel, folder string, fs []protocol.FileInfo) {
-	m.mut.RLock()
-	fset := m.folderFiles[folder]
-	m.mut.RUnlock()
-
-	fset.Update(protocol.LocalDeviceID, fs)
-	seq := fset.Sequence(protocol.LocalDeviceID)
+	m.sdb.Update(folder, protocol.LocalDeviceID, fs)
+	seq, err := m.sdb.GetDeviceSequence(folder, protocol.LocalDeviceID)
+	if err != nil {
+		panic(err)
+	}
 	filenames := make([]string, len(fs))
 	for i, file := range fs {
 		filenames[i] = file.Name
@@ -345,15 +293,6 @@ func newDeviceConfiguration(defaultCfg config.DeviceConfiguration, id protocol.D
 	return cfg
 }
 
-func newFileSet(t testing.TB, folder string, ldb *db.Lowlevel) *db.FileSet {
-	t.Helper()
-	fset, err := db.NewFileSet(folder, ldb)
-	if err != nil {
-		t.Fatal(err)
-	}
-	return fset
-}
-
 func replace(t testing.TB, w config.Wrapper, to config.Configuration) {
 	t.Helper()
 	waiter, err := w.Modify(func(cfg *config.Configuration) {
diff --git a/lib/protocol/protocol_test.go b/lib/protocol/protocol_test.go
index c4f304126e1..8a0c1fa1ff5 100644
--- a/lib/protocol/protocol_test.go
+++ b/lib/protocol/protocol_test.go
@@ -10,10 +10,8 @@ import (
 	"bytes"
 	"context"
 	"encoding/hex"
-	"encoding/json"
 	"errors"
 	"io"
-	"os"
 	"sync"
 	"testing"
 	"time"
@@ -280,28 +278,6 @@ func TestUnmarshalFDPUv16v17(t *testing.T) {
 	}
 }
 
-func testMarshal(t *testing.T, prefix string, m1, m2 proto.Message) bool {
-	buf, err := proto.Marshal(m1)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	err = proto.Unmarshal(buf, m2)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	bs1, _ := json.MarshalIndent(m1, "", "  ")
-	bs2, _ := json.MarshalIndent(m2, "", "  ")
-	if !bytes.Equal(bs1, bs2) {
-		os.WriteFile(prefix+"-1.txt", bs1, 0o644)
-		os.WriteFile(prefix+"-2.txt", bs2, 0o644)
-		return false
-	}
-
-	return true
-}
-
 func TestWriteCompressed(t *testing.T) {
 	for _, random := range []bool{false, true} {
 		buf := new(bytes.Buffer)
diff --git a/lib/stats/stats_test.go b/lib/stats/stats_test.go
index 0ebb423424e..7f850ae36b6 100644
--- a/lib/stats/stats_test.go
+++ b/lib/stats/stats_test.go
@@ -13,15 +13,20 @@ import (
 	"testing"
 	"time"
 
-	"github.com/syncthing/syncthing/lib/db/backend"
-	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/internal/db"
+	"github.com/syncthing/syncthing/internal/db/sqlite"
 )
 
 func TestDeviceStat(t *testing.T) {
-	db := backend.OpenLevelDBMemory()
-	defer db.Close()
+	sdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		sdb.Close()
+	})
 
-	sr := NewDeviceStatisticsReference(db, protocol.LocalDeviceID)
+	sr := NewDeviceStatisticsReference(db.NewTyped(sdb, "devstatref"))
 	if err := sr.WasSeen(); err != nil {
 		t.Fatal(err)
 	}
diff --git a/lib/syncthing/syncthing_test.go b/lib/syncthing/syncthing_test.go
index e9feedf4487..89dcabc1c11 100644
--- a/lib/syncthing/syncthing_test.go
+++ b/lib/syncthing/syncthing_test.go
@@ -8,11 +8,12 @@ package syncthing
 
 import (
 	"os"
+	"strings"
 	"testing"
 	"time"
 
+	"github.com/syncthing/syncthing/internal/db/sqlite"
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/svcutil"
@@ -71,8 +72,14 @@ func TestStartupFail(t *testing.T) {
 	}, protocol.LocalDeviceID, events.NoopLogger)
 	defer os.Remove(cfg.ConfigPath())
 
-	db := backend.OpenMemory()
-	app, err := New(cfg, db, events.NoopLogger, cert, Options{})
+	sdb, err := sqlite.OpenTemp()
+	if err != nil {
+		t.Fatal(err)
+	}
+	t.Cleanup(func() {
+		sdb.Close()
+	})
+	app, err := New(cfg, sdb, events.NoopLogger, cert, Options{})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -102,10 +109,9 @@ func TestStartupFail(t *testing.T) {
 		t.Errorf(`Got different errors "%v" from Start and "%v" from Error`, startErr, err)
 	}
 
-	if trans, err := db.NewReadTransaction(); err == nil {
+	if _, err := sdb.ListFolders(); err == nil {
 		t.Error("Expected error due to db being closed, got nil")
-		trans.Release()
-	} else if !backend.IsClosed(err) {
+	} else if !strings.Contains(err.Error(), "closed") {
 		t.Error("Expected error due to db being closed, got", err)
 	}
 }
diff --git a/lib/versioner/simple_test.go b/lib/versioner/simple_test.go
index 0087f826bd1..affcf3081ee 100644
--- a/lib/versioner/simple_test.go
+++ b/lib/versioner/simple_test.go
@@ -64,7 +64,7 @@ func TestSimpleVersioningVersionCount(t *testing.T) {
 			},
 		},
 	}
-	fs := cfg.Filesystem(nil)
+	fs := cfg.Filesystem()
 
 	v := newSimple(cfg)
 
@@ -116,7 +116,7 @@ func TestPathTildes(t *testing.T) {
 			},
 		},
 	}
-	fs := cfg.Filesystem(nil)
+	fs := cfg.Filesystem()
 	v := newSimple(cfg)
 
 	const testPath = "test"
diff --git a/lib/versioner/trashcan_test.go b/lib/versioner/trashcan_test.go
index a12ee60765c..d1abe03496b 100644
--- a/lib/versioner/trashcan_test.go
+++ b/lib/versioner/trashcan_test.go
@@ -34,7 +34,7 @@ func TestTrashcanArchiveRestoreSwitcharoo(t *testing.T) {
 			FSPath: tmpDir2,
 		},
 	}
-	folderFs := cfg.Filesystem(nil)
+	folderFs := cfg.Filesystem()
 
 	versionsFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir2)
 
@@ -113,7 +113,7 @@ func TestTrashcanRestoreDeletedFile(t *testing.T) {
 		},
 	}
 
-	folderFs := cfg.Filesystem(nil)
+	folderFs := cfg.Filesystem()
 
 	versionsFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir2)
 
@@ -209,7 +209,7 @@ func TestTrashcanCleanOut(t *testing.T) {
 		},
 	}
 
-	fs := cfg.Filesystem(nil)
+	fs := cfg.Filesystem()
 
 	v := newTrashcan(cfg)
 
diff --git a/meta/copyright_test.go b/meta/copyright_test.go
index 8de2662102d..31078562ac6 100644
--- a/meta/copyright_test.go
+++ b/meta/copyright_test.go
@@ -19,11 +19,12 @@ import (
 
 // File extensions to check
 var copyrightCheckExts = map[string]bool{
-	".go": true,
+	".go":  true,
+	".sql": true,
 }
 
 // Directories to search
-var copyrightCheckDirs = []string{".", "../cmd", "../lib", "../test", "../script"}
+var copyrightCheckDirs = []string{".", "../cmd", "../internal", "../lib", "../test", "../script"}
 
 // Valid copyright headers, searched for in the top five lines in each file.
 var copyrightRegexps = []string{