queryview 0.0.4__tar.gz → 0.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {queryview-0.0.4 → queryview-0.0.6}/.github/actions/start-git-daemon/action.yaml +3 -3
  2. {queryview-0.0.4 → queryview-0.0.6}/.github/workflows/publish.yaml +4 -17
  3. {queryview-0.0.4 → queryview-0.0.6}/.gitignore +1 -0
  4. {queryview-0.0.4 → queryview-0.0.6}/CLAUDE.md +12 -0
  5. {queryview-0.0.4 → queryview-0.0.6}/PKG-INFO +68 -12
  6. {queryview-0.0.4 → queryview-0.0.6}/README.md +67 -10
  7. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/conftest.py +16 -12
  8. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/connect.py +16 -15
  9. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/gitsync.py +85 -23
  10. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/env.py +1 -1
  11. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +7 -13
  12. queryview-0.0.6/backend/queryview/static/assets/index-Ba8F6rEq.css +2 -0
  13. queryview-0.0.4/backend/queryview/static/assets/index-BE8_u4Np.js → queryview-0.0.6/backend/queryview/static/assets/index-DX2c3Adh.js +1 -1
  14. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/static/index.html +2 -2
  15. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_gitsync.py +1 -1
  16. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_connect_store.py +2 -3
  17. queryview-0.0.6/backend/queryview/test_data_dir.py +51 -0
  18. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_gitsync.py +109 -9
  19. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_mcp_gitsync.py +2 -4
  20. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_migrations.py +5 -7
  21. {queryview-0.0.4 → queryview-0.0.6}/docker/Dockerfile +13 -6
  22. {queryview-0.0.4 → queryview-0.0.6}/docs/connect.md +10 -5
  23. {queryview-0.0.4 → queryview-0.0.6}/docs/future.md +3 -2
  24. {queryview-0.0.4 → queryview-0.0.6}/docs/gitsync.md +19 -9
  25. {queryview-0.0.4 → queryview-0.0.6}/docs/workspace.md +3 -4
  26. {queryview-0.0.4 → queryview-0.0.6}/e2e/conftest.py +11 -0
  27. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_app.py +11 -1
  28. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_gitsync.py +5 -4
  29. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/QueryView.tsx +38 -2
  30. queryview-0.0.6/frontend/src/app/databaseFilter.test.ts +28 -0
  31. queryview-0.0.6/frontend/src/app/databaseFilter.ts +7 -0
  32. {queryview-0.0.4 → queryview-0.0.6}/pyproject.toml +0 -1
  33. {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_browser.sh +1 -1
  34. {queryview-0.0.4 → queryview-0.0.6}/uv.lock +0 -2
  35. queryview-0.0.4/backend/queryview/static/assets/index-B3W6A5bg.css +0 -2
  36. queryview-0.0.4/backend/queryview/test_db_path.py +0 -58
  37. {queryview-0.0.4 → queryview-0.0.6}/.claude/settings.json +0 -0
  38. {queryview-0.0.4 → queryview-0.0.6}/.github/workflows/ci.yml +0 -0
  39. {queryview-0.0.4 → queryview-0.0.6}/.mcp.json +0 -0
  40. {queryview-0.0.4 → queryview-0.0.6}/.pre-commit-config.yaml +0 -0
  41. {queryview-0.0.4 → queryview-0.0.6}/LICENSE +0 -0
  42. {queryview-0.0.4 → queryview-0.0.6}/backend/alembic.ini +0 -0
  43. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/__init__.py +0 -0
  44. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/dashboard_queries.py +0 -0
  45. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/dashboards.py +0 -0
  46. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/__init__.py +0 -0
  47. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/base.py +0 -0
  48. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/clickhouse.py +0 -0
  49. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/duckdb.py +0 -0
  50. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/postgres.py +0 -0
  51. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_base.py +0 -0
  52. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_clickhouse.py +0 -0
  53. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_contract.py +0 -0
  54. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_duckdb.py +0 -0
  55. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_postgres.py +0 -0
  56. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/main.py +0 -0
  57. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/mcp_server.py +0 -0
  58. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/script.py.mako +0 -0
  59. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/9a536b7c0328_initial_schema.py +0 -0
  60. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +0 -0
  61. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +0 -0
  62. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/queries.py +0 -0
  63. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/remote.py +0 -0
  64. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/static/favicon.svg +0 -0
  65. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_db.py +0 -0
  66. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_export_import.py +0 -0
  67. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_workspaces.py +0 -0
  68. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_connect_flow.py +0 -0
  69. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_dashboard_queries.py +0 -0
  70. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_dashboards.py +0 -0
  71. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_main.py +0 -0
  72. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_mcp_mount.py +0 -0
  73. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_queries.py +0 -0
  74. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_remote.py +0 -0
  75. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_validation.py +0 -0
  76. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_workspaces.py +0 -0
  77. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_yamlio.py +0 -0
  78. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/validation.py +0 -0
  79. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/workspaces.py +0 -0
  80. {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/yamlio.py +0 -0
  81. {queryview-0.0.4 → queryview-0.0.6}/docs/api.md +0 -0
  82. {queryview-0.0.4 → queryview-0.0.6}/docs/dashboard.md +0 -0
  83. {queryview-0.0.4 → queryview-0.0.6}/docs/explorer.md +0 -0
  84. {queryview-0.0.4 → queryview-0.0.6}/docs/export-import.md +0 -0
  85. {queryview-0.0.4 → queryview-0.0.6}/docs/query.md +0 -0
  86. {queryview-0.0.4 → queryview-0.0.6}/docs/queryview.md +0 -0
  87. {queryview-0.0.4 → queryview-0.0.6}/docs/remote.md +0 -0
  88. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_dashboard.py +0 -0
  89. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_drivers.py +0 -0
  90. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_explorer.py +0 -0
  91. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_export_import.py +0 -0
  92. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_query.py +0 -0
  93. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_remote.py +0 -0
  94. {queryview-0.0.4 → queryview-0.0.6}/e2e/test_workspaces.py +0 -0
  95. {queryview-0.0.4 → queryview-0.0.6}/frontend/.gitignore +0 -0
  96. {queryview-0.0.4 → queryview-0.0.6}/frontend/README.md +0 -0
  97. {queryview-0.0.4 → queryview-0.0.6}/frontend/eslint.config.js +0 -0
  98. {queryview-0.0.4 → queryview-0.0.6}/frontend/index.html +0 -0
  99. {queryview-0.0.4 → queryview-0.0.6}/frontend/package.json +0 -0
  100. {queryview-0.0.4 → queryview-0.0.6}/frontend/public/favicon.svg +0 -0
  101. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/App.tsx +0 -0
  102. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/DashboardView.tsx +0 -0
  103. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/ExplorerView.tsx +0 -0
  104. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/compactNumber.test.ts +0 -0
  105. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/compactNumber.ts +0 -0
  106. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/connection.ts +0 -0
  107. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/ExportImportControls.tsx +0 -0
  108. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/GitSyncControls.tsx +0 -0
  109. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/Toast.tsx +0 -0
  110. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/WorkspaceSwitcher.tsx +0 -0
  111. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/drivers.ts +0 -0
  112. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/gitsync.test.ts +0 -0
  113. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/gitsync.ts +0 -0
  114. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/promptSuggestions.test.ts +0 -0
  115. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/promptSuggestions.ts +0 -0
  116. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/sessionLock.test.ts +0 -0
  117. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/sessionLock.ts +0 -0
  118. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/workspace.test.ts +0 -0
  119. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/workspace.ts +0 -0
  120. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/yamlio.test.ts +0 -0
  121. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/yamlio.ts +0 -0
  122. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/CellViewModal.tsx +0 -0
  123. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/ComplexCell.tsx +0 -0
  124. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellView.test.ts +0 -0
  125. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellView.ts +0 -0
  126. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellViewYaml.test.ts +0 -0
  127. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellViewYaml.tsx +0 -0
  128. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/complexCells.test.ts +0 -0
  129. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/complexCells.ts +0 -0
  130. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/DashboardFrame.tsx +0 -0
  131. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/srcDoc.test.ts +0 -0
  132. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/srcDoc.ts +0 -0
  133. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/index.ts +0 -0
  134. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/params/queryParams.test.ts +0 -0
  135. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/params/queryParams.ts +0 -0
  136. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/FieldPickers.tsx +0 -0
  137. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/presentation.test.ts +0 -0
  138. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/presentation.ts +0 -0
  139. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/ResultsTable.tsx +0 -0
  140. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/rows.test.ts +0 -0
  141. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/rows.ts +0 -0
  142. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/index.css +0 -0
  143. {queryview-0.0.4 → queryview-0.0.6}/frontend/src/main.tsx +0 -0
  144. {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.app.json +0 -0
  145. {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.json +0 -0
  146. {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.node.json +0 -0
  147. {queryview-0.0.4 → queryview-0.0.6}/frontend/vite.config.ts +0 -0
  148. {queryview-0.0.4 → queryview-0.0.6}/package-lock.json +0 -0
  149. {queryview-0.0.4 → queryview-0.0.6}/package.json +0 -0
  150. {queryview-0.0.4 → queryview-0.0.6}/pyrightconfig.json +0 -0
  151. {queryview-0.0.4 → queryview-0.0.6}/scripts/dev.sh +0 -0
  152. {queryview-0.0.4 → queryview-0.0.6}/scripts/setup.sh +0 -0
  153. {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_clickhouse.sh +0 -0
  154. {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_postgres.sh +0 -0
@@ -3,8 +3,8 @@ description: >-
3
3
  Start a loopback `git daemon` that serves — and accepts pushes into — bare
4
4
  repos under a base path, so e2e tests exercise git sync against a real
5
5
  git:// remote with no auth. Creates an empty bare repo and exports
6
- GIT_SYNC_REMOTE pointing at it; `--detach` keeps the daemon alive across
7
- steps.
6
+ E2E_GIT_REMOTE pointing at it, which the e2e suite applies to the default
7
+ workspace; `--detach` keeps the daemon alive across steps.
8
8
 
9
9
  runs:
10
10
  using: composite
@@ -15,7 +15,7 @@ runs:
15
15
  GIT_DAEMON_BASE="$RUNNER_TEMP/gitsrv"
16
16
  mkdir -p "$GIT_DAEMON_BASE"
17
17
  git init --bare -b main "$GIT_DAEMON_BASE/queryview.git"
18
- echo "GIT_SYNC_REMOTE=git://127.0.0.1:9418/queryview.git" >> "$GITHUB_ENV"
18
+ echo "E2E_GIT_REMOTE=git://127.0.0.1:9418/queryview.git" >> "$GITHUB_ENV"
19
19
  git daemon --reuseaddr --listen=127.0.0.1 --port=9418 \
20
20
  --base-path="$GIT_DAEMON_BASE" --export-all \
21
21
  --enable=receive-pack --detach
@@ -221,7 +221,7 @@ jobs:
221
221
  - name: Smoke-test installed package
222
222
  working-directory: ${{ runner.temp }}
223
223
  run: |
224
- DB_PATH="$PWD/queryview.db" "$GITHUB_WORKSPACE/.venv/bin/queryview" > server.log 2>&1 &
224
+ DATA_DIR="$PWD/qv-data" "$GITHUB_WORKSPACE/.venv/bin/queryview" > server.log 2>&1 &
225
225
  pid=$!
226
226
  for i in $(seq 1 30); do
227
227
  curl -sf http://localhost:8000/api/health > /dev/null && break
@@ -417,6 +417,9 @@ jobs:
417
417
 
418
418
  # Retag the exact multi-arch manifest the smoke gate tested — no
419
419
  # rebuild, no wait-for-PyPI. Tested digests ARE the published digests.
420
+ # The rc tags are left in place: a GHCR package version is a digest,
421
+ # and vX.Y.Z / latest share the rc manifest's digest, so deleting the
422
+ # rc version would delete the release with it.
420
423
  - name: Promote rc manifest
421
424
  run: |
422
425
  REF="ghcr.io/kolodkin/queryview"
@@ -425,19 +428,3 @@ jobs:
425
428
  TAGS+=(-t "${REF}:latest")
426
429
  fi
427
430
  docker buildx imagetools create "${TAGS[@]}" "${REF}:${{ inputs.tag }}-rc"
428
-
429
- # Best-effort: GITHUB_TOKEN may lack package-version delete rights;
430
- # leftover -rc tags are harmless and useful for debugging.
431
- - name: Clean up rc tags
432
- continue-on-error: true
433
- env:
434
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
435
- run: |
436
- for suffix in rc rc-amd64 rc-arm64; do
437
- TAG="${{ inputs.tag }}-${suffix}"
438
- VERSION_ID=$(gh api "/user/packages/container/queryview/versions" --paginate \
439
- --jq ".[] | select(.metadata.container.tags[]? == \"${TAG}\") | .id" | head -n1)
440
- if [ -n "$VERSION_ID" ]; then
441
- gh api -X DELETE "/user/packages/container/queryview/versions/${VERSION_ID}" || true
442
- fi
443
- done
@@ -26,6 +26,7 @@ backend/queryview/static/
26
26
  backend/*.db
27
27
  backend/*.db-*
28
28
  backend/*.key
29
+ backend/db.sqlite*
29
30
 
30
31
  # Editor
31
32
  .DS_Store
@@ -3,6 +3,18 @@
3
3
  1. Avoid `Co-Authored-By: Claude` (and similar Claude/AI attribution) trailers in
4
4
  commit messages and PR bodies.
5
5
 
6
+ # Data Privacy
7
+
8
+ 1. **Never commit tenant data.** Real customer, tenant, or environment
9
+ identifiers (database and table names, hostnames, account or user names,
10
+ query results, screenshots of live data) must not appear in code, tests,
11
+ fixtures, docs, or commit messages.
12
+ 2. **Always anonymize.** When an example needs realistic-looking data, invent
13
+ generic names (e.g. `sales_reporting`, `example.internal`). Rewrite any
14
+ pasted samples before using them.
15
+ 3. If tenant data slips into history, rewrite the commits to remove it and
16
+ force-push the branch rather than adding a follow-up commit.
17
+
6
18
  # Operational Guidelines
7
19
 
8
20
  1. Use `uv run` (e.g. `uv run pytest`, `uv run python ...`), not `python -m` or a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: queryview
3
- Version: 0.0.4
3
+ Version: 0.0.6
4
4
  Summary: QueryView: FastAPI + SQLModel backend and React SPA over the ClickHouse HTTP interface
5
5
  Project-URL: Homepage, https://github.com/kolodkin/queryview
6
6
  Project-URL: Repository, https://github.com/kolodkin/queryview
@@ -16,7 +16,6 @@ Requires-Dist: duckdb>=1.0
16
16
  Requires-Dist: fastapi>=0.110
17
17
  Requires-Dist: httpx>=0.27
18
18
  Requires-Dist: mcp<2,>=1.9
19
- Requires-Dist: platformdirs>=4
20
19
  Requires-Dist: pyyaml>=6
21
20
  Requires-Dist: sqlalchemy[asyncio]>=2.0
22
21
  Requires-Dist: sqlmodel>=0.0.16
@@ -51,9 +50,69 @@ To serve on a different host port, remap it (the container keeps listening on
51
50
  expects to be reached from localhost only, so prefer binding the published port
52
51
  to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
53
52
 
54
- State (the SQLite DB and its encryption key) lives in `/home/queryview`; mount a
55
- volume there to persist it across containers:
56
- `docker run -p 8000:8000 -v queryview-data:/home/queryview ghcr.io/kolodkin/queryview:latest`.
53
+ The command above keeps its state inside the container, so connections and
54
+ workspaces are lost when the container is removed. See
55
+ [Run with Docker](#run-with-docker) for a persistent setup.
56
+
57
+ ## Run with Docker
58
+
59
+ The image runs as the non-root user `queryview` (UID 1000) and sets
60
+ `DATA_DIR=/var/lib/queryview`, so every piece of state lives directly under that
61
+ one directory:
62
+
63
+ | Path | Contents |
64
+ |---|---|
65
+ | `/var/lib/queryview/db.sqlite` | SQLite store: connections, workspaces, queries, dashboards |
66
+ | `/var/lib/queryview/encryption.key` | Key that encrypts stored connection passwords |
67
+ | `/var/lib/queryview/gitsync/` | Local clones used by workspace git sync |
68
+
69
+ Mount a volume at `/var/lib/queryview` and all three persist across container
70
+ restarts, removals and upgrades.
71
+
72
+ ### Persisting it
73
+
74
+ ```bash
75
+ mkdir -p ~/.queryview
76
+ docker run -d --name queryview \
77
+ -p 127.0.0.1:8000:8000 \
78
+ -v ~/.queryview:/var/lib/queryview \
79
+ ghcr.io/kolodkin/queryview:latest
80
+ ```
81
+
82
+ Create the directory first so Docker doesn't create it owned by root. The
83
+ container runs as UID 1000, the first user on most Linux distributions; if
84
+ `id -u` prints something else, add `--user "$(id -u):$(id -g)"` so the
85
+ container writes the files as you.
86
+
87
+ `~/.queryview` is also where a local `uvx queryview` keeps its state, so the
88
+ container and a local run share one database, key and set of clones with no
89
+ extra setup. Don't run both at once: each would serve the same SQLite file.
90
+
91
+ To let Docker own the location instead, swap the path for a named volume,
92
+ `-v queryview-data:/var/lib/queryview`. Docker seeds it from the image, so
93
+ ownership is already right and there is nothing to create up front, but the
94
+ state no longer lines up with a local run.
95
+
96
+ ### Git sync
97
+
98
+ The image ships `git`, so workspace git sync works in the container, and its
99
+ clones live under `/var/lib/queryview/gitsync/`, which the mount above already
100
+ covers. Give the workspace an HTTPS remote carrying a scoped, expiring token —
101
+ [docs/gitsync.md](docs/gitsync.md) has the URL form and what becomes of the
102
+ credential.
103
+
104
+ SSH also works, but nothing in the container can answer a prompt: the key must
105
+ be passphrase-free and `known_hosts` must already list the host. Mount one
106
+ dedicated deploy key, not your whole `.ssh`, readable by UID 1000:
107
+
108
+ ```bash
109
+ docker run -d --name queryview \
110
+ -p 127.0.0.1:8000:8000 \
111
+ -v ~/.queryview:/var/lib/queryview \
112
+ -v ~/.ssh/queryview_deploy:/home/queryview/.ssh/id_ed25519:ro \
113
+ -v ~/.ssh/known_hosts:/home/queryview/.ssh/known_hosts:ro \
114
+ ghcr.io/kolodkin/queryview:latest
115
+ ```
57
116
 
58
117
  ## Layout
59
118
 
@@ -201,10 +260,7 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
201
260
  connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
202
261
  auto-connect are specified in [docs/connect.md](docs/connect.md).
203
262
 
204
- Connections are stored in SQLite. The default location is the platform's
205
- user-data directory — `$XDG_DATA_HOME/queryview/queryview.db` (i.e.
206
- `~/.local/share/queryview/`) on Linux, `~/Library/Application Support/queryview/`
207
- on macOS, `%LOCALAPPDATA%\queryview\` on Windows — overridable with `DB_PATH`.
208
- Alongside it the backend writes a local password-encryption key
209
- (`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
210
- (`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
263
+ All state lives in one data directory, `~/.queryview` on every OS, relocated
264
+ with `DATA_DIR`. Inside are the SQLite store `db.sqlite`, the local
265
+ password-encryption key `encryption.key`, and the workspace git-sync clones
266
+ under `gitsync/`.
@@ -26,9 +26,69 @@ To serve on a different host port, remap it (the container keeps listening on
26
26
  expects to be reached from localhost only, so prefer binding the published port
27
27
  to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
28
28
 
29
- State (the SQLite DB and its encryption key) lives in `/home/queryview`; mount a
30
- volume there to persist it across containers:
31
- `docker run -p 8000:8000 -v queryview-data:/home/queryview ghcr.io/kolodkin/queryview:latest`.
29
+ The command above keeps its state inside the container, so connections and
30
+ workspaces are lost when the container is removed. See
31
+ [Run with Docker](#run-with-docker) for a persistent setup.
32
+
33
+ ## Run with Docker
34
+
35
+ The image runs as the non-root user `queryview` (UID 1000) and sets
36
+ `DATA_DIR=/var/lib/queryview`, so every piece of state lives directly under that
37
+ one directory:
38
+
39
+ | Path | Contents |
40
+ |---|---|
41
+ | `/var/lib/queryview/db.sqlite` | SQLite store: connections, workspaces, queries, dashboards |
42
+ | `/var/lib/queryview/encryption.key` | Key that encrypts stored connection passwords |
43
+ | `/var/lib/queryview/gitsync/` | Local clones used by workspace git sync |
44
+
45
+ Mount a volume at `/var/lib/queryview` and all three persist across container
46
+ restarts, removals and upgrades.
47
+
48
+ ### Persisting it
49
+
50
+ ```bash
51
+ mkdir -p ~/.queryview
52
+ docker run -d --name queryview \
53
+ -p 127.0.0.1:8000:8000 \
54
+ -v ~/.queryview:/var/lib/queryview \
55
+ ghcr.io/kolodkin/queryview:latest
56
+ ```
57
+
58
+ Create the directory first so Docker doesn't create it owned by root. The
59
+ container runs as UID 1000, the first user on most Linux distributions; if
60
+ `id -u` prints something else, add `--user "$(id -u):$(id -g)"` so the
61
+ container writes the files as you.
62
+
63
+ `~/.queryview` is also where a local `uvx queryview` keeps its state, so the
64
+ container and a local run share one database, key and set of clones with no
65
+ extra setup. Don't run both at once: each would serve the same SQLite file.
66
+
67
+ To let Docker own the location instead, swap the path for a named volume,
68
+ `-v queryview-data:/var/lib/queryview`. Docker seeds it from the image, so
69
+ ownership is already right and there is nothing to create up front, but the
70
+ state no longer lines up with a local run.
71
+
72
+ ### Git sync
73
+
74
+ The image ships `git`, so workspace git sync works in the container, and its
75
+ clones live under `/var/lib/queryview/gitsync/`, which the mount above already
76
+ covers. Give the workspace an HTTPS remote carrying a scoped, expiring token —
77
+ [docs/gitsync.md](docs/gitsync.md) has the URL form and what becomes of the
78
+ credential.
79
+
80
+ SSH also works, but nothing in the container can answer a prompt: the key must
81
+ be passphrase-free and `known_hosts` must already list the host. Mount one
82
+ dedicated deploy key, not your whole `.ssh`, readable by UID 1000:
83
+
84
+ ```bash
85
+ docker run -d --name queryview \
86
+ -p 127.0.0.1:8000:8000 \
87
+ -v ~/.queryview:/var/lib/queryview \
88
+ -v ~/.ssh/queryview_deploy:/home/queryview/.ssh/id_ed25519:ro \
89
+ -v ~/.ssh/known_hosts:/home/queryview/.ssh/known_hosts:ro \
90
+ ghcr.io/kolodkin/queryview:latest
91
+ ```
32
92
 
33
93
  ## Layout
34
94
 
@@ -176,10 +236,7 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
176
236
  connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
177
237
  auto-connect are specified in [docs/connect.md](docs/connect.md).
178
238
 
179
- Connections are stored in SQLite. The default location is the platform's
180
- user-data directory — `$XDG_DATA_HOME/queryview/queryview.db` (i.e.
181
- `~/.local/share/queryview/`) on Linux, `~/Library/Application Support/queryview/`
182
- on macOS, `%LOCALAPPDATA%\queryview\` on Windows — overridable with `DB_PATH`.
183
- Alongside it the backend writes a local password-encryption key
184
- (`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
185
- (`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
239
+ All state lives in one data directory, `~/.queryview` on every OS, relocated
240
+ with `DATA_DIR`. Inside are the SQLite store `db.sqlite`, the local
241
+ password-encryption key `encryption.key`, and the workspace git-sync clones
242
+ under `gitsync/`.
@@ -1,8 +1,8 @@
1
1
  """Shared fixtures for backend (non-e2e) tests.
2
2
 
3
- Redirects the SQLite store and encryption-key file to a per-session tempdir so
4
- tests don't touch the real `backend/queryview.db`, and resets the lazy
5
- module-level engine/schema state in `queryview.connect` before tests run."""
3
+ Redirects the data directory to a per-session tempdir so tests don't touch the
4
+ real one, and resets the lazy module-level engine/schema state in
5
+ `queryview.connect` before tests run."""
6
6
 
7
7
  from __future__ import annotations
8
8
 
@@ -14,7 +14,18 @@ import pytest
14
14
 
15
15
 
16
16
  @pytest.fixture
17
- def git_env(tmp_path, monkeypatch):
17
+ def clone_base(tmp_path, monkeypatch) -> Path:
18
+ """Redirect every workspace's sync clone into this test's tmpdir, so clones
19
+ never land in the session data dir and can't leak between tests."""
20
+ from queryview import gitsync
21
+
22
+ base = tmp_path / "clones"
23
+ monkeypatch.setattr(gitsync, "_clone_base", lambda: base)
24
+ return base
25
+
26
+
27
+ @pytest.fixture
28
+ def git_env(tmp_path, clone_base):
18
29
  """A local bare repo as the default workspace's git-sync remote + a fresh
19
30
  per-workspace clone base dir. Resets the default workspace to 'no remote'
20
31
  on teardown so unconfigured-state tests stay valid."""
@@ -28,7 +39,6 @@ def git_env(tmp_path, monkeypatch):
28
39
  check=True,
29
40
  capture_output=True,
30
41
  )
31
- monkeypatch.setenv("GIT_SYNC_DIR", str(tmp_path / "clones"))
32
42
  asyncio.run(update_workspace(DEFAULT_WORKSPACE, remote=str(remote)))
33
43
  yield remote
34
44
  asyncio.run(update_workspace(DEFAULT_WORKSPACE, remote=None))
@@ -69,13 +79,7 @@ def default_ws_id() -> int:
69
79
  @pytest.fixture(scope="session", autouse=True)
70
80
  def _isolated_db(tmp_path_factory: pytest.TempPathFactory):
71
81
  tmp: Path = tmp_path_factory.mktemp("qv_backend_tests")
72
- os.environ["DB_PATH"] = str(tmp / "test.db")
73
- os.environ["DB_KEY_PATH"] = str(tmp / "test.db.key")
74
-
75
- # The workspaces migration seeds the default workspace from GIT_SYNC_*;
76
- # tests control that per-test (monkeypatch), never from ambient env.
77
- os.environ.pop("GIT_SYNC_REMOTE", None)
78
- os.environ.pop("GIT_SYNC_BRANCH", None)
82
+ os.environ["DATA_DIR"] = str(tmp)
79
83
 
80
84
  # Reset the lazy globals so the next DB touch picks up the new paths.
81
85
  import queryview.connect as _c
@@ -14,7 +14,6 @@ from dataclasses import dataclass
14
14
  from pathlib import Path
15
15
  from typing import TYPE_CHECKING, Any, ClassVar
16
16
 
17
- import platformdirs
18
17
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
19
18
  from sqlalchemy.ext.asyncio import create_async_engine
20
19
  from sqlmodel import Field, SQLModel, col, select
@@ -40,15 +39,18 @@ class Connection(SQLModel, table=True):
40
39
  last_active_at: int # unix ms; the max is the "latest active"
41
40
 
42
41
 
42
+ def _data_dir() -> Path:
43
+ """Every piece of state lives here: DATA_DIR, else `~/.queryview` — the same
44
+ path on every OS, and the image's mount point, so a local run and a container
45
+ share state. Deliberately not package-relative: that put the DB in
46
+ site-packages, which is uv's disposable cache under `uvx`."""
47
+ env = os.environ.get("DATA_DIR")
48
+ return Path(env) if env else Path.home() / ".queryview"
49
+
50
+
43
51
  def _db_path() -> Path:
44
- """The SQLite store's path: DB_PATH, else the platform's user-data dir
45
- (`$XDG_DATA_HOME/queryview` on Linux, `Application Support` on macOS,
46
- `%LOCALAPPDATA%` on Windows). Deliberately not package-relative — that put
47
- the DB in site-packages, which is uv's disposable cache under `uvx`."""
48
- env = os.environ.get("DB_PATH")
49
- if env:
50
- return Path(env)
51
- return Path(platformdirs.user_data_dir("queryview")) / "queryview.db"
52
+ """The SQLite store: a fixed name in the data dir — move the dir, not it."""
53
+ return _data_dir() / "db.sqlite"
52
54
 
53
55
 
54
56
  _engine = None
@@ -67,7 +69,7 @@ def _engine_for_db():
67
69
  def _alembic_config() -> Config:
68
70
  """Alembic Config built in code (not a cwd alembic.ini) so migrations run from
69
71
  any directory and from the packaged wheel. Points at the package's migrations
70
- dir and injects a *sync* SQLite URL for the current DB_PATH."""
72
+ dir and injects a *sync* SQLite URL for the current data dir."""
71
73
  from alembic.config import Config
72
74
 
73
75
  cfg = Config()
@@ -85,9 +87,9 @@ async def _ensure_schema() -> None:
85
87
  return
86
88
  from alembic import command
87
89
 
88
- # SQLite won't create a missing parent, and the user-data dir doesn't exist
89
- # on a fresh install; the key file and git-sync clones land here too.
90
- _db_path().parent.mkdir(parents=True, exist_ok=True)
90
+ # SQLite won't create a missing parent, and the data dir doesn't exist on a
91
+ # fresh install; the key file and git-sync clones land here too.
92
+ _data_dir().mkdir(parents=True, exist_ok=True)
91
93
  command.upgrade(_alembic_config(), "head")
92
94
  _schema_ready = True
93
95
 
@@ -99,8 +101,7 @@ async def _ensure_schema() -> None:
99
101
 
100
102
 
101
103
  def _key_path() -> Path:
102
- env = os.environ.get("DB_KEY_PATH")
103
- return Path(env) if env else Path(f"{_db_path()}.key")
104
+ return _data_dir() / "encryption.key"
104
105
 
105
106
 
106
107
  _key: bytes | None = None
@@ -11,9 +11,11 @@ import os
11
11
  import shutil
12
12
  from pathlib import Path
13
13
  from typing import Any
14
+ from urllib.parse import unquote, urlsplit, urlunsplit
14
15
 
15
16
  import yaml
16
17
 
18
+ from .connect import _data_dir
17
19
  from .workspaces import WorkspaceRec
18
20
  from .yamlio import YamlIOError, dashboard_from_data, dump_yaml, query_from_data, query_to_data, slug
19
21
 
@@ -86,8 +88,7 @@ def dashboard_from_files(files: dict[str, str]) -> dict[str, Any]:
86
88
 
87
89
 
88
90
  # --- Configuration ---------------------------------------------------------
89
- # Runtime config lives on the workspace row (GIT_SYNC_REMOTE/GIT_SYNC_BRANCH
90
- # are read once, by the workspaces migration, to seed the default workspace).
91
+ # All config lives on the workspace row; there is no environment fallback.
91
92
 
92
93
 
93
94
  def _require_remote(ws: WorkspaceRec) -> str:
@@ -96,15 +97,15 @@ def _require_remote(ws: WorkspaceRec) -> str:
96
97
  return ws.remote
97
98
 
98
99
 
100
+ def _clone_base() -> Path:
101
+ """Where every workspace's sync clone lives, inside the data dir."""
102
+ return _data_dir() / "gitsync"
103
+
104
+
99
105
  def _workdir(ws: WorkspaceRec) -> Path:
100
106
  """This workspace's clone: {base}/{workspace id}. Keyed by id so renaming
101
- a workspace never orphans its clone. GIT_SYNC_DIR overrides the base."""
102
- env = os.environ.get("GIT_SYNC_DIR")
103
- if env:
104
- return Path(env) / str(ws.id)
105
- from .connect import _db_path
106
-
107
- return Path(f"{_db_path()}.gitsync") / str(ws.id)
107
+ a workspace never orphans its clone."""
108
+ return _clone_base() / str(ws.id)
108
109
 
109
110
 
110
111
  def configured(ws: WorkspaceRec) -> bool:
@@ -129,15 +130,73 @@ def _lock(ws: WorkspaceRec) -> asyncio.Lock:
129
130
  return lock
130
131
 
131
132
 
132
- async def _git(*args: str, cwd: Path | None = None) -> str:
133
- proc = await asyncio.create_subprocess_exec(
134
- "git",
135
- *args,
136
- cwd=str(cwd) if cwd else None,
137
- stdout=asyncio.subprocess.PIPE,
138
- stderr=asyncio.subprocess.PIPE,
139
- )
140
- out, err = await proc.communicate()
133
+ # Seconds before a git invocation is killed; only network calls come close.
134
+ _GIT_TIMEOUT_S = 120
135
+
136
+ # Feeds the credential back per invocation from the child's environment, so it
137
+ # reaches neither the repo's config nor our argv. Git calls a helper with "get",
138
+ # "store" or "erase"; answering only "get" avoids writing it anywhere.
139
+ _CREDENTIAL_HELPER = (
140
+ '!f() { test "$1" = get && printf "username=%s\\npassword=%s\\n" "$QV_GIT_USERNAME" "$QV_GIT_PASSWORD"; }; f'
141
+ )
142
+
143
+
144
+ def _split_credential(url: str) -> tuple[str, tuple[str, str] | None]:
145
+ """Split an http(s) URL's credential from the URL, so the URL can be handed
146
+ to git (and persisted in the clone's config) without it.
147
+
148
+ Only http(s) userinfo is a secret: `git@host:path` and `ssh://git@host` name
149
+ an SSH login, so those are returned untouched."""
150
+ parts = urlsplit(url)
151
+ if parts.scheme not in ("http", "https") or not (parts.username or parts.password):
152
+ return url, None
153
+ host = parts.hostname or ""
154
+ if parts.port:
155
+ host = f"{host}:{parts.port}"
156
+ sanitized = urlunsplit((parts.scheme, host, parts.path, parts.query, parts.fragment))
157
+ return sanitized, (unquote(parts.username or ""), unquote(parts.password or ""))
158
+
159
+
160
+ def _credential(ws: WorkspaceRec) -> tuple[str, str] | None:
161
+ """This workspace's git credential, if its remote embeds one."""
162
+ return _split_credential(_require_remote(ws))[1]
163
+
164
+
165
+ async def _git(*args: str, cwd: Path | None = None, credential: tuple[str, str] | None = None) -> str:
166
+ """Run one git command, never interactively: stdin is closed and any prompt
167
+ (terminal, SSH passphrase, unknown host) becomes a failure rather than a
168
+ blocked request. A credential, when passed, arrives via the environment."""
169
+ env = dict(os.environ)
170
+ env["GIT_TERMINAL_PROMPT"] = "0"
171
+ env["GIT_SSH_COMMAND"] = f"{os.environ.get('GIT_SSH_COMMAND', 'ssh')} -o BatchMode=yes"
172
+ if credential is not None:
173
+ env["QV_GIT_USERNAME"], env["QV_GIT_PASSWORD"] = credential
174
+ # Config through the environment, not `-c` flags, so argv stays exactly
175
+ # what the caller passed. The empty first value clears any helper
176
+ # inherited from system/global config, leaving ours the only one asked.
177
+ env["GIT_CONFIG_COUNT"] = "2"
178
+ env["GIT_CONFIG_KEY_0"], env["GIT_CONFIG_VALUE_0"] = "credential.helper", ""
179
+ env["GIT_CONFIG_KEY_1"], env["GIT_CONFIG_VALUE_1"] = "credential.helper", _CREDENTIAL_HELPER
180
+ try:
181
+ proc = await asyncio.create_subprocess_exec(
182
+ "git",
183
+ *args,
184
+ cwd=str(cwd) if cwd else None,
185
+ env=env,
186
+ stdin=asyncio.subprocess.DEVNULL,
187
+ stdout=asyncio.subprocess.PIPE,
188
+ stderr=asyncio.subprocess.PIPE,
189
+ )
190
+ except FileNotFoundError as e:
191
+ # No `git` on PATH: a deployment problem, not a repo problem — say so
192
+ # instead of surfacing a bare FileNotFoundError as a 500.
193
+ raise GitSyncError("git is not installed on the server") from e
194
+ try:
195
+ out, err = await asyncio.wait_for(proc.communicate(), _GIT_TIMEOUT_S)
196
+ except TimeoutError as e:
197
+ proc.kill()
198
+ await proc.wait()
199
+ raise GitSyncError(f"git {args[0]} timed out after {_GIT_TIMEOUT_S}s") from e
141
200
  if proc.returncode != 0:
142
201
  tail = err.decode("utf-8", "replace").strip()[-500:]
143
202
  raise GitSyncError(f"git {args[0]} failed: {tail}")
@@ -147,19 +206,22 @@ async def _git(*args: str, cwd: Path | None = None) -> str:
147
206
  async def _ensure_repo(ws: WorkspaceRec) -> Path:
148
207
  """The workspace's sync clone, cloning (or, for an empty remote, init +
149
208
  remote add) on first use. Idempotent."""
150
- remote, branch, wd = _require_remote(ws), ws.branch, _workdir(ws)
209
+ branch, wd = ws.branch, _workdir(ws)
151
210
  if (wd / ".git").exists():
152
211
  return wd
153
212
  wd.parent.mkdir(parents=True, exist_ok=True)
213
+ # The clone records its remote in .git/config, so only the credential-free
214
+ # URL goes to git as an argument; the credential travels out of band.
215
+ remote, cred = _split_credential(_require_remote(ws))
154
216
  try:
155
- await _git("clone", "--branch", branch, remote, str(wd))
217
+ await _git("clone", "--branch", branch, remote, str(wd), credential=cred)
156
218
  except GitSyncError as clone_err:
157
219
  # Clone can fail either because the branch genuinely doesn't exist yet
158
220
  # on an empty remote (the only case we should paper over with a local
159
221
  # init) or because the remote itself is unreachable/misconfigured. Ask
160
222
  # the remote directly to tell the two apart.
161
223
  try:
162
- heads = await _git("ls-remote", "--heads", remote, branch)
224
+ heads = await _git("ls-remote", "--heads", remote, branch, credential=cred)
163
225
  except GitSyncError as probe_err:
164
226
  raise GitSyncError(f"git remote unreachable: {probe_err}") from probe_err
165
227
  if heads.strip():
@@ -177,7 +239,7 @@ async def _ensure_repo(ws: WorkspaceRec) -> Path:
177
239
  async def _origin_head(wd: Path, ws: WorkspaceRec) -> str | None:
178
240
  """Fetch, then the remote branch ref if it exists (None on an empty remote).
179
241
  Network/auth failures raise."""
180
- await _git("fetch", "origin", cwd=wd)
242
+ await _git("fetch", "origin", cwd=wd, credential=_credential(ws))
181
243
  ref = f"origin/{ws.branch}"
182
244
  try:
183
245
  await _git("rev-parse", "--verify", ref, cwd=wd)
@@ -257,7 +319,7 @@ async def store(
257
319
  return {"committed": False, "sha": None, "message": "no changes"}
258
320
  label = f"{conn_type}/{name}" if kind == "query" else name
259
321
  await _git("commit", "-m", message or f"store {kind} {label}", cwd=wd)
260
- await _git("push", "origin", ws.branch, cwd=wd)
322
+ await _git("push", "origin", ws.branch, cwd=wd, credential=_credential(ws))
261
323
  sha = (await _git("rev-parse", "HEAD", cwd=wd)).strip()
262
324
  return {"committed": True, "sha": sha, "message": "stored"}
263
325
 
@@ -21,7 +21,7 @@ target_metadata = SQLModel.metadata
21
21
 
22
22
 
23
23
  def _url() -> str:
24
- """Prefer the URL the app injects; fall back to DB_PATH so the dev CLI
24
+ """Prefer the URL the app injects; fall back to the data dir so the dev CLI
25
25
  (`alembic ...`) targets the same SQLite file the app would."""
26
26
  from queryview.connect import _db_path
27
27
 
@@ -1,8 +1,8 @@
1
1
  """workspaces: per-workspace git sync — workspaces table, entity workspace_id
2
2
 
3
- Seeds a 'default' workspace from GIT_SYNC_REMOTE/GIT_SYNC_BRANCH (read once
4
- here; runtime config lives in the table from now on) and backfills all
5
- existing predefined queries and dashboards into it. Name uniqueness becomes
3
+ Creates the 'default' workspace (its remote is configured through the UI/API
4
+ afterwards) and backfills all existing predefined queries and dashboards into
5
+ it. Name uniqueness becomes
6
6
  per-workspace.
7
7
 
8
8
  Revision ID: c7d8e9f0a1b2
@@ -13,7 +13,6 @@ Create Date: 2026-07-10
13
13
 
14
14
  from __future__ import annotations
15
15
 
16
- import os
17
16
  from collections.abc import Sequence
18
17
 
19
18
  import sqlalchemy as sa
@@ -37,17 +36,12 @@ def upgrade() -> None:
37
36
  )
38
37
  op.create_index(op.f("ix_workspaces_name"), "workspaces", ["name"], unique=True)
39
38
 
40
- # Seed 'default' from the legacy env config. The app's key loader is
41
- # imported here, mirroring the connection-config-blob migration.
42
- from queryview.connect import _encrypt_str
43
-
44
- env_remote = os.environ.get("GIT_SYNC_REMOTE")
45
- remote = _encrypt_str(env_remote) if env_remote else None
46
- branch = os.environ.get("GIT_SYNC_BRANCH") or "main"
39
+ # No remote yet: git sync stays disabled until one is set through the
40
+ # UI/API, which encrypts it on the way in.
47
41
  conn = op.get_bind()
48
42
  conn.execute(
49
- sa.text("INSERT INTO workspaces (name, remote, branch) VALUES (:n, :r, :b)"),
50
- {"n": "default", "r": remote, "b": branch},
43
+ sa.text("INSERT INTO workspaces (name, remote, branch) VALUES (:n, NULL, :b)"),
44
+ {"n": "default", "b": "main"},
51
45
  )
52
46
  default_id = conn.execute(sa.text("SELECT id FROM workspaces WHERE name = 'default'")).scalar()
53
47