diff --git a/AGENTS.md b/AGENTS.md index a72adb36..b69cfa06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # Repository Guidelines ## Project Structure & Module Organization -`cmd/silo` contains the main server entrypoint. Backend code lives in `internal/`, organized by domain (`api`, `catalog`, `metadata`, `playback`, `scanner`, `jellycompat`, etc.); keep new code in the package that owns the behavior instead of creating catch-all helpers. Database changes belong in `migrations/` as paired numbered `.up.sql` and `.down.sql` files. The React frontend lives in `web/src/`, with feature code split across `components/`, `pages/`, `hooks/`, `player/`, and `lib/`. Reference material belongs in `docs/architecture/` or `docs/superpowers/{specs,plans}/`; ad hoc SQL helpers live in `scripts/`. +`cmd/silo` contains the main server entrypoint. Backend code lives in `internal/`, organized by domain (`api`, `catalog`, `metadata`, `playback`, `scanner`, `jellycompat`, etc.); keep new code in the package that owns the behavior instead of creating catch-all helpers. Database changes belong in `migrations/sql/` as Goose SQL migrations. Legacy converted migrations intentionally keep their original numeric versions so existing `schema_versions` rows can bootstrap cleanly into Goose. New migrations must use timestamped filenames created with `make migrate-create NAME=add_thing`; do not run `goose fix` or create paired `.up.sql` / `.down.sql` files. The React frontend lives in `web/src/`, with feature code split across `components/`, `pages/`, `hooks/`, `player/`, and `lib/`. Reference material belongs in `docs/architecture/` or `docs/superpowers/{specs,plans}/`; ad hoc SQL helpers live in `scripts/`. When creating or editing `docs/superpowers/specs/` or `docs/superpowers/plans/`, never include local absolute filesystem paths or transient worktree IDs. Use repository-relative paths and wording like "Commands assume the repository root is the cwd." @@ -53,6 +53,7 @@ Use the checked-in `Makefile` for the common paths: - `make dev-frontend`: start the Vite dev server with HMR - `make dev-proxy` / `make dev-transcode`: run standalone worker modes - `make lint`: run `golangci-lint` and frontend ESLint +- `make migrate-status` / `make migrate-up`: inspect or apply Goose migrations through Silo's legacy-safe bootstrapping runner Run before opening a merge request: diff --git a/Makefile b/Makefile index 60a2eb4c..907b81d5 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,13 @@ -.PHONY: frontend build dev-frontend dev-backend dev-proxy dev-transcode lint clean jellyfin-web-bundle migrate-continuum-check verify-local-paths install-hooks +.PHONY: frontend build dev-frontend dev-backend dev-proxy dev-transcode lint clean jellyfin-web-bundle migrate-continuum-check verify-local-paths install-hooks migrate-create migrate-validate migrate-status migrate-up GIT_COMMON_DIR := $(strip $(shell git rev-parse --git-common-dir 2>/dev/null)) MAIN_CHECKOUT_ROOT := $(if $(GIT_COMMON_DIR),$(abspath $(GIT_COMMON_DIR)/..)) SHARED_MAKEFILE_LOCAL := $(if $(GIT_COMMON_DIR),$(abspath $(GIT_COMMON_DIR)/../Makefile.local)) DEFAULT_PLUGIN_SDK_DIR := $(abspath ../silo-plugin-sdk) SHARED_PLUGIN_SDK_DIR := $(if $(MAIN_CHECKOUT_ROOT),$(abspath $(MAIN_CHECKOUT_ROOT)/../silo-plugin-sdk)) +GOOSE := go run github.com/pressly/goose/v3/cmd/goose@v3.27.1 +GOOSE_DIR := migrations/sql +ENV_FILE ?= .env ifneq ($(wildcard $(DEFAULT_PLUGIN_SDK_DIR)),) DEV_PLUGIN_SDK_DIR ?= $(DEFAULT_PLUGIN_SDK_DIR) @@ -54,6 +57,23 @@ lint: verify-local-paths: scripts/check-local-path-leaks.sh +# Create a timestamped Goose SQL migration. Usage: make migrate-create NAME=add_thing +migrate-create: + @if [ -z "$(NAME)" ]; then echo "usage: make migrate-create NAME=add_thing"; exit 1; fi + $(GOOSE) -dir $(GOOSE_DIR) create "$(NAME)" sql + +# Validate Goose migration annotations and SQL parsing without touching a database. +migrate-validate: + $(GOOSE) -dir $(GOOSE_DIR) validate + +# Show Goose migration status through Silo's bootstrapping runner. +migrate-status: + go run ./cmd/silo/ --env "$(ENV_FILE)" --migrate-status + +# Apply pending Goose migrations through Silo's bootstrapping runner. +migrate-up: + go run ./cmd/silo/ --env "$(ENV_FILE)" --migrate-only + # Install repo-local git hooks for this checkout/worktree. install-hooks: @existing="$$(git config --local core.hooksPath 2>/dev/null || true)"; \ diff --git a/README.md b/README.md index 930e532b..95357042 100644 --- a/README.md +++ b/README.md @@ -178,8 +178,36 @@ Technical notes: | `make dev-backend` | Run Go backend (integrated mode) | | `make dev-proxy` | Run a standalone proxy node | | `make dev-transcode` | Run a standalone transcode node | +| `make migrate-create NAME=add_thing` | Create a timestamped Goose SQL migration | +| `make migrate-validate` | Validate Goose migration files without touching a database | +| `make migrate-status` | Show Goose migration status using Silo's bootstrapping runner | +| `make migrate-up` | Apply pending Goose migrations using Silo's bootstrapping runner | | `make clean` | Remove build artifacts | +### Database Migrations + +PostgreSQL schema migrations are managed by Goose. Migration SQL files live in +`migrations/sql/` and use Goose annotations. Converted legacy migrations keep +their original numeric versions so existing `schema_versions` rows can bootstrap +cleanly into Goose without replaying old SQL. New migrations should be created +with timestamped filenames: + +```sh +make migrate-create NAME=add_thing +make migrate-validate +``` + +Do not run `goose fix`; timestamped migrations are the repository policy because +they avoid version collisions across parallel PRs. The existing `001`-style +files are historical compatibility records, not the naming pattern for new work. +Runtime migrations are applied by the integrated/API server only. Proxy and +transcode modes never mutate schema. +For existing installs, use `make migrate-status` and `make migrate-up` rather +than invoking the Goose CLI directly; those targets copy legacy +`schema_versions` rows into `public.goose_db_version` under the migration lock +before reading or applying migrations. Set `ENV_FILE=path/to/.env` when the +database URL should be read from a non-default env file. + ### Running Tests ```sh @@ -285,5 +313,5 @@ internal/ scanner/ Media file discovery and FFProbe worker/ Background jobs (scan, match, reconcile) web/ React + TypeScript frontend (Vite, Tailwind, shadcn/ui) -migrations/ PostgreSQL schema migrations +migrations/sql/ Goose-managed PostgreSQL schema migrations ``` diff --git a/cmd/silo/main.go b/cmd/silo/main.go index eeabac56..00430c35 100644 --- a/cmd/silo/main.go +++ b/cmd/silo/main.go @@ -250,6 +250,8 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon func main() { envFile := flag.String("env", ".env", "path to .env bootstrap file") + migrateOnly := flag.Bool("migrate-only", false, "apply database migrations and exit") + migrateStatus := flag.Bool("migrate-status", false, "show database migration status and exit") flag.Parse() ctx := context.Background() @@ -269,12 +271,48 @@ func main() { defer pool.Close() slog.Info("connected to PostgreSQL") + if *migrateStatus { + migCtx, migCancel := context.WithTimeout(ctx, 5*time.Minute) + statuses, statusErr := database.MigrationStatuses(migCtx, pool, migrations.FS, "sql") + migCancel() + if statusErr != nil { + log.Fatalf("failed to read migration status: %v", statusErr) + } + + fmt.Printf("%-8s %8s %-25s %s\n", "STATE", "VERSION", "APPLIED_AT", "MIGRATION") + for _, status := range statuses { + appliedAt := "-" + if !status.AppliedAt.IsZero() { + appliedAt = status.AppliedAt.UTC().Format(time.RFC3339) + } + source := status.Source + if source != "" { + source = filepath.Base(source) + } else { + source = "-" + } + fmt.Printf("%-8s %8d %-25s %s\n", status.State, status.Version, appliedAt, source) + } + return + } + + if *migrateOnly { + migCtx, migCancel := context.WithTimeout(ctx, 5*time.Minute) + migErr := database.RunMigrations(migCtx, pool, migrations.FS, "sql") + migCancel() + if migErr != nil { + log.Fatalf("failed to run migrations: %v", migErr) + } + slog.Info("database migrations applied") + return + } + // Run migrations only for integrated/api modes. Proxy and transcode nodes // should never alter the schema — they may scale independently and would // race or apply migrations before the primary node is deliberately upgraded. if bc.Mode == "integrated" || bc.Mode == "api" || bc.Mode == "" { migCtx, migCancel := context.WithTimeout(ctx, 5*time.Minute) - if migErr := database.RunMigrations(migCtx, pool, migrations.FS, "."); migErr != nil { + if migErr := database.RunMigrations(migCtx, pool, migrations.FS, "sql"); migErr != nil { migCancel() log.Fatalf("failed to run migrations: %v", migErr) } diff --git a/docs/superpowers/plans/2026-06-05-autoscan-source-labels.md b/docs/superpowers/plans/2026-06-05-autoscan-source-labels.md index b8bfd9de..93b05d26 100644 --- a/docs/superpowers/plans/2026-06-05-autoscan-source-labels.md +++ b/docs/superpowers/plans/2026-06-05-autoscan-source-labels.md @@ -979,7 +979,7 @@ Expected: `healthy`. ```bash docker compose exec -T postgres psql -U silo -d silo -c \ - "SELECT version, filename FROM schema_versions WHERE version=174;" + "SELECT version_id FROM public.goose_db_version WHERE version_id=174 AND is_applied;" docker compose exec -T postgres psql -U silo -d silo -c \ "SELECT column_name FROM information_schema.columns WHERE table_name='autoscan_sources' AND column_name='label';" ``` diff --git a/go.mod b/go.mod index 6c3acd65..7ccd3c6a 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,10 @@ require ( github.com/joho/godotenv v1.5.1 github.com/oklog/ulid/v2 v2.1.0 github.com/pgvector/pgvector-go v0.3.0 + github.com/pressly/goose/v3 v3.27.1 go.n16f.net/thumbhash v1.1.0 golang.org/x/image v0.39.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) @@ -38,12 +39,16 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/yamux v0.1.2 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/oklog/run v1.1.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.53.0 // indirect ) @@ -67,15 +72,14 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.20.1 // indirect github.com/robfig/cron/v3 v3.0.1 github.com/sony/sonyflake v1.3.0 - go.opentelemetry.io/otel v1.41.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/sync v0.20.0 golang.org/x/text v0.36.0 golang.org/x/time v0.14.0 - google.golang.org/grpc v1.79.3 + google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 ) diff --git a/go.sum b/go.sum index 1d4f989e..2af3be7e 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,7 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -45,6 +46,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= @@ -95,8 +98,8 @@ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -112,12 +115,16 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= 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 v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= @@ -127,22 +134,28 @@ github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2 github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4= +github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sony/sonyflake v1.3.0 h1:tiB4Dlp0lnmKp/h6BLXA14P8Qi+LYS9+0QRpcrKHvg4= github.com/sony/sonyflake v1.3.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -175,20 +188,22 @@ go.n16f.net/thumbhash v1.1.0 h1:aBEvuAd4yiwzeQ7Sm4BZoHJYbrQ1ewjrmrRlCE79snk= go.n16f.net/thumbhash v1.1.0/go.mod h1:mo9pP7WtfdV9ojIamGFR/Vc0PaPA2l0CUtmYQf/SweU= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= @@ -204,19 +219,18 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -233,3 +247,11 @@ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= +modernc.org/libc v1.72.1 h1:db1xwJ6u1kE3KHTFTTbe2GCrczHPKzlURP0aDC4NGD0= +modernc.org/libc v1.72.1/go.mod h1:HRMiC/PhPGLIPM7GzAFCbI+oSgE3dhZ8FWftmRrHVlY= +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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U= +modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= diff --git a/internal/catalogseed/service.go b/internal/catalogseed/service.go index e3838dad..b62bea14 100644 --- a/internal/catalogseed/service.go +++ b/internal/catalogseed/service.go @@ -634,7 +634,7 @@ func (s *Service) exportFolders(ctx context.Context, libraryIDs []int) ([]Librar func (s *Service) schemaVersion(ctx context.Context) (int, error) { var version int - if err := s.pool.QueryRow(ctx, "SELECT COALESCE(MAX(version), 0) FROM schema_versions").Scan(&version); err != nil { + if err := s.pool.QueryRow(ctx, "SELECT COALESCE(MAX(version_id), 0) FROM public.goose_db_version").Scan(&version); err != nil { return 0, fmt.Errorf("loading schema version: %w", err) } return version, nil diff --git a/internal/database/migrate.go b/internal/database/migrate.go index dee3917f..5d19893f 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -2,233 +2,244 @@ package database import ( "context" + "database/sql" "fmt" "io/fs" - "path/filepath" - "sort" - "strconv" - "strings" "time" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" + "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + "github.com/pressly/goose/v3/lock" ) -// MigrationFile represents a parsed migration file with its version number, -// filename, and SQL content. -type MigrationFile struct { - Version int - Filename string - SQL string -} +const ( + schemaMigrationsLockID int64 = 8_034_219_741 + gooseVersionTable = "public.goose_db_version" +) -const schemaMigrationsLockID int64 = 8_034_219_741 - -type migrationDB interface { - Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) - Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) - BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) -} - -// RunMigrations applies all pending .up.sql migrations from the given embed.FS. -// It creates a schema_versions table to track which migrations have been applied, -// parses migration filenames to extract version numbers, and applies each -// pending migration within its own transaction. -// -// The dir parameter specifies the subdirectory within the fs.FS that contains -// the migration files. -func RunMigrations(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS, dir string) (err error) { - conn, err := pool.Acquire(ctx) +// RunMigrations applies all pending Goose migrations from fsys/dir. +func RunMigrations(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS, dir string) error { + provider, err := newMigrationProvider(pool, fsys, dir) if err != nil { - return fmt.Errorf("acquiring migration connection: %w", err) + return err } - defer conn.Release() + defer provider.Close() - if err := lockMigrations(ctx, conn); err != nil { - return fmt.Errorf("acquiring migration lock: %w", err) + if _, err := provider.Up(ctx); err != nil { + return fmt.Errorf("running goose migrations: %w", err) } - defer func() { - if unlockErr := unlockMigrations(conn); unlockErr != nil { - if err != nil { - err = fmt.Errorf("%w; releasing migration lock: %v", err, unlockErr) - return - } - err = fmt.Errorf("releasing migration lock: %w", unlockErr) - } - }() - - if err := ensureSchemaVersionsTable(ctx, conn); err != nil { - return fmt.Errorf("ensuring schema_versions table: %w", err) - } - - migrations, err := loadMigrations(fsys, dir) - if err != nil { - return fmt.Errorf("loading migrations: %w", err) - } - - applied, err := getAppliedVersions(ctx, conn) - if err != nil { - return fmt.Errorf("getting applied versions: %w", err) - } - - for _, m := range migrations { - if applied[m.Version] { - continue - } - - if err := applyMigration(ctx, conn, m); err != nil { - return fmt.Errorf("applying migration %s (version %d): %w", m.Filename, m.Version, err) - } - applied[m.Version] = true - } - return nil } -func lockMigrations(ctx context.Context, db migrationDB) error { - _, err := db.Exec(ctx, "SELECT pg_advisory_lock($1)", schemaMigrationsLockID) - return err +// MigrationStatus describes a migration source and whether Goose has applied it. +type MigrationStatus struct { + Version int64 + Source string + State string + AppliedAt time.Time } -func unlockMigrations(db migrationDB) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := db.Exec(ctx, "SELECT pg_advisory_unlock($1)", schemaMigrationsLockID) - return err -} - -// ensureSchemaVersionsTable creates the schema_versions table if it does not -// already exist. -func ensureSchemaVersionsTable(ctx context.Context, db migrationDB) error { - _, err := db.Exec(ctx, ` - CREATE TABLE IF NOT EXISTS public.schema_versions ( - version INT PRIMARY KEY, - filename TEXT NOT NULL, - applied_at TIMESTAMPTZ NOT NULL DEFAULT now() - ) - `) - return err -} - -// loadMigrations reads all .up.sql files from the specified directory within -// the filesystem and returns them sorted by version number. -func loadMigrations(fsys fs.FS, dir string) ([]MigrationFile, error) { - entries, err := fs.ReadDir(fsys, dir) - if err != nil { - return nil, fmt.Errorf("reading migrations directory %q: %w", dir, err) - } - - var migrations []MigrationFile - seenVersions := make(map[int]string) - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - name := entry.Name() - if !strings.HasSuffix(name, ".up.sql") { - continue - } - - version, err := parseVersion(name) - if err != nil { - return nil, fmt.Errorf("parsing version from %q: %w", name, err) - } - if existing, ok := seenVersions[version]; ok { - return nil, fmt.Errorf("duplicate migration version %d: %s and %s", version, existing, name) - } - seenVersions[version] = name - - content, err := fs.ReadFile(fsys, filepath.Join(dir, name)) - if err != nil { - return nil, fmt.Errorf("reading migration file %q: %w", name, err) - } - - migrations = append(migrations, MigrationFile{ - Version: version, - Filename: name, - SQL: string(content), - }) - } - - sort.Slice(migrations, func(i, j int) bool { - return migrations[i].Version < migrations[j].Version - }) - - return migrations, nil -} - -// parseVersion extracts the numeric version prefix from a migration filename. -// For example, "001_initial_schema.up.sql" returns 1. -func parseVersion(filename string) (int, error) { - // Split on the first underscore to get the version prefix. - parts := strings.SplitN(filename, "_", 2) - if len(parts) < 2 { - return 0, fmt.Errorf("migration filename %q must be in format NNN_description.up.sql", filename) - } - - version, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, fmt.Errorf("invalid version number in %q: %w", filename, err) - } - - if version <= 0 { - return 0, fmt.Errorf("version number must be positive, got %d in %q", version, filename) - } - - return version, nil -} - -// getAppliedVersions returns a set of migration versions that have already -// been applied. -func getAppliedVersions(ctx context.Context, db migrationDB) (map[int]bool, error) { - rows, err := db.Query(ctx, "SELECT version FROM public.schema_versions") +// MigrationStatuses returns Goose status using the same legacy bootstrap and locking path as RunMigrations. +func MigrationStatuses(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS, dir string) ([]MigrationStatus, error) { + provider, err := newMigrationProvider(pool, fsys, dir) if err != nil { return nil, err } + defer provider.Close() + + gooseStatuses, err := provider.Status(ctx) + if err != nil { + return nil, fmt.Errorf("reading goose migration status: %w", err) + } + + statuses := make([]MigrationStatus, 0, len(gooseStatuses)) + for _, status := range gooseStatuses { + var version int64 + var source string + if status.Source != nil { + version = status.Source.Version + source = status.Source.Path + } + statuses = append(statuses, MigrationStatus{ + Version: version, + Source: source, + State: string(status.State), + AppliedAt: status.AppliedAt, + }) + } + return statuses, nil +} + +func newMigrationProvider(pool *pgxpool.Pool, fsys fs.FS, dir string) (*goose.Provider, error) { + migrationFS, err := migrationSubFS(fsys, dir) + if err != nil { + return nil, err + } + + sqlDB := stdlib.OpenDBFromPool(pool) + + locker, err := lock.NewPostgresSessionLocker(lock.WithLockID(schemaMigrationsLockID)) + if err != nil { + _ = sqlDB.Close() + return nil, fmt.Errorf("creating goose migration lock: %w", err) + } + + provider, err := goose.NewProvider( + goose.DialectPostgres, + sqlDB, + migrationFS, + goose.WithTableName(gooseVersionTable), + goose.WithAllowOutofOrder(true), + goose.WithSessionLocker(&legacyBootstrapLocker{delegate: locker}), + ) + if err != nil { + _ = sqlDB.Close() + return nil, fmt.Errorf("creating goose migration provider: %w", err) + } + return provider, nil +} + +func migrationSubFS(fsys fs.FS, dir string) (fs.FS, error) { + if dir == "" || dir == "." { + return fsys, nil + } + + sub, err := fs.Sub(fsys, dir) + if err != nil { + return nil, fmt.Errorf("opening migration directory %q: %w", dir, err) + } + return sub, nil +} + +type legacyBootstrapLocker struct { + delegate lock.SessionLocker +} + +func (l *legacyBootstrapLocker) SessionLock(ctx context.Context, conn *sql.Conn) error { + if err := l.delegate.SessionLock(ctx, conn); err != nil { + return err + } + if err := bootstrapLegacyGooseVersions(ctx, conn); err != nil { + _ = l.delegate.SessionUnlock(context.WithoutCancel(ctx), conn) + return fmt.Errorf("bootstrapping goose migration versions: %w", err) + } + return nil +} + +func (l *legacyBootstrapLocker) SessionUnlock(ctx context.Context, conn *sql.Conn) error { + return l.delegate.SessionUnlock(ctx, conn) +} + +func bootstrapLegacyGooseVersions(ctx context.Context, conn *sql.Conn) error { + if err := ensureGooseVersionTable(ctx, conn); err != nil { + return err + } + if err := ensureGooseZeroVersion(ctx, conn); err != nil { + return err + } + + hasLegacyVersions, err := tableExists(ctx, conn, "public.schema_versions") + if err != nil { + return fmt.Errorf("checking legacy schema_versions table: %w", err) + } + if !hasLegacyVersions { + return nil + } + + if err := copyLegacyVersions(ctx, conn); err != nil { + return err + } + return verifyLegacyVersions(ctx, conn) +} + +func ensureGooseVersionTable(ctx context.Context, conn *sql.Conn) error { + _, err := conn.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS public.goose_db_version ( + id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + version_id bigint NOT NULL, + is_applied boolean NOT NULL, + tstamp timestamp NOT NULL DEFAULT now() + ) + `) + if err != nil { + return fmt.Errorf("ensuring goose version table: %w", err) + } + return nil +} + +func ensureGooseZeroVersion(ctx context.Context, conn *sql.Conn) error { + _, err := conn.ExecContext(ctx, ` + INSERT INTO public.goose_db_version (version_id, is_applied) + SELECT 0, true + WHERE NOT EXISTS ( + SELECT 1 + FROM public.goose_db_version + WHERE version_id = 0 AND is_applied + ) + `) + if err != nil { + return fmt.Errorf("ensuring goose zero version: %w", err) + } + return nil +} + +func copyLegacyVersions(ctx context.Context, conn *sql.Conn) error { + _, err := conn.ExecContext(ctx, ` + INSERT INTO public.goose_db_version (version_id, is_applied, tstamp) + SELECT sv.version, true, sv.applied_at::timestamp + FROM public.schema_versions sv + WHERE NOT EXISTS ( + SELECT 1 + FROM public.goose_db_version gv + WHERE gv.version_id = sv.version AND gv.is_applied + ) + ORDER BY sv.version + `) + if err != nil { + return fmt.Errorf("copying legacy schema_versions rows: %w", err) + } + return nil +} + +func verifyLegacyVersions(ctx context.Context, conn *sql.Conn) error { + rows, err := conn.QueryContext(ctx, ` + SELECT sv.version + FROM public.schema_versions sv + WHERE NOT EXISTS ( + SELECT 1 + FROM public.goose_db_version gv + WHERE gv.version_id = sv.version AND gv.is_applied + ) + ORDER BY sv.version + LIMIT 10 + `) + if err != nil { + return fmt.Errorf("verifying legacy schema_versions rows: %w", err) + } defer rows.Close() - applied := make(map[int]bool) + var missing []int for rows.Next() { var version int if err := rows.Scan(&version); err != nil { - return nil, err + return fmt.Errorf("scanning missing legacy version: %w", err) } - applied[version] = true + missing = append(missing, version) } - - return applied, rows.Err() -} - -// applyMigration runs a single migration within a transaction and records the -// version in schema_versions. -func applyMigration(ctx context.Context, db migrationDB, m MigrationFile) error { - tx, err := db.BeginTx(ctx, pgx.TxOptions{}) - if err != nil { - return fmt.Errorf("beginning transaction: %w", err) + if err := rows.Err(); err != nil { + return fmt.Errorf("reading missing legacy versions: %w", err) } - defer func() { - // Rollback is a no-op if the tx has already been committed. - _ = tx.Rollback(ctx) - }() - - if _, err := tx.Exec(ctx, m.SQL); err != nil { - return fmt.Errorf("executing SQL: %w", err) + if len(missing) > 0 { + return fmt.Errorf("legacy schema_versions rows missing from goose_db_version: %v", missing) } - - if _, err := tx.Exec(ctx, - "INSERT INTO public.schema_versions (version, filename) VALUES ($1, $2)", - m.Version, m.Filename, - ); err != nil { - return fmt.Errorf("recording version: %w", err) - } - - if err := tx.Commit(ctx); err != nil { - return fmt.Errorf("committing transaction: %w", err) - } - return nil } + +func tableExists(ctx context.Context, conn *sql.Conn, qualifiedName string) (bool, error) { + var exists bool + if err := conn.QueryRowContext(ctx, "SELECT to_regclass($1) IS NOT NULL", qualifiedName).Scan(&exists); err != nil { + return false, err + } + return exists, nil +} diff --git a/internal/metadata/stale_media_id_repo.go b/internal/metadata/stale_media_id_repo.go index 56735b98..6e7519a3 100644 --- a/internal/metadata/stale_media_id_repo.go +++ b/internal/metadata/stale_media_id_repo.go @@ -61,7 +61,7 @@ func (r *StaleMediaIDRepository) Upsert(ctx context.Context, contentID, provider // staleMediaIDContentFKConstraint is the Postgres-auto-generated name of the // stale_media_ids.content_id → media_items(content_id) foreign key, declared -// inline in migrations/036_stale_media_ids.up.sql (hence "{table}_{column}_fkey"). +// inline in migrations/sql/036_stale_media_ids.sql (hence "{table}_{column}_fkey"). const staleMediaIDContentFKConstraint = "stale_media_ids_content_id_fkey" // resolveStaleUpsertError maps an Upsert error to the value Upsert should diff --git a/migrations/003_replace_kometa_with_tmdb.down.sql b/migrations/003_replace_kometa_with_tmdb.down.sql deleted file mode 100644 index f092332e..00000000 --- a/migrations/003_replace_kometa_with_tmdb.down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Convert any tmdb collections to manual before reverting the constraint. -UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'tmdb'; - --- Revert CHECK constraint to pre-migration state (manual, mdblist, kometa). -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; -ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check - CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'kometa'])); diff --git a/migrations/003_replace_kometa_with_tmdb.up.sql b/migrations/003_replace_kometa_with_tmdb.up.sql deleted file mode 100644 index 852eed0a..00000000 --- a/migrations/003_replace_kometa_with_tmdb.up.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Replace any existing kometa collections with manual type. -UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'kometa'; - --- Drop old CHECK constraint and add updated one (kometa → tmdb). -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; -ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check - CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'tmdb'])); diff --git a/migrations/004_collection_image_thumbhash.down.sql b/migrations/004_collection_image_thumbhash.down.sql deleted file mode 100644 index d3b2f60c..00000000 --- a/migrations/004_collection_image_thumbhash.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_thumbhash; -ALTER TABLE library_collections DROP COLUMN IF EXISTS backdrop_thumbhash; diff --git a/migrations/004_collection_image_thumbhash.up.sql b/migrations/004_collection_image_thumbhash.up.sql deleted file mode 100644 index 7216aac9..00000000 --- a/migrations/004_collection_image_thumbhash.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE library_collections ADD COLUMN poster_thumbhash TEXT NOT NULL DEFAULT ''; -ALTER TABLE library_collections ADD COLUMN backdrop_thumbhash TEXT NOT NULL DEFAULT ''; diff --git a/migrations/010_session_tracking.down.sql b/migrations/010_session_tracking.down.sql deleted file mode 100644 index 822c3690..00000000 --- a/migrations/010_session_tracking.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE playback_sessions_sync DROP COLUMN IF EXISTS has_websocket; -ALTER TABLE playback_sessions_sync DROP COLUMN IF EXISTS is_paused; -DROP TABLE IF EXISTS node_heartbeats; diff --git a/migrations/013_item_refresh_jobs.down.sql b/migrations/013_item_refresh_jobs.down.sql deleted file mode 100644 index 8e21a9e9..00000000 --- a/migrations/013_item_refresh_jobs.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS public.admin_jobs_active_job_type_idx; - -CREATE UNIQUE INDEX admin_jobs_active_job_type_idx -ON public.admin_jobs USING btree (job_type) -WHERE (status = ANY (ARRAY['queued'::text, 'running'::text])); diff --git a/migrations/013_item_refresh_jobs.up.sql b/migrations/013_item_refresh_jobs.up.sql deleted file mode 100644 index 8c131576..00000000 --- a/migrations/013_item_refresh_jobs.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP INDEX IF EXISTS public.admin_jobs_active_job_type_idx; - -CREATE UNIQUE INDEX admin_jobs_active_job_type_idx -ON public.admin_jobs USING btree (job_type) -WHERE ( - status = ANY (ARRAY['queued'::text, 'running'::text]) - AND job_type = ANY (ARRAY['catalog_export'::text, 'catalog_import'::text]) -); diff --git a/migrations/015_admin_smart_collections.down.sql b/migrations/015_admin_smart_collections.down.sql deleted file mode 100644 index 8ccb76e8..00000000 --- a/migrations/015_admin_smart_collections.down.sql +++ /dev/null @@ -1,16 +0,0 @@ -UPDATE library_collections lc -SET library_id = lcl.library_id -FROM ( - SELECT DISTINCT ON (collection_id) collection_id, library_id - FROM library_collection_libraries - ORDER BY collection_id, library_id -) lcl -WHERE lc.id = lcl.collection_id; - -DROP INDEX IF EXISTS idx_library_collection_libraries_library; -DROP TABLE IF EXISTS library_collection_libraries; - -ALTER TABLE library_collections - DROP COLUMN IF EXISTS sort_config, - DROP COLUMN IF EXISTS query_definition, - DROP COLUMN IF EXISTS collection_mode; diff --git a/migrations/016_user_smart_collections.down.sql b/migrations/016_user_smart_collections.down.sql deleted file mode 100644 index db648c17..00000000 --- a/migrations/016_user_smart_collections.down.sql +++ /dev/null @@ -1,18 +0,0 @@ -UPDATE user_personal_collections upc -SET profile_id = ucpp.profile_id -FROM ( - SELECT DISTINCT ON (user_id, collection_id) user_id, collection_id, profile_id - FROM user_personal_collection_profiles - ORDER BY user_id, collection_id, profile_id -) ucpp -WHERE upc.user_id = ucpp.user_id AND upc.id = ucpp.collection_id; - -DROP INDEX IF EXISTS idx_user_collection_profiles_lookup; -DROP TABLE IF EXISTS user_personal_collection_profiles; - -ALTER TABLE user_personal_collections - DROP COLUMN IF EXISTS sort_config, - DROP COLUMN IF EXISTS query_definition, - DROP COLUMN IF EXISTS is_shared, - DROP COLUMN IF EXISTS collection_mode, - DROP COLUMN IF EXISTS creator_profile_id; diff --git a/migrations/019_catalog_release_date.down.sql b/migrations/019_catalog_release_date.down.sql deleted file mode 100644 index 8c2c2feb..00000000 --- a/migrations/019_catalog_release_date.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.media_items - DROP COLUMN IF EXISTS release_date; diff --git a/migrations/022_skipped_media_roots.down.sql b/migrations/022_skipped_media_roots.down.sql deleted file mode 100644 index b4cfb3c3..00000000 --- a/migrations/022_skipped_media_roots.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS skipped_media_roots; diff --git a/migrations/023_opslog_component_level_index.down.sql b/migrations/023_opslog_component_level_index.down.sql deleted file mode 100644 index db752629..00000000 --- a/migrations/023_opslog_component_level_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS idx_operational_logs_component_level; diff --git a/migrations/023_opslog_component_level_index.up.sql b/migrations/023_opslog_component_level_index.up.sql deleted file mode 100644 index 044b8ab9..00000000 --- a/migrations/023_opslog_component_level_index.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_operational_logs_component_level - ON public.operational_logs (component, level, "timestamp" DESC); diff --git a/migrations/025_merge_collection_mode_type.down.sql b/migrations/025_merge_collection_mode_type.down.sql deleted file mode 100644 index 116b5466..00000000 --- a/migrations/025_merge_collection_mode_type.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Reverse: rename collection_type back to collection_mode for user personal collections. -ALTER TABLE user_personal_collections RENAME COLUMN collection_type TO collection_mode; - --- Re-add collection_mode column to library collections. -ALTER TABLE library_collections ADD COLUMN collection_mode TEXT NOT NULL DEFAULT 'manual'; - --- Backfill: smart type → smart mode. -UPDATE library_collections SET collection_mode = 'smart' WHERE collection_type = 'smart'; - --- Revert smart type back to manual. -UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'smart'; - --- Restore original check constraint without 'smart'. -ALTER TABLE library_collections DROP CONSTRAINT library_collections_collection_type_check; -ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check - CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'tmdb'])); diff --git a/migrations/025_merge_collection_mode_type.up.sql b/migrations/025_merge_collection_mode_type.up.sql deleted file mode 100644 index aeec5915..00000000 --- a/migrations/025_merge_collection_mode_type.up.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Merge collection_mode into collection_type for library collections. - --- Drop the old constraint first so the backfill can write 'smart'. -ALTER TABLE library_collections DROP CONSTRAINT library_collections_collection_type_check; - --- Backfill: smart mode → smart type. -UPDATE library_collections SET collection_type = 'smart' WHERE collection_mode = 'smart'; - --- Drop the mode column. -ALTER TABLE library_collections DROP COLUMN collection_mode; - --- Re-add constraint with 'smart' included. -ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check - CHECK (collection_type = ANY (ARRAY['manual', 'smart', 'mdblist', 'tmdb'])); - --- Rename collection_mode → collection_type for user personal collections. -ALTER TABLE user_personal_collections RENAME COLUMN collection_mode TO collection_type; diff --git a/migrations/026_match_attempted_queue.down.sql b/migrations/026_match_attempted_queue.down.sql deleted file mode 100644 index 205d3351..00000000 --- a/migrations/026_match_attempted_queue.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_files_unmatched_match_queue; - -ALTER TABLE public.media_files - DROP COLUMN IF EXISTS match_attempted_at; diff --git a/migrations/027_user_home_item_dismissals.down.sql b/migrations/027_user_home_item_dismissals.down.sql deleted file mode 100644 index 0dc266ca..00000000 --- a/migrations/027_user_home_item_dismissals.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_home_item_dismissals; diff --git a/migrations/028_log_partitioning.down.sql b/migrations/028_log_partitioning.down.sql deleted file mode 100644 index cceee0a5..00000000 --- a/migrations/028_log_partitioning.down.sql +++ /dev/null @@ -1,174 +0,0 @@ -CREATE TABLE public.operational_logs_unpartitioned ( - id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - "timestamp" timestamp with time zone DEFAULT now() NOT NULL, - level text NOT NULL, - component text NOT NULL, - message text NOT NULL, - request_id text, - user_id integer, - session_id text, - client_ip inet, - node_id text, - attrs jsonb DEFAULT '{}'::jsonb NOT NULL, - playback_session_id text -); - -INSERT INTO public.operational_logs_unpartitioned ( - id, - "timestamp", - level, - component, - message, - request_id, - user_id, - session_id, - client_ip, - node_id, - attrs, - playback_session_id -) -OVERRIDING SYSTEM VALUE -SELECT - id, - "timestamp", - level, - component, - message, - request_id, - user_id, - session_id, - client_ip, - node_id, - attrs, - playback_session_id -FROM public.operational_logs; - -DO $$ -DECLARE - seq_name text; - max_id bigint; -BEGIN - SELECT pg_get_serial_sequence('public.operational_logs_unpartitioned', 'id') INTO seq_name; - SELECT MAX(id) INTO max_id FROM public.operational_logs_unpartitioned; - IF max_id IS NULL THEN - EXECUTE format('SELECT setval(%L, 1, false)', seq_name); - ELSE - EXECUTE format('SELECT setval(%L, %s, true)', seq_name, max_id); - END IF; -END -$$; - -ALTER TABLE public.operational_logs RENAME TO operational_logs_partitioned_old; -ALTER TABLE public.operational_logs_unpartitioned RENAME TO operational_logs; -DROP TABLE public.operational_logs_partitioned_old; - -CREATE INDEX idx_operational_logs_timestamp - ON public.operational_logs USING btree ("timestamp" DESC); -CREATE INDEX idx_operational_logs_level - ON public.operational_logs USING btree (level, "timestamp" DESC); -CREATE INDEX idx_operational_logs_component - ON public.operational_logs USING btree (component, "timestamp" DESC); -CREATE INDEX idx_operational_logs_request_id - ON public.operational_logs USING btree (request_id, "timestamp" DESC); -CREATE INDEX idx_operational_logs_user_id - ON public.operational_logs USING btree (user_id, "timestamp" DESC); -CREATE INDEX idx_operational_logs_session_id - ON public.operational_logs USING btree (session_id, "timestamp" DESC); -CREATE INDEX idx_operational_logs_playback_session_id - ON public.operational_logs USING btree (playback_session_id, "timestamp" DESC); -CREATE INDEX idx_operational_logs_component_level - ON public.operational_logs USING btree (component, level, "timestamp" DESC); - -ALTER TABLE public.operational_logs - ADD CONSTRAINT operational_logs_user_id_fkey - FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; - -ALTER TABLE public.activity_log - ALTER COLUMN id DROP DEFAULT; -ALTER SEQUENCE public.activity_log_id_seq - OWNED BY NONE; - -CREATE TABLE public.activity_log_unpartitioned ( - id bigint NOT NULL DEFAULT nextval('public.activity_log_id_seq'::regclass), - "timestamp" timestamp with time zone DEFAULT now() NOT NULL, - client_ip inet NOT NULL, - user_id integer, - session_id text, - method text NOT NULL, - path text NOT NULL, - status_code integer, - user_agent text, - duration_ms integer, - request_id text, - node_id text, - path_pattern text, - playback_session_id text, - impersonator_user_id integer, - PRIMARY KEY (id) -); - -INSERT INTO public.activity_log_unpartitioned ( - id, - "timestamp", - client_ip, - user_id, - session_id, - method, - path, - status_code, - user_agent, - duration_ms, - request_id, - node_id, - path_pattern, - playback_session_id, - impersonator_user_id -) -SELECT - id, - "timestamp", - client_ip, - user_id, - session_id, - method, - path, - status_code, - user_agent, - duration_ms, - request_id, - node_id, - path_pattern, - playback_session_id, - impersonator_user_id -FROM public.activity_log; - -SELECT setval( - 'public.activity_log_id_seq', - COALESCE((SELECT MAX(id) FROM public.activity_log_unpartitioned), 1), - EXISTS (SELECT 1 FROM public.activity_log_unpartitioned) -); - -ALTER TABLE public.activity_log RENAME TO activity_log_partitioned_old; -ALTER TABLE public.activity_log_unpartitioned RENAME TO activity_log; -DROP TABLE public.activity_log_partitioned_old; - -ALTER SEQUENCE public.activity_log_id_seq - OWNED BY public.activity_log.id; - -CREATE INDEX idx_activity_log_client_ip - ON public.activity_log USING btree (client_ip, "timestamp" DESC); -CREATE INDEX idx_activity_log_timestamp - ON public.activity_log USING btree ("timestamp" DESC); -CREATE INDEX idx_activity_log_user_id - ON public.activity_log USING btree (user_id, "timestamp" DESC); -CREATE INDEX idx_activity_log_playback_session_id - ON public.activity_log USING btree (playback_session_id, "timestamp" DESC); -CREATE INDEX idx_activity_log_request_id - ON public.activity_log USING btree (request_id, "timestamp" DESC); -CREATE INDEX idx_activity_log_impersonator_user_id - ON public.activity_log USING btree (impersonator_user_id, "timestamp" DESC) - WHERE impersonator_user_id IS NOT NULL; - -ALTER TABLE public.activity_log - ADD CONSTRAINT activity_log_user_id_fkey - FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; diff --git a/migrations/029_show_forced_subtitles.down.sql b/migrations/029_show_forced_subtitles.down.sql deleted file mode 100644 index 212c57d2..00000000 --- a/migrations/029_show_forced_subtitles.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE user_subtitle_preferences - DROP COLUMN show_forced_subtitles; - -ALTER TABLE user_library_playback_preferences - DROP COLUMN show_forced_subtitles; - -ALTER TABLE user_profiles - DROP COLUMN show_forced_subtitles; diff --git a/migrations/029_show_forced_subtitles.up.sql b/migrations/029_show_forced_subtitles.up.sql deleted file mode 100644 index 5bb640d2..00000000 --- a/migrations/029_show_forced_subtitles.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -ALTER TABLE user_profiles - ADD COLUMN show_forced_subtitles BOOLEAN NOT NULL DEFAULT true; - -ALTER TABLE user_library_playback_preferences - ADD COLUMN show_forced_subtitles BOOLEAN; - -ALTER TABLE user_subtitle_preferences - ADD COLUMN show_forced_subtitles BOOLEAN; diff --git a/migrations/030_file_first_fallback_library_visibility.down.sql b/migrations/030_file_first_fallback_library_visibility.down.sql deleted file mode 100644 index bdb54035..00000000 --- a/migrations/030_file_first_fallback_library_visibility.down.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE episodes - DROP COLUMN metadata_source; - -ALTER TABLE seasons - DROP COLUMN metadata_source; - -ALTER TABLE media_items - DROP COLUMN episode_metadata_last_checked_at, - DROP COLUMN episode_metadata_incomplete; diff --git a/migrations/030_file_first_fallback_library_visibility.up.sql b/migrations/030_file_first_fallback_library_visibility.up.sql deleted file mode 100644 index 8d08277e..00000000 --- a/migrations/030_file_first_fallback_library_visibility.up.sql +++ /dev/null @@ -1,9 +0,0 @@ -ALTER TABLE media_items - ADD COLUMN episode_metadata_incomplete BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN episode_metadata_last_checked_at TIMESTAMPTZ; - -ALTER TABLE seasons - ADD COLUMN metadata_source TEXT NOT NULL DEFAULT 'provider'; - -ALTER TABLE episodes - ADD COLUMN metadata_source TEXT NOT NULL DEFAULT 'provider'; diff --git a/migrations/031_plugin_runtime.down.sql b/migrations/031_plugin_runtime.down.sql deleted file mode 100644 index 3bbf41ff..00000000 --- a/migrations/031_plugin_runtime.down.sql +++ /dev/null @@ -1,28 +0,0 @@ -ALTER TABLE media_files - DROP COLUMN markers_confidence, - DROP COLUMN markers_source; - -DROP INDEX IF EXISTS idx_media_analyzer_bindings_folder_installation; -DROP TABLE IF EXISTS media_analyzer_bindings; - -DROP INDEX IF EXISTS idx_plugin_auth_identities_installation_subject; -DROP TABLE IF EXISTS plugin_auth_identities; - -DROP INDEX IF EXISTS idx_plugin_auth_bindings_installation_capability; -DROP TABLE IF EXISTS plugin_auth_bindings; - -DROP INDEX IF EXISTS idx_plugin_task_bindings_installation_capability; -DROP TABLE IF EXISTS plugin_task_bindings; - -DROP INDEX IF EXISTS idx_plugin_runtime_configs_installation_key; -DROP TABLE IF EXISTS plugin_runtime_configs; - -DROP INDEX IF EXISTS idx_plugin_capabilities_installation_capability; -DROP INDEX IF EXISTS idx_plugin_capabilities_installation; -DROP TABLE IF EXISTS plugin_capabilities; - -DROP INDEX IF EXISTS idx_plugin_installations_plugin_version; -DROP TABLE IF EXISTS plugin_installations; - -DROP INDEX IF EXISTS idx_plugin_repositories_enabled; -DROP TABLE IF EXISTS plugin_repositories; diff --git a/migrations/032_plugin_archives.down.sql b/migrations/032_plugin_archives.down.sql deleted file mode 100644 index d4a01056..00000000 --- a/migrations/032_plugin_archives.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS plugin_archives; diff --git a/migrations/033_plugin_update_policy.down.sql b/migrations/033_plugin_update_policy.down.sql deleted file mode 100644 index 0230e213..00000000 --- a/migrations/033_plugin_update_policy.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE plugin_installations - DROP COLUMN update_policy, - DROP COLUMN available_version; diff --git a/migrations/033_plugin_update_policy.up.sql b/migrations/033_plugin_update_policy.up.sql deleted file mode 100644 index 9e80cee2..00000000 --- a/migrations/033_plugin_update_policy.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE plugin_installations - ADD COLUMN update_policy TEXT NOT NULL DEFAULT 'auto', - ADD COLUMN available_version TEXT; diff --git a/migrations/034_plugin_auth_local_passwords.down.sql b/migrations/034_plugin_auth_local_passwords.down.sql deleted file mode 100644 index 19056677..00000000 --- a/migrations/034_plugin_auth_local_passwords.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.users - DROP COLUMN IF EXISTS local_password_login_enabled; diff --git a/migrations/036_stale_media_ids.down.sql b/migrations/036_stale_media_ids.down.sql deleted file mode 100644 index 70160b3b..00000000 --- a/migrations/036_stale_media_ids.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS stale_media_ids; diff --git a/migrations/037_provider_chain_content_levels.down.sql b/migrations/037_provider_chain_content_levels.down.sql deleted file mode 100644 index b3afb397..00000000 --- a/migrations/037_provider_chain_content_levels.down.sql +++ /dev/null @@ -1,12 +0,0 @@ --- Remove per-level entries, keeping only legacy flat entries. -DELETE FROM library_provider_chains WHERE content_level != ''; - -ALTER TABLE library_provider_chains - DROP CONSTRAINT library_provider_chains_pkey; - -ALTER TABLE library_provider_chains - ADD PRIMARY KEY (media_folder_id, provider_id); - -ALTER TABLE library_provider_chains - DROP COLUMN enabled, - DROP COLUMN content_level; diff --git a/migrations/038_media_item_roots.down.sql b/migrations/038_media_item_roots.down.sql deleted file mode 100644 index 8d886d3f..00000000 --- a/migrations/038_media_item_roots.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS media_item_roots; diff --git a/migrations/039_plex_history_import.down.sql b/migrations/039_plex_history_import.down.sql deleted file mode 100644 index 59a923e3..00000000 --- a/migrations/039_plex_history_import.down.sql +++ /dev/null @@ -1,16 +0,0 @@ -DROP TABLE IF EXISTS history_import_plex_sessions; - -ALTER TABLE history_import_runs - DROP CONSTRAINT history_import_runs_connection_mode_check, - ADD CONSTRAINT history_import_runs_connection_mode_check - CHECK (connection_mode IN ('connect', 'predefined', 'custom')); - -ALTER TABLE history_import_runs - DROP CONSTRAINT history_import_runs_source_type_check, - ADD CONSTRAINT history_import_runs_source_type_check - CHECK (source_type IN ('emby')); - -ALTER TABLE history_import_sources - DROP CONSTRAINT history_import_sources_source_type_check, - ADD CONSTRAINT history_import_sources_source_type_check - CHECK (source_type IN ('emby')); diff --git a/migrations/040_history_import_run_heartbeats.down.sql b/migrations/040_history_import_run_heartbeats.down.sql deleted file mode 100644 index 411a7288..00000000 --- a/migrations/040_history_import_run_heartbeats.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_history_import_runs_running_heartbeat; - -ALTER TABLE history_import_runs - DROP COLUMN IF EXISTS last_heartbeat_at; diff --git a/migrations/041_jellyfin_history_import.down.sql b/migrations/041_jellyfin_history_import.down.sql deleted file mode 100644 index 0e756cf3..00000000 --- a/migrations/041_jellyfin_history_import.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE history_import_runs - DROP CONSTRAINT history_import_runs_source_type_check, - ADD CONSTRAINT history_import_runs_source_type_check - CHECK (source_type IN ('emby', 'plex')); diff --git a/migrations/041_jellyfin_history_import.up.sql b/migrations/041_jellyfin_history_import.up.sql deleted file mode 100644 index 1b116489..00000000 --- a/migrations/041_jellyfin_history_import.up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Expand run source_type to allow 'jellyfin' -ALTER TABLE history_import_runs - DROP CONSTRAINT history_import_runs_source_type_check, - ADD CONSTRAINT history_import_runs_source_type_check - CHECK (source_type IN ('emby', 'jellyfin', 'plex')); diff --git a/migrations/042_downloads.down.sql b/migrations/042_downloads.down.sql deleted file mode 100644 index 3f59f660..00000000 --- a/migrations/042_downloads.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.downloads; diff --git a/migrations/043_empty_trash_setting.down.sql b/migrations/043_empty_trash_setting.down.sql deleted file mode 100644 index 9a1f6010..00000000 --- a/migrations/043_empty_trash_setting.down.sql +++ /dev/null @@ -1 +0,0 @@ -DELETE FROM server_settings WHERE key = 'scanner.empty_trash_after_scan'; diff --git a/migrations/043_empty_trash_setting.up.sql b/migrations/043_empty_trash_setting.up.sql deleted file mode 100644 index d1dabaa1..00000000 --- a/migrations/043_empty_trash_setting.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -INSERT INTO server_settings (key, value) -VALUES ('scanner.empty_trash_after_scan', 'true') -ON CONFLICT (key) DO NOTHING; diff --git a/migrations/044_embedding_orphan_cleanup.down.sql b/migrations/044_embedding_orphan_cleanup.down.sql deleted file mode 100644 index d72f6ac7..00000000 --- a/migrations/044_embedding_orphan_cleanup.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE media_item_embeddings - DROP CONSTRAINT IF EXISTS media_item_embeddings_media_item_id_fkey; diff --git a/migrations/045_library_sort_order.down.sql b/migrations/045_library_sort_order.down.sql deleted file mode 100644 index 79b0c73c..00000000 --- a/migrations/045_library_sort_order.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_folders DROP COLUMN sort_order; diff --git a/migrations/046_drop_media_analyzer_bindings.up.sql b/migrations/046_drop_media_analyzer_bindings.up.sql deleted file mode 100644 index 8ea2fba4..00000000 --- a/migrations/046_drop_media_analyzer_bindings.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_media_analyzer_bindings_folder_installation; -DROP TABLE IF EXISTS media_analyzer_bindings; diff --git a/migrations/047_remove_metadata_providers.up.sql b/migrations/047_remove_metadata_providers.up.sql deleted file mode 100644 index 33e5ce4c..00000000 --- a/migrations/047_remove_metadata_providers.up.sql +++ /dev/null @@ -1,53 +0,0 @@ --- Remove the metadata_providers mirror table. --- library_provider_chains now references plugin capabilities directly --- via the composite natural key (plugin_installation_id, capability_id). - -BEGIN; - --- Step 1: Add new columns (nullable initially for the data migration). -ALTER TABLE library_provider_chains - ADD COLUMN plugin_installation_id BIGINT, - ADD COLUMN capability_id TEXT, - ADD COLUMN capability_type TEXT NOT NULL DEFAULT 'metadata_provider.v1'; - --- Step 2: Populate from metadata_providers settings. -UPDATE library_provider_chains lpc -SET plugin_installation_id = (mp.settings->>'plugin_installation_id')::bigint, - capability_id = mp.settings->>'capability_id' -FROM metadata_providers mp -WHERE lpc.provider_id = mp.id; - --- Step 3: Delete orphaned rows that couldn't be mapped (provider was --- uninstalled but chain entry lingered). These are invalid anyway. -DELETE FROM library_provider_chains -WHERE plugin_installation_id IS NULL OR capability_id IS NULL; - --- Step 4: Make new columns NOT NULL now that data is populated. -ALTER TABLE library_provider_chains - ALTER COLUMN plugin_installation_id SET NOT NULL, - ALTER COLUMN capability_id SET NOT NULL; - --- Step 5: Drop old constraints and column. -ALTER TABLE library_provider_chains - DROP CONSTRAINT library_provider_chains_provider_id_fkey; - -ALTER TABLE library_provider_chains - DROP CONSTRAINT library_provider_chains_pkey; - -ALTER TABLE library_provider_chains - DROP COLUMN provider_id; - --- Step 6: New PK and FK. -ALTER TABLE library_provider_chains - ADD PRIMARY KEY (media_folder_id, plugin_installation_id, capability_id, content_level); - -ALTER TABLE library_provider_chains - ADD CONSTRAINT library_provider_chains_installation_fkey - FOREIGN KEY (plugin_installation_id) - REFERENCES plugin_installations(id) ON DELETE CASCADE; - --- Step 7: Drop the mirror table. -DROP TABLE IF EXISTS metadata_providers; -DROP SEQUENCE IF EXISTS metadata_providers_id_seq; - -COMMIT; diff --git a/migrations/048_library_refresh_jobs.down.sql b/migrations/048_library_refresh_jobs.down.sql deleted file mode 100644 index bac79e10..00000000 --- a/migrations/048_library_refresh_jobs.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.admin_jobs_active_library_refresh_idx; diff --git a/migrations/049_admin_history_import.down.sql b/migrations/049_admin_history_import.down.sql deleted file mode 100644 index 1b6741c8..00000000 --- a/migrations/049_admin_history_import.down.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE history_import_runs - DROP CONSTRAINT history_import_runs_connection_mode_check, - ADD CONSTRAINT history_import_runs_connection_mode_check - CHECK (connection_mode IN ('connect', 'predefined', 'custom', 'plex_oauth')); - -DROP INDEX IF EXISTS idx_history_import_runs_mapping; - -ALTER TABLE history_import_runs - DROP COLUMN IF EXISTS mapping_id; - -DROP INDEX IF EXISTS idx_history_import_user_mappings_user; -DROP INDEX IF EXISTS idx_history_import_user_mappings_source; - -DROP TABLE IF EXISTS history_import_user_mappings; - -ALTER TABLE history_import_sources - DROP COLUMN IF EXISTS encrypted_admin_token; diff --git a/migrations/050_rename_encrypted_admin_token.down.sql b/migrations/050_rename_encrypted_admin_token.down.sql deleted file mode 100644 index f8e8106e..00000000 --- a/migrations/050_rename_encrypted_admin_token.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE history_import_sources RENAME COLUMN admin_token TO encrypted_admin_token; diff --git a/migrations/050_rename_encrypted_admin_token.up.sql b/migrations/050_rename_encrypted_admin_token.up.sql deleted file mode 100644 index 9aef7ec2..00000000 --- a/migrations/050_rename_encrypted_admin_token.up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Rename column: encryption was removed; plain "admin_token" is clearer. -ALTER TABLE history_import_sources RENAME COLUMN encrypted_admin_token TO admin_token; diff --git a/migrations/051_allow_jellyfin_sources.down.sql b/migrations/051_allow_jellyfin_sources.down.sql deleted file mode 100644 index 941d5e6e..00000000 --- a/migrations/051_allow_jellyfin_sources.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE history_import_sources - DROP CONSTRAINT history_import_sources_source_type_check, - ADD CONSTRAINT history_import_sources_source_type_check - CHECK (source_type IN ('emby', 'plex')); diff --git a/migrations/052_library_metadata_language.down.sql b/migrations/052_library_metadata_language.down.sql deleted file mode 100644 index 7059aa21..00000000 --- a/migrations/052_library_metadata_language.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_folders DROP COLUMN metadata_language; diff --git a/migrations/052_library_metadata_language.up.sql b/migrations/052_library_metadata_language.up.sql deleted file mode 100644 index a2aeb4bb..00000000 --- a/migrations/052_library_metadata_language.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_folders ADD COLUMN metadata_language text NOT NULL DEFAULT 'en'; diff --git a/migrations/053_user_history_hidden_items.down.sql b/migrations/053_user_history_hidden_items.down.sql deleted file mode 100644 index 004d5805..00000000 --- a/migrations/053_user_history_hidden_items.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_user_history_hidden_items_profile; - -DROP TABLE IF EXISTS public.user_history_hidden_items; diff --git a/migrations/054_catalog_filter_support_indexes.down.sql b/migrations/054_catalog_filter_support_indexes.down.sql deleted file mode 100644 index c2f63d31..00000000 --- a/migrations/054_catalog_filter_support_indexes.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS public.idx_user_watch_history_profile_item_completed; - -DROP INDEX IF EXISTS public.idx_item_people_content_kind_person; diff --git a/migrations/055_metadata_localizations.down.sql b/migrations/055_metadata_localizations.down.sql deleted file mode 100644 index 1efab1fc..00000000 --- a/migrations/055_metadata_localizations.down.sql +++ /dev/null @@ -1,16 +0,0 @@ -DROP INDEX IF EXISTS idx_episode_localizations_language; -DROP INDEX IF EXISTS idx_season_localizations_language; -DROP INDEX IF EXISTS idx_media_item_localizations_language; - -DROP TABLE IF EXISTS episode_localizations; -DROP TABLE IF EXISTS season_localizations; -DROP TABLE IF EXISTS media_item_localizations; - -ALTER TABLE episodes - DROP COLUMN IF EXISTS default_metadata_language; - -ALTER TABLE seasons - DROP COLUMN IF EXISTS default_metadata_language; - -ALTER TABLE media_items - DROP COLUMN IF EXISTS default_metadata_language; diff --git a/migrations/056_user_max_profiles.down.sql b/migrations/056_user_max_profiles.down.sql deleted file mode 100644 index cef12418..00000000 --- a/migrations/056_user_max_profiles.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.users - DROP COLUMN IF EXISTS max_profiles; diff --git a/migrations/056_user_max_profiles.up.sql b/migrations/056_user_max_profiles.up.sql deleted file mode 100644 index f24df5be..00000000 --- a/migrations/056_user_max_profiles.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.users - ADD COLUMN max_profiles integer DEFAULT 0 NOT NULL; diff --git a/migrations/057_calendar_indexes.down.sql b/migrations/057_calendar_indexes.down.sql deleted file mode 100644 index 2e44a039..00000000 --- a/migrations/057_calendar_indexes.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_episodes_air_date; -DROP INDEX IF EXISTS idx_seasons_air_date; -DROP INDEX IF EXISTS idx_media_items_movie_release_date; diff --git a/migrations/058_add_air_time.down.sql b/migrations/058_add_air_time.down.sql deleted file mode 100644 index 2697243f..00000000 --- a/migrations/058_add_air_time.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_items DROP COLUMN air_time; diff --git a/migrations/058_add_air_time.up.sql b/migrations/058_add_air_time.up.sql deleted file mode 100644 index 77e6601e..00000000 --- a/migrations/058_add_air_time.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_items ADD COLUMN air_time text; diff --git a/migrations/059_plex_sync.down.sql b/migrations/059_plex_sync.down.sql deleted file mode 100644 index 36a95d01..00000000 --- a/migrations/059_plex_sync.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP TABLE IF EXISTS public.plex_sync_item_state; -DROP TABLE IF EXISTS public.plex_sync_item_bindings; -DROP TABLE IF EXISTS public.plex_sync_actor_mappings; -DROP TABLE IF EXISTS public.plex_sync_connections; diff --git a/migrations/060_webhook_sync.down.sql b/migrations/060_webhook_sync.down.sql deleted file mode 100644 index d096b4a5..00000000 --- a/migrations/060_webhook_sync.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TABLE IF EXISTS public.webhook_sync_item_state; -DROP TABLE IF EXISTS public.webhook_sync_actor_mappings; -DROP TABLE IF EXISTS public.webhook_sync_connections; diff --git a/migrations/061_user_series_playback_preferences.down.sql b/migrations/061_user_series_playback_preferences.down.sql deleted file mode 100644 index ec6d48a0..00000000 --- a/migrations/061_user_series_playback_preferences.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS user_series_playback_preferences; diff --git a/migrations/062_playback_requested_media_file.down.sql b/migrations/062_playback_requested_media_file.down.sql deleted file mode 100644 index af2738d6..00000000 --- a/migrations/062_playback_requested_media_file.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE playback_sessions_sync - DROP COLUMN IF EXISTS requested_media_file_id; diff --git a/migrations/063_collection_sync_schedule.down.sql b/migrations/063_collection_sync_schedule.down.sql deleted file mode 100644 index d44539e3..00000000 --- a/migrations/063_collection_sync_schedule.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS idx_library_collections_next_sync_due; - -ALTER TABLE library_collections - DROP COLUMN IF EXISTS next_sync_at, - DROP COLUMN IF EXISTS sync_schedule; diff --git a/migrations/064_collection_poster_auto_generated.down.sql b/migrations/064_collection_poster_auto_generated.down.sql deleted file mode 100644 index 945309dd..00000000 --- a/migrations/064_collection_poster_auto_generated.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_auto_generated; diff --git a/migrations/064_collection_poster_auto_generated.up.sql b/migrations/064_collection_poster_auto_generated.up.sql deleted file mode 100644 index dc2bb8a7..00000000 --- a/migrations/064_collection_poster_auto_generated.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE library_collections - ADD COLUMN IF NOT EXISTS poster_auto_generated BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/065_user_track_signatures.down.sql b/migrations/065_user_track_signatures.down.sql deleted file mode 100644 index 7a823096..00000000 --- a/migrations/065_user_track_signatures.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE user_audio_preferences - DROP COLUMN IF EXISTS audio_track_signature; - -ALTER TABLE user_subtitle_preferences - DROP COLUMN IF EXISTS subtitle_track_signature; diff --git a/migrations/065_user_track_signatures.up.sql b/migrations/065_user_track_signatures.up.sql deleted file mode 100644 index f67bb4d0..00000000 --- a/migrations/065_user_track_signatures.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE user_audio_preferences - ADD COLUMN IF NOT EXISTS audio_track_signature JSONB NOT NULL DEFAULT '{}'::jsonb; - -ALTER TABLE user_subtitle_preferences - ADD COLUMN IF NOT EXISTS subtitle_track_signature JSONB NOT NULL DEFAULT '{}'::jsonb; diff --git a/migrations/066_media_chapters_and_library_thumbnail_settings.down.sql b/migrations/066_media_chapters_and_library_thumbnail_settings.down.sql deleted file mode 100644 index 2f2612ac..00000000 --- a/migrations/066_media_chapters_and_library_thumbnail_settings.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE public.media_folders - DROP COLUMN IF EXISTS chapter_thumbnails_enabled; - -ALTER TABLE public.media_files - DROP COLUMN IF EXISTS chapters; diff --git a/migrations/066_media_chapters_and_library_thumbnail_settings.up.sql b/migrations/066_media_chapters_and_library_thumbnail_settings.up.sql deleted file mode 100644 index b9c85904..00000000 --- a/migrations/066_media_chapters_and_library_thumbnail_settings.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE public.media_files - ADD COLUMN chapters jsonb; - -ALTER TABLE public.media_folders - ADD COLUMN chapter_thumbnails_enabled boolean NOT NULL DEFAULT false; diff --git a/migrations/067_media_file_chapter_thumbnail_failure_state.down.sql b/migrations/067_media_file_chapter_thumbnail_failure_state.down.sql deleted file mode 100644 index ee3528ae..00000000 --- a/migrations/067_media_file_chapter_thumbnail_failure_state.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE public.media_files - DROP COLUMN IF EXISTS chapter_thumbnail_last_error, - DROP COLUMN IF EXISTS chapter_thumbnail_failure_count, - DROP COLUMN IF EXISTS chapter_thumbnail_retry_after; diff --git a/migrations/067_media_file_chapter_thumbnail_failure_state.up.sql b/migrations/067_media_file_chapter_thumbnail_failure_state.up.sql deleted file mode 100644 index d4a51b1f..00000000 --- a/migrations/067_media_file_chapter_thumbnail_failure_state.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE public.media_files - ADD COLUMN chapter_thumbnail_retry_after timestamptz, - ADD COLUMN chapter_thumbnail_failure_count integer NOT NULL DEFAULT 0, - ADD COLUMN chapter_thumbnail_last_error text; diff --git a/migrations/068_webhook_sync_event_logs.down.sql b/migrations/068_webhook_sync_event_logs.down.sql deleted file mode 100644 index a91e9478..00000000 --- a/migrations/068_webhook_sync_event_logs.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.webhook_sync_event_logs; diff --git a/migrations/069_watch_together_rooms.down.sql b/migrations/069_watch_together_rooms.down.sql deleted file mode 100644 index 9db60baa..00000000 --- a/migrations/069_watch_together_rooms.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_watch_together_rooms_status_created_at; -DROP TABLE IF EXISTS watch_together_rooms; diff --git a/migrations/070_watch_together_room_first.down.sql b/migrations/070_watch_together_room_first.down.sql deleted file mode 100644 index 3733aab2..00000000 --- a/migrations/070_watch_together_room_first.down.sql +++ /dev/null @@ -1,33 +0,0 @@ -ALTER TABLE watch_together_rooms - ADD COLUMN content_id TEXT, - ADD COLUMN file_id INTEGER, - ADD COLUMN library_id INTEGER, - ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; - -UPDATE watch_together_rooms -SET content_id = COALESCE(selected_content_id, ''), - file_id = selected_file_id, - library_id = selected_library_id, - status = CASE - WHEN phase = 'ended' THEN 'closed' - ELSE 'active' - END; - -ALTER TABLE watch_together_rooms - ALTER COLUMN content_id SET NOT NULL, - DROP CONSTRAINT IF EXISTS watch_together_rooms_phase_check, - DROP CONSTRAINT IF EXISTS watch_together_rooms_selection_mode_check, - DROP COLUMN IF EXISTS phase, - DROP COLUMN IF EXISTS selection_mode, - DROP COLUMN IF EXISTS selection_revision, - DROP COLUMN IF EXISTS selected_content_id, - DROP COLUMN IF EXISTS selected_file_id, - DROP COLUMN IF EXISTS selected_library_id; - -ALTER TABLE watch_together_rooms - ADD CONSTRAINT watch_together_rooms_status_check - CHECK (status IN ('active', 'closed')); - -DROP INDEX IF EXISTS idx_watch_together_rooms_phase_created_at; -CREATE INDEX idx_watch_together_rooms_status_created_at - ON watch_together_rooms (status, created_at DESC); diff --git a/migrations/071_watch_together_suggestions.down.sql b/migrations/071_watch_together_suggestions.down.sql deleted file mode 100644 index a336a674..00000000 --- a/migrations/071_watch_together_suggestions.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_wt_votes_suggestion; -DROP INDEX IF EXISTS idx_wt_suggestions_room; -DROP TABLE IF EXISTS watch_together_votes; -DROP TABLE IF EXISTS watch_together_suggestions; diff --git a/migrations/072_watch_together_playback_state.down.sql b/migrations/072_watch_together_playback_state.down.sql deleted file mode 100644 index 799be2b4..00000000 --- a/migrations/072_watch_together_playback_state.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE watch_together_rooms - DROP CONSTRAINT IF EXISTS watch_together_rooms_playback_state_check, - DROP COLUMN IF EXISTS resume_on_ready, - DROP COLUMN IF EXISTS playback_state; diff --git a/migrations/073_device_login_requests.down.sql b/migrations/073_device_login_requests.down.sql deleted file mode 100644 index 1e178d23..00000000 --- a/migrations/073_device_login_requests.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS device_login_requests; diff --git a/migrations/074_scanned_media_roots.down.sql b/migrations/074_scanned_media_roots.down.sql deleted file mode 100644 index 7535ebb9..00000000 --- a/migrations/074_scanned_media_roots.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS scanned_media_roots; diff --git a/migrations/075_media_root_overrides.down.sql b/migrations/075_media_root_overrides.down.sql deleted file mode 100644 index 30f5f4f8..00000000 --- a/migrations/075_media_root_overrides.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS media_root_overrides; diff --git a/migrations/076_media_file_root_and_variant_metadata.down.sql b/migrations/076_media_file_root_and_variant_metadata.down.sql deleted file mode 100644 index b059f105..00000000 --- a/migrations/076_media_file_root_and_variant_metadata.down.sql +++ /dev/null @@ -1,15 +0,0 @@ -DROP INDEX IF EXISTS idx_media_files_variant_group; -DROP INDEX IF EXISTS idx_media_files_folder_root; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS multi_episode_end, - DROP COLUMN IF EXISTS multi_episode_start, - DROP COLUMN IF EXISTS presentation_part_total, - DROP COLUMN IF EXISTS presentation_part_index, - DROP COLUMN IF EXISTS presentation_group_key, - DROP COLUMN IF EXISTS presentation_kind, - DROP COLUMN IF EXISTS edition_source, - DROP COLUMN IF EXISTS edition_confidence, - DROP COLUMN IF EXISTS edition_key, - DROP COLUMN IF EXISTS edition_raw, - DROP COLUMN IF EXISTS canonical_root_path; diff --git a/migrations/077_user_progress_edition_hints.down.sql b/migrations/077_user_progress_edition_hints.down.sql deleted file mode 100644 index 7a013c4b..00000000 --- a/migrations/077_user_progress_edition_hints.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE user_watch_progress - DROP COLUMN IF EXISTS last_edition_key; diff --git a/migrations/077_user_progress_edition_hints.up.sql b/migrations/077_user_progress_edition_hints.up.sql deleted file mode 100644 index b8a219fc..00000000 --- a/migrations/077_user_progress_edition_hints.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE user_watch_progress - ADD COLUMN IF NOT EXISTS last_edition_key TEXT; diff --git a/migrations/078_content_groups_and_locations.down.sql b/migrations/078_content_groups_and_locations.down.sql deleted file mode 100644 index f0ac33bd..00000000 --- a/migrations/078_content_groups_and_locations.down.sql +++ /dev/null @@ -1,18 +0,0 @@ -DROP TABLE IF EXISTS media_group_overrides; -DROP TABLE IF EXISTS media_item_groups; -DROP TABLE IF EXISTS media_group_locations; -DROP TABLE IF EXISTS observed_media_locations; -DROP TABLE IF EXISTS scanned_media_groups; - -DROP INDEX IF EXISTS idx_media_files_folder_observed_root; -DROP INDEX IF EXISTS idx_media_files_folder_group; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS identity_json, - DROP COLUMN IF EXISTS identity_confidence, - DROP COLUMN IF EXISTS base_type, - DROP COLUMN IF EXISTS base_year, - DROP COLUMN IF EXISTS base_title, - DROP COLUMN IF EXISTS group_key_version, - DROP COLUMN IF EXISTS content_group_key, - DROP COLUMN IF EXISTS observed_root_path; diff --git a/migrations/079_media_item_provider_ids.down.sql b/migrations/079_media_item_provider_ids.down.sql deleted file mode 100644 index 681cdd9b..00000000 --- a/migrations/079_media_item_provider_ids.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_media_item_provider_ids_provider_id; -DROP TABLE IF EXISTS media_item_provider_ids; diff --git a/migrations/080_series_match_queue.down.sql b/migrations/080_series_match_queue.down.sql deleted file mode 100644 index 652e7597..00000000 --- a/migrations/080_series_match_queue.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX IF EXISTS idx_series_match_queue_folder; -DROP INDEX IF EXISTS idx_series_match_queue_attempt; -DROP TABLE IF EXISTS series_match_queue; diff --git a/migrations/081_series_root_match_queue.down.sql b/migrations/081_series_root_match_queue.down.sql deleted file mode 100644 index bcd84081..00000000 --- a/migrations/081_series_root_match_queue.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS series_root_match_queue; diff --git a/migrations/082_media_files_file_modified_at.down.sql b/migrations/082_media_files_file_modified_at.down.sql deleted file mode 100644 index 8a4a15af..00000000 --- a/migrations/082_media_files_file_modified_at.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_media_files_folder_file_modified_at; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS file_modified_at; diff --git a/migrations/082_media_files_file_modified_at.up.sql b/migrations/082_media_files_file_modified_at.up.sql deleted file mode 100644 index f8a2c5af..00000000 --- a/migrations/082_media_files_file_modified_at.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE media_files - ADD COLUMN IF NOT EXISTS file_modified_at TIMESTAMPTZ; - -CREATE INDEX IF NOT EXISTS idx_media_files_folder_file_modified_at - ON media_files (media_folder_id, file_modified_at); diff --git a/migrations/083_movie_match_queue.down.sql b/migrations/083_movie_match_queue.down.sql deleted file mode 100644 index 306ffed7..00000000 --- a/migrations/083_movie_match_queue.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS movie_match_queue; diff --git a/migrations/084_series_root_match_queue_available_at.down.sql b/migrations/084_series_root_match_queue_available_at.down.sql deleted file mode 100644 index 56012149..00000000 --- a/migrations/084_series_root_match_queue_available_at.down.sql +++ /dev/null @@ -1,7 +0,0 @@ -DROP INDEX IF EXISTS idx_series_root_match_queue_available; - -CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_attempt - ON series_root_match_queue (last_attempted_at ASC NULLS FIRST, media_folder_id, observed_root_path); - -ALTER TABLE series_root_match_queue - DROP COLUMN IF EXISTS available_at; diff --git a/migrations/084_series_root_match_queue_available_at.up.sql b/migrations/084_series_root_match_queue_available_at.up.sql deleted file mode 100644 index 0dae7550..00000000 --- a/migrations/084_series_root_match_queue_available_at.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -ALTER TABLE series_root_match_queue - ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); - -DROP INDEX IF EXISTS idx_series_root_match_queue_attempt; - -CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_available - ON series_root_match_queue (available_at, last_attempted_at, media_folder_id, observed_root_path); diff --git a/migrations/085_scan_runs.down.sql b/migrations/085_scan_runs.down.sql deleted file mode 100644 index 74590041..00000000 --- a/migrations/085_scan_runs.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS scan_runs; diff --git a/migrations/086_s3_public_private_split.down.sql b/migrations/086_s3_public_private_split.down.sql deleted file mode 100644 index 484262b0..00000000 --- a/migrations/086_s3_public_private_split.down.sql +++ /dev/null @@ -1,22 +0,0 @@ -DELETE FROM server_settings -WHERE key IN ( - 's3.public_endpoint', - 's3.public_read_endpoint', - 's3.public_region', - 's3.public_path_style', - 's3.public_bucket', - 's3.public_key_prefix', - 's3.public_access_key', - 's3.public_secret_key', - 's3.public_url_auth', - 's3.public_token_secret', - 's3.public_token_param', - 's3.public_token_ttl', - 's3.private_endpoint', - 's3.private_region', - 's3.private_path_style', - 's3.private_bucket', - 's3.private_key_prefix', - 's3.private_access_key', - 's3.private_secret_key' -); diff --git a/migrations/087_catalog_query_performance_indexes.down.sql b/migrations/087_catalog_query_performance_indexes.down.sql deleted file mode 100644 index f1cd7c03..00000000 --- a/migrations/087_catalog_query_performance_indexes.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -DROP INDEX IF EXISTS public.idx_episodes_series_air_date; - -DROP INDEX IF EXISTS public.idx_media_files_active_content_folder; - -DROP INDEX IF EXISTS public.idx_media_items_content_rating; - -DROP INDEX IF EXISTS public.idx_media_items_original_language; - -DROP INDEX IF EXISTS public.idx_media_items_countries; - -DROP INDEX IF EXISTS public.idx_media_items_networks; - -DROP INDEX IF EXISTS public.idx_media_items_studios; diff --git a/migrations/088_media_item_provider_id_types.down.sql b/migrations/088_media_item_provider_id_types.down.sql deleted file mode 100644 index 0a2e52f8..00000000 --- a/migrations/088_media_item_provider_id_types.down.sql +++ /dev/null @@ -1,27 +0,0 @@ -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM ( - SELECT provider, provider_id - FROM media_item_provider_ids - GROUP BY provider, provider_id - HAVING COUNT(*) > 1 - ) duplicate_ids - ) THEN - RAISE EXCEPTION 'cross-type provider ids exist; resolve duplicates before rolling back migration 088'; - END IF; -END $$; - -ALTER TABLE media_item_provider_ids - DROP CONSTRAINT IF EXISTS media_item_provider_ids_provider_provider_id_item_type_key; - -ALTER TABLE media_item_provider_ids - DROP COLUMN IF EXISTS item_type; - -ALTER TABLE media_item_provider_ids - ADD CONSTRAINT media_item_provider_ids_provider_provider_id_key - UNIQUE (provider, provider_id); - -CREATE INDEX IF NOT EXISTS idx_media_item_provider_ids_provider_id - ON media_item_provider_ids (provider, provider_id); diff --git a/migrations/089_metadata_refresh_debt.down.sql b/migrations/089_metadata_refresh_debt.down.sql deleted file mode 100644 index b4de6893..00000000 --- a/migrations/089_metadata_refresh_debt.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS metadata_refresh_debt; diff --git a/migrations/090_episode_catalog_active_file_index.down.sql b/migrations/090_episode_catalog_active_file_index.down.sql deleted file mode 100644 index 114716ed..00000000 --- a/migrations/090_episode_catalog_active_file_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_files_active_episode_folder; diff --git a/migrations/090_episode_catalog_active_file_index.up.sql b/migrations/090_episode_catalog_active_file_index.up.sql deleted file mode 100644 index 6c3af985..00000000 --- a/migrations/090_episode_catalog_active_file_index.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_media_files_active_episode_folder -ON public.media_files USING btree (episode_id, media_folder_id) -WHERE ((episode_id IS NOT NULL) AND (missing_since IS NULL)); diff --git a/migrations/091_episode_release_date_sort_index.down.sql b/migrations/091_episode_release_date_sort_index.down.sql deleted file mode 100644 index 82750e2c..00000000 --- a/migrations/091_episode_release_date_sort_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.idx_episodes_air_date_content; diff --git a/migrations/091_episode_release_date_sort_index.up.sql b/migrations/091_episode_release_date_sort_index.up.sql deleted file mode 100644 index 6abee89f..00000000 --- a/migrations/091_episode_release_date_sort_index.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_episodes_air_date_content -ON public.episodes USING btree (air_date DESC, content_id) -WHERE (air_date IS NOT NULL); diff --git a/migrations/092_episode_libraries.down.sql b/migrations/092_episode_libraries.down.sql deleted file mode 100644 index 85142d88..00000000 --- a/migrations/092_episode_libraries.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS public.idx_episode_libraries_folder_first_seen; - -DROP INDEX IF EXISTS public.idx_episode_libraries_folder_episode; - -DROP TABLE IF EXISTS public.episode_libraries; diff --git a/migrations/093_user_max_profiles_floor.down.sql b/migrations/093_user_max_profiles_floor.down.sql deleted file mode 100644 index f745a5f1..00000000 --- a/migrations/093_user_max_profiles_floor.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE public.users - DROP CONSTRAINT IF EXISTS users_max_profiles_min_check, - ALTER COLUMN max_profiles SET DEFAULT 0; diff --git a/migrations/094_profiles_is_primary.down.sql b/migrations/094_profiles_is_primary.down.sql deleted file mode 100644 index 5db308f1..00000000 --- a/migrations/094_profiles_is_primary.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS user_profiles_primary_per_user; - -ALTER TABLE user_profiles - DROP COLUMN IF EXISTS is_primary; diff --git a/migrations/095_jellyfin_compat_session_ttl_default.down.sql b/migrations/095_jellyfin_compat_session_ttl_default.down.sql deleted file mode 100644 index 220de58f..00000000 --- a/migrations/095_jellyfin_compat_session_ttl_default.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE server_settings -SET value = '24h' -WHERE key = 'jellyfin_compat.session_ttl' - AND value = '87600h'; diff --git a/migrations/095_jellyfin_compat_session_ttl_default.up.sql b/migrations/095_jellyfin_compat_session_ttl_default.up.sql deleted file mode 100644 index 574c5884..00000000 --- a/migrations/095_jellyfin_compat_session_ttl_default.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE server_settings -SET value = '87600h' -WHERE key = 'jellyfin_compat.session_ttl' - AND value = '24h'; diff --git a/migrations/096_user_device_settings.down.sql b/migrations/096_user_device_settings.down.sql deleted file mode 100644 index 6c7748b4..00000000 --- a/migrations/096_user_device_settings.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.user_device_settings; diff --git a/migrations/097_user_device_settings_profile_scope.down.sql b/migrations/097_user_device_settings_profile_scope.down.sql deleted file mode 100644 index 44a02357..00000000 --- a/migrations/097_user_device_settings_profile_scope.down.sql +++ /dev/null @@ -1,42 +0,0 @@ -ALTER TABLE public.user_device_settings RENAME TO user_device_settings_profile_scoped_legacy; - -CREATE TABLE IF NOT EXISTS public.user_device_settings_legacy_restore ( - user_id integer NOT NULL, - device_id text NOT NULL, - key text NOT NULL, - value text NOT NULL, - device_name text NOT NULL DEFAULT '', - device_platform text NOT NULL DEFAULT '', - updated_at timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT user_device_settings_legacy_restore_pkey PRIMARY KEY (user_id, device_id, key), - CONSTRAINT user_device_settings_legacy_restore_user_id_fkey - FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE -); - -INSERT INTO public.user_device_settings_legacy_restore ( - user_id, - device_id, - key, - value, - device_name, - device_platform, - updated_at -) -SELECT DISTINCT ON (user_id, device_id, key) - user_id, - device_id, - key, - value, - device_name, - device_platform, - updated_at -FROM public.user_device_settings_profile_scoped_legacy -ORDER BY user_id, device_id, key, updated_at DESC, profile_id ASC; - -CREATE INDEX IF NOT EXISTS idx_user_device_settings_legacy_restore_user_key - ON public.user_device_settings_legacy_restore (user_id, key); - -DROP TABLE public.user_device_settings_profile_scoped_legacy; - -ALTER TABLE public.user_device_settings_legacy_restore RENAME TO user_device_settings; -ALTER INDEX public.idx_user_device_settings_legacy_restore_user_key RENAME TO idx_user_device_settings_user_key; diff --git a/migrations/098_playback_settings_device_scope.down.sql b/migrations/098_playback_settings_device_scope.down.sql deleted file mode 100644 index f470d7d4..00000000 --- a/migrations/098_playback_settings_device_scope.down.sql +++ /dev/null @@ -1,19 +0,0 @@ -INSERT INTO public.user_settings ( - user_id, - key, - value -) -SELECT DISTINCT ON (user_id, key) - user_id, - key, - value -FROM public.user_device_settings -WHERE key IN ( - 'playback.preferred_quality', - 'playback.audio_language', - 'playback.auto_skip_intro', - 'playback.auto_skip_credits', - 'playback.auto_play_next' -) -ORDER BY user_id, key, updated_at DESC, profile_id ASC, device_id ASC -ON CONFLICT (user_id, key) DO NOTHING; diff --git a/migrations/099_media_item_keywords.down.sql b/migrations/099_media_item_keywords.down.sql deleted file mode 100644 index 71f86d07..00000000 --- a/migrations/099_media_item_keywords.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.media_items -DROP COLUMN IF EXISTS keywords; diff --git a/migrations/099_media_item_keywords.up.sql b/migrations/099_media_item_keywords.up.sql deleted file mode 100644 index bd2f8140..00000000 --- a/migrations/099_media_item_keywords.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.media_items -ADD COLUMN IF NOT EXISTS keywords TEXT[] NOT NULL DEFAULT '{}'::TEXT[]; diff --git a/migrations/100_metadata_refresh_debt_targets.down.sql b/migrations/100_metadata_refresh_debt_targets.down.sql deleted file mode 100644 index a2f7df32..00000000 --- a/migrations/100_metadata_refresh_debt_targets.down.sql +++ /dev/null @@ -1,20 +0,0 @@ -DELETE FROM metadata_refresh_debt -WHERE target_type <> 'item'; - -ALTER TABLE metadata_refresh_debt - DROP CONSTRAINT IF EXISTS metadata_refresh_debt_pkey; - -ALTER TABLE metadata_refresh_debt - DROP CONSTRAINT IF EXISTS metadata_refresh_debt_target_type_check; - -ALTER TABLE metadata_refresh_debt - ADD PRIMARY KEY (content_id); - -ALTER TABLE metadata_refresh_debt - ADD CONSTRAINT metadata_refresh_debt_content_id_fkey - FOREIGN KEY (content_id) REFERENCES media_items(content_id) ON DELETE CASCADE; - -DROP INDEX IF EXISTS idx_metadata_refresh_debt_target_due; - -ALTER TABLE metadata_refresh_debt - DROP COLUMN IF EXISTS target_type; diff --git a/migrations/101_watch_history_identity.down.sql b/migrations/101_watch_history_identity.down.sql deleted file mode 100644 index c5cc9e55..00000000 --- a/migrations/101_watch_history_identity.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.user_watch_history DROP COLUMN watch_identity; diff --git a/migrations/101_watch_history_identity.up.sql b/migrations/101_watch_history_identity.up.sql deleted file mode 100644 index 0916b387..00000000 --- a/migrations/101_watch_history_identity.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.user_watch_history - ADD COLUMN watch_identity JSONB NOT NULL DEFAULT '{}'::jsonb; diff --git a/migrations/102_catalog_perf_indexes.down.sql b/migrations/102_catalog_perf_indexes.down.sql deleted file mode 100644 index f5e21033..00000000 --- a/migrations/102_catalog_perf_indexes.down.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Recreate dropped duplicate indexes (matching original 001 schema) -CREATE INDEX IF NOT EXISTS idx_api_keys_api_key -ON public.api_keys USING btree (api_key); - -CREATE INDEX IF NOT EXISTS idx_seasons_series -ON public.seasons USING btree (series_id, season_number); - -CREATE INDEX IF NOT EXISTS idx_episodes_series -ON public.episodes USING btree (series_id, season_number, episode_number); - --- Drop new indexes -DROP INDEX IF EXISTS public.idx_item_people_person_kind; -DROP INDEX IF EXISTS public.idx_uwp_profile_in_progress; -DROP INDEX IF EXISTS public.idx_uwp_profile_completed; -DROP INDEX IF EXISTS public.idx_media_items_keywords; -DROP INDEX IF EXISTS public.idx_media_items_tvdb_id; -DROP INDEX IF EXISTS public.idx_media_items_imdb_id; -DROP INDEX IF EXISTS public.idx_media_items_tmdb_id; -DROP INDEX IF EXISTS public.idx_media_items_release_date; -DROP INDEX IF EXISTS public.idx_media_items_sort_key; -DROP INDEX IF EXISTS public.idx_people_name_trgm; --- idx_people_name_lower is owned by migration 007; do not drop on 102 rollback. --- Note: pg_trgm extension is left installed; harmless if unused. diff --git a/migrations/103_media_items_last_air_date_denorm.down.sql b/migrations/103_media_items_last_air_date_denorm.down.sql deleted file mode 100644 index 7781cd51..00000000 --- a/migrations/103_media_items_last_air_date_denorm.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_items_last_air_date_at; -ALTER TABLE public.media_items DROP COLUMN IF EXISTS last_air_date_at; diff --git a/migrations/104_media_files_language_arrays.down.sql b/migrations/104_media_files_language_arrays.down.sql deleted file mode 100644 index 080021f6..00000000 --- a/migrations/104_media_files_language_arrays.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_files_subtitle_lang_gin; -DROP INDEX IF EXISTS public.idx_media_files_audio_lang_gin; -ALTER TABLE public.media_files DROP COLUMN IF EXISTS subtitle_language_codes; -ALTER TABLE public.media_files DROP COLUMN IF EXISTS audio_language_codes; -DROP FUNCTION IF EXISTS public.jsonb_track_language_codes(jsonb); diff --git a/migrations/105_media_items_title_normalized.down.sql b/migrations/105_media_items_title_normalized.down.sql deleted file mode 100644 index 8de78611..00000000 --- a/migrations/105_media_items_title_normalized.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; -ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; diff --git a/migrations/107_recently_added_section_index.down.sql b/migrations/107_recently_added_section_index.down.sql deleted file mode 100644 index df574510..00000000 --- a/migrations/107_recently_added_section_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.idx_item_libraries_folder_seen_content; diff --git a/migrations/107_recently_added_section_index.up.sql b/migrations/107_recently_added_section_index.up.sql deleted file mode 100644 index 1e16e67b..00000000 --- a/migrations/107_recently_added_section_index.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_item_libraries_folder_seen_content -ON public.media_item_libraries USING btree (media_folder_id, first_seen_at DESC, content_id ASC); diff --git a/migrations/108_intro_marker_detection.down.sql b/migrations/108_intro_marker_detection.down.sql deleted file mode 100644 index c5dbe3a8..00000000 --- a/migrations/108_intro_marker_detection.down.sql +++ /dev/null @@ -1,12 +0,0 @@ -DROP TABLE IF EXISTS intro_season_analysis_state; -DROP INDEX IF EXISTS idx_media_intro_fingerprints_hash; -DROP TABLE IF EXISTS media_intro_fingerprints; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS intro_markers_detected_at, - DROP COLUMN IF EXISTS intro_markers_algorithm, - DROP COLUMN IF EXISTS intro_markers_confidence, - DROP COLUMN IF EXISTS intro_markers_source; - -ALTER TABLE media_folders - DROP COLUMN IF EXISTS intro_detection_enabled; diff --git a/migrations/109_online_marker_sources.down.sql b/migrations/109_online_marker_sources.down.sql deleted file mode 100644 index e03485b6..00000000 --- a/migrations/109_online_marker_sources.down.sql +++ /dev/null @@ -1,10 +0,0 @@ -DELETE FROM server_settings -WHERE key IN ('markers.mode', 'markers.lazy_playback'); - -ALTER TABLE media_files - DROP COLUMN IF EXISTS credits_markers_detected_at, - DROP COLUMN IF EXISTS credits_markers_algorithm, - DROP COLUMN IF EXISTS credits_markers_confidence, - DROP COLUMN IF EXISTS credits_markers_provider, - DROP COLUMN IF EXISTS credits_markers_source, - DROP COLUMN IF EXISTS intro_markers_provider; diff --git a/migrations/110_watch_provider_foundation.down.sql b/migrations/110_watch_provider_foundation.down.sql deleted file mode 100644 index 5e847fff..00000000 --- a/migrations/110_watch_provider_foundation.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP TABLE IF EXISTS public.watch_provider_scrobble_sessions; -DROP TABLE IF EXISTS public.watch_provider_history_exports; -DROP TABLE IF EXISTS public.watch_provider_sync_runs; -DROP TABLE IF EXISTS public.watch_provider_auth_sessions; -DROP TABLE IF EXISTS public.watch_provider_connections; diff --git a/migrations/111_watch_provider_sync_hourly.down.sql b/migrations/111_watch_provider_sync_hourly.down.sql deleted file mode 100644 index fc4e4047..00000000 --- a/migrations/111_watch_provider_sync_hourly.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE public.task_triggers -SET interval = 15 * 60 * 1000 -WHERE task_key = 'sync_watch_providers' - AND type = 'interval' - AND interval = 60 * 60 * 1000; diff --git a/migrations/111_watch_provider_sync_hourly.up.sql b/migrations/111_watch_provider_sync_hourly.up.sql deleted file mode 100644 index 5ae66f86..00000000 --- a/migrations/111_watch_provider_sync_hourly.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE public.task_triggers -SET interval = 60 * 60 * 1000 -WHERE task_key = 'sync_watch_providers' - AND type = 'interval' - AND interval = 15 * 60 * 1000; diff --git a/migrations/112_watch_provider_unwatched_export.down.sql b/migrations/112_watch_provider_unwatched_export.down.sql deleted file mode 100644 index 53dc9cd6..00000000 --- a/migrations/112_watch_provider_unwatched_export.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.watch_provider_connections - DROP COLUMN IF EXISTS export_unwatched_enabled; diff --git a/migrations/112_watch_provider_unwatched_export.up.sql b/migrations/112_watch_provider_unwatched_export.up.sql deleted file mode 100644 index 42774174..00000000 --- a/migrations/112_watch_provider_unwatched_export.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.watch_provider_connections - ADD COLUMN export_unwatched_enabled boolean NOT NULL DEFAULT false; diff --git a/migrations/113_watch_provider_scrobble_session_fields.down.sql b/migrations/113_watch_provider_scrobble_session_fields.down.sql deleted file mode 100644 index a0dcbeaf..00000000 --- a/migrations/113_watch_provider_scrobble_session_fields.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -ALTER TABLE public.watch_provider_scrobble_sessions - DROP COLUMN IF EXISTS completed, - DROP COLUMN IF EXISTS duration_seconds, - DROP COLUMN IF EXISTS episode_number, - DROP COLUMN IF EXISTS season_number, - DROP COLUMN IF EXISTS series_tvdb_id, - DROP COLUMN IF EXISTS series_tmdb_id, - DROP COLUMN IF EXISTS series_imdb_id, - DROP COLUMN IF EXISTS tvdb_id, - DROP COLUMN IF EXISTS tmdb_id, - DROP COLUMN IF EXISTS imdb_id, - DROP COLUMN IF EXISTS kind, - DROP COLUMN IF EXISTS provider_item_key; diff --git a/migrations/114_watch_provider_sync_cursors.down.sql b/migrations/114_watch_provider_sync_cursors.down.sql deleted file mode 100644 index 5b339a29..00000000 --- a/migrations/114_watch_provider_sync_cursors.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.watch_provider_connections - DROP COLUMN sync_cursors; diff --git a/migrations/114_watch_provider_sync_cursors.up.sql b/migrations/114_watch_provider_sync_cursors.up.sql deleted file mode 100644 index aba91f72..00000000 --- a/migrations/114_watch_provider_sync_cursors.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.watch_provider_connections - ADD COLUMN sync_cursors jsonb NOT NULL DEFAULT '{}'::jsonb; diff --git a/migrations/115_watch_provider_favorites.down.sql b/migrations/115_watch_provider_favorites.down.sql deleted file mode 100644 index 59446a92..00000000 --- a/migrations/115_watch_provider_favorites.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -DROP TABLE IF EXISTS public.watch_provider_favorite_items; - -ALTER TABLE public.watch_provider_sync_runs - DROP COLUMN IF EXISTS favorite_removals_sent, - DROP COLUMN IF EXISTS outbound_favorites_sent, - DROP COLUMN IF EXISTS outbound_favorites_found, - DROP COLUMN IF EXISTS inbound_favorites_imported, - DROP COLUMN IF EXISTS inbound_favorites_found; - -ALTER TABLE public.watch_provider_connections - DROP COLUMN IF EXISTS last_favorites_sync_at, - DROP COLUMN IF EXISTS sync_favorite_removals_enabled, - DROP COLUMN IF EXISTS export_favorites_enabled, - DROP COLUMN IF EXISTS import_favorites_enabled; diff --git a/migrations/116_add_trakt_library_collection_type.up.sql b/migrations/116_add_trakt_library_collection_type.up.sql deleted file mode 100644 index 1af653c8..00000000 --- a/migrations/116_add_trakt_library_collection_type.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; - -ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check - CHECK (collection_type = ANY (ARRAY['manual', 'smart', 'mdblist', 'tmdb', 'trakt'])); - diff --git a/migrations/117_library_collection_management_metadata.down.sql b/migrations/117_library_collection_management_metadata.down.sql deleted file mode 100644 index 9cfbe819..00000000 --- a/migrations/117_library_collection_management_metadata.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP INDEX IF EXISTS idx_library_collections_section_management_key; - -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; - -ALTER TABLE library_collections - DROP COLUMN IF EXISTS management_key, - DROP COLUMN IF EXISTS management_source, - DROP COLUMN IF EXISTS management_mode; diff --git a/migrations/118_user_collection_imports.down.sql b/migrations/118_user_collection_imports.down.sql deleted file mode 100644 index 81c79623..00000000 --- a/migrations/118_user_collection_imports.down.sql +++ /dev/null @@ -1,12 +0,0 @@ -DROP INDEX IF EXISTS idx_user_collections_next_sync_due; - -ALTER TABLE user_personal_collections - DROP COLUMN IF EXISTS item_count, - DROP COLUMN IF EXISTS last_sync_message, - DROP COLUMN IF EXISTS last_sync_status, - DROP COLUMN IF EXISTS last_sync_at, - DROP COLUMN IF EXISTS next_sync_at, - DROP COLUMN IF EXISTS sync_schedule, - DROP COLUMN IF EXISTS source_config, - DROP COLUMN IF EXISTS source_url, - DROP COLUMN IF EXISTS description; diff --git a/migrations/119_user_collection_server_visibility.down.sql b/migrations/119_user_collection_server_visibility.down.sql deleted file mode 100644 index 7b9aa934..00000000 --- a/migrations/119_user_collection_server_visibility.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_user_collections_server_visible; - -ALTER TABLE user_personal_collections - DROP COLUMN IF EXISTS include_in_server_collections; diff --git a/migrations/120_user_collection_poster.down.sql b/migrations/120_user_collection_poster.down.sql deleted file mode 100644 index a0ac98be..00000000 --- a/migrations/120_user_collection_poster.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE user_personal_collections - DROP COLUMN IF EXISTS poster_thumbhash, - DROP COLUMN IF EXISTS poster_url; diff --git a/migrations/121_canonicalize_language_codes.down.sql b/migrations/121_canonicalize_language_codes.down.sql deleted file mode 100644 index 3555b75f..00000000 --- a/migrations/121_canonicalize_language_codes.down.sql +++ /dev/null @@ -1,38 +0,0 @@ --- Rollback canonicalization. Original code values were lossy on the way --- down (multiple raw forms collapsed to one), so the data rewrite cannot --- be reversed. This rollback restores the previous LOWER-only function --- and re-creates the STORED columns; downstream UI will see whatever --- canonical values the up migration left behind. - -DROP INDEX IF EXISTS idx_media_files_audio_lang_gin; -DROP INDEX IF EXISTS idx_media_files_subtitle_lang_gin; - -ALTER TABLE public.media_files DROP COLUMN IF EXISTS audio_language_codes; -ALTER TABLE public.media_files DROP COLUMN IF EXISTS subtitle_language_codes; - -CREATE OR REPLACE FUNCTION public.jsonb_track_language_codes(tracks jsonb) -RETURNS text[] -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -AS $$ - SELECT array_agg(LOWER(elem->>'language')) - FROM jsonb_array_elements(COALESCE(tracks, '[]'::jsonb)) AS elem - WHERE (elem->>'language') IS NOT NULL AND (elem->>'language') <> '' -$$; - -ALTER TABLE public.media_files -ADD COLUMN IF NOT EXISTS audio_language_codes text[] - GENERATED ALWAYS AS (public.jsonb_track_language_codes(audio_tracks)) STORED; - -ALTER TABLE public.media_files -ADD COLUMN IF NOT EXISTS subtitle_language_codes text[] - GENERATED ALWAYS AS (public.jsonb_track_language_codes(subtitle_tracks)) STORED; - -CREATE INDEX IF NOT EXISTS idx_media_files_audio_lang_gin -ON public.media_files USING gin (audio_language_codes); - -CREATE INDEX IF NOT EXISTS idx_media_files_subtitle_lang_gin -ON public.media_files USING gin (subtitle_language_codes); - -DROP FUNCTION IF EXISTS public.canonical_language_code(text); diff --git a/migrations/122_user_personal_collections_sort_order.down.sql b/migrations/122_user_personal_collections_sort_order.down.sql deleted file mode 100644 index 841a2314..00000000 --- a/migrations/122_user_personal_collections_sort_order.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_user_personal_collections_sort_order; -ALTER TABLE user_personal_collections DROP COLUMN sort_order; diff --git a/migrations/123_collection_groups.down.sql b/migrations/123_collection_groups.down.sql deleted file mode 100644 index b8aa9ccb..00000000 --- a/migrations/123_collection_groups.down.sql +++ /dev/null @@ -1,7 +0,0 @@ -DROP INDEX IF EXISTS idx_user_personal_collections_group_order; - -ALTER TABLE user_personal_collections DROP COLUMN IF EXISTS group_label; -ALTER TABLE library_collections DROP COLUMN IF EXISTS group_label; - -DROP TABLE IF EXISTS user_collection_groups; -DROP TABLE IF EXISTS library_collection_groups; diff --git a/migrations/124_collection_group_identity_and_membership.down.sql b/migrations/124_collection_group_identity_and_membership.down.sql deleted file mode 100644 index 099d0a5b..00000000 --- a/migrations/124_collection_group_identity_and_membership.down.sql +++ /dev/null @@ -1,64 +0,0 @@ -DROP INDEX IF EXISTS idx_user_personal_collections_group_id_order; - -ALTER TABLE user_personal_collections - DROP CONSTRAINT IF EXISTS user_personal_collections_group_fkey; - -UPDATE user_personal_collections upc -SET group_label = COALESCE(g.label, '') -FROM user_collection_groups g -WHERE g.user_id = upc.user_id - AND g.id = upc.group_id; - -ALTER TABLE user_personal_collections - DROP COLUMN IF EXISTS group_id; - -ALTER TABLE user_collection_groups - DROP CONSTRAINT IF EXISTS user_collection_groups_user_slug_unique, - DROP CONSTRAINT IF EXISTS user_collection_groups_user_id_unique, - DROP CONSTRAINT IF EXISTS user_collection_groups_default_sort_mode_check, - DROP COLUMN IF EXISTS default_sort_mode, - DROP COLUMN IF EXISTS slug, - DROP COLUMN IF EXISTS name, - DROP COLUMN IF EXISTS id; - -ALTER TABLE media_folders - DROP COLUMN IF EXISTS collection_ungrouped_sort_order; - -DROP INDEX IF EXISTS idx_library_collection_libraries_group_order; - -ALTER TABLE library_collection_libraries - DROP CONSTRAINT IF EXISTS library_collection_libraries_group_library_fkey; - -UPDATE library_collections lc -SET group_label = COALESCE(g.label, '') -FROM library_collection_libraries lcl -LEFT JOIN library_collection_groups g - ON g.id = lcl.group_id - AND g.library_id = lcl.library_id -WHERE lcl.collection_id = lc.id - AND lc.library_id = lcl.library_id; - -ALTER TABLE library_collection_libraries - DROP COLUMN IF EXISTS updated_at, - DROP COLUMN IF EXISTS created_at, - DROP COLUMN IF EXISTS sort_order, - DROP COLUMN IF EXISTS group_id; - -DROP INDEX IF EXISTS idx_library_collection_groups_order; -DROP INDEX IF EXISTS idx_library_collection_groups_user_unique; - -DELETE FROM library_collection_groups -WHERE kind = 'user_collections' - OR label = 'user-collections'; - -ALTER TABLE library_collection_groups - DROP CONSTRAINT IF EXISTS library_collection_groups_library_slug_unique, - DROP CONSTRAINT IF EXISTS library_collection_groups_id_library_unique, - DROP CONSTRAINT IF EXISTS library_collection_groups_id_unique, - DROP CONSTRAINT IF EXISTS library_collection_groups_default_sort_mode_check, - DROP CONSTRAINT IF EXISTS library_collection_groups_kind_check, - DROP COLUMN IF EXISTS default_sort_mode, - DROP COLUMN IF EXISTS kind, - DROP COLUMN IF EXISTS slug, - DROP COLUMN IF EXISTS name, - DROP COLUMN IF EXISTS id; diff --git a/migrations/125_collection_smart_count_cache.down.sql b/migrations/125_collection_smart_count_cache.down.sql deleted file mode 100644 index f1d2ea6e..00000000 --- a/migrations/125_collection_smart_count_cache.down.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS idx_library_collections_smart_count_sort; - -ALTER TABLE library_collections - DROP COLUMN IF EXISTS item_count_cached_at, - DROP COLUMN IF EXISTS item_count_cached; diff --git a/migrations/125_collection_smart_count_cache.up.sql b/migrations/125_collection_smart_count_cache.up.sql deleted file mode 100644 index 7e374918..00000000 --- a/migrations/125_collection_smart_count_cache.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE library_collections - ADD COLUMN item_count_cached INTEGER NOT NULL DEFAULT 0, - ADD COLUMN item_count_cached_at TIMESTAMPTZ; - -CREATE INDEX idx_library_collections_smart_count_sort - ON library_collections (collection_type, item_count_cached DESC, title); diff --git a/migrations/126_oauth_session.down.sql b/migrations/126_oauth_session.down.sql deleted file mode 100644 index 59941183..00000000 --- a/migrations/126_oauth_session.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE IF EXISTS oauth_completion; -DROP TABLE IF EXISTS oauth_session; diff --git a/migrations/127_search_synonym_and_ampersand.down.sql b/migrations/127_search_synonym_and_ampersand.down.sql deleted file mode 100644 index c99e686b..00000000 --- a/migrations/127_search_synonym_and_ampersand.down.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Restore the pre-127 normalization: collapse non-alnum to spaces and --- lowercase, but DO NOT strip standalone "and" tokens. "&" and "and" will --- once again be inequivalent in title matching. --- --- The legacy idx_media_items_search (english combined title+overview) is --- intentionally NOT restored: it was unused by the post-migration-105 query --- path, and rolling back to a known-dead index just imposes a GIN rebuild --- cost for no benefit. - -DROP INDEX IF EXISTS public.idx_media_items_search_title_fields; -DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; - -ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; -ALTER TABLE public.media_items - ADD COLUMN title_normalized text - GENERATED ALWAYS AS ( - BTRIM(LOWER(REGEXP_REPLACE(COALESCE(title, ''), '[^[:alnum:]]+', ' ', 'g'))) - ) STORED; - -CREATE INDEX IF NOT EXISTS idx_media_items_title_normalized_trgm - ON public.media_items USING gin (title_normalized public.gin_trgm_ops); - -CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields - ON public.media_items USING gin (( - setweight(to_tsvector('simple', COALESCE(title, '')), 'A') || - setweight(to_tsvector('simple', COALESCE(original_title, '')), 'A') || - setweight(to_tsvector('simple', COALESCE(sort_title, '')), 'B') - )); - -DROP FUNCTION IF EXISTS public.normalize_search_text(text); diff --git a/migrations/128_media_items_show_status.down.sql b/migrations/128_media_items_show_status.down.sql deleted file mode 100644 index 10a3e2c4..00000000 --- a/migrations/128_media_items_show_status.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE media_items - DROP COLUMN show_status; diff --git a/migrations/132_downloaded_subtitles_admin_index.down.sql b/migrations/132_downloaded_subtitles_admin_index.down.sql deleted file mode 100644 index a05e4426..00000000 --- a/migrations/132_downloaded_subtitles_admin_index.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP INDEX IF EXISTS idx_downloaded_subtitles_provider_created; -DROP INDEX IF EXISTS idx_downloaded_subtitles_created; diff --git a/migrations/132_downloaded_subtitles_admin_index.up.sql b/migrations/132_downloaded_subtitles_admin_index.up.sql deleted file mode 100644 index d6a0e8dc..00000000 --- a/migrations/132_downloaded_subtitles_admin_index.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_downloaded_subtitles_created - ON downloaded_subtitles (created_at DESC); - -CREATE INDEX IF NOT EXISTS idx_downloaded_subtitles_provider_created - ON downloaded_subtitles (provider, created_at DESC); diff --git a/migrations/133_library_collection_template_bundle_management.down.sql b/migrations/133_library_collection_template_bundle_management.down.sql deleted file mode 100644 index a260c467..00000000 --- a/migrations/133_library_collection_template_bundle_management.down.sql +++ /dev/null @@ -1,10 +0,0 @@ -DROP INDEX IF EXISTS idx_library_collections_managed_management_key; - -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; - -ALTER TABLE library_collections ADD CONSTRAINT library_collections_management_mode_check - CHECK (management_mode = ANY (ARRAY['manual', 'section'])); - -CREATE UNIQUE INDEX IF NOT EXISTS idx_library_collections_section_management_key - ON library_collections (management_key) - WHERE management_mode = 'section' AND management_key <> ''; diff --git a/migrations/133_library_collection_template_bundle_management.up.sql b/migrations/133_library_collection_template_bundle_management.up.sql deleted file mode 100644 index 4ca4914b..00000000 --- a/migrations/133_library_collection_template_bundle_management.up.sql +++ /dev/null @@ -1,10 +0,0 @@ -ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; - -ALTER TABLE library_collections ADD CONSTRAINT library_collections_management_mode_check - CHECK (management_mode = ANY (ARRAY['manual', 'section', 'template_bundle'])); - -DROP INDEX IF EXISTS idx_library_collections_section_management_key; - -CREATE UNIQUE INDEX IF NOT EXISTS idx_library_collections_managed_management_key - ON library_collections (management_key) - WHERE management_mode <> 'manual' AND management_key <> ''; diff --git a/migrations/134_library_collection_poster_template_origin.down.sql b/migrations/134_library_collection_poster_template_origin.down.sql deleted file mode 100644 index 0c901d97..00000000 --- a/migrations/134_library_collection_poster_template_origin.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_from_template; diff --git a/migrations/134_library_collection_poster_template_origin.up.sql b/migrations/134_library_collection_poster_template_origin.up.sql deleted file mode 100644 index a4227667..00000000 --- a/migrations/134_library_collection_poster_template_origin.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE library_collections - ADD COLUMN IF NOT EXISTS poster_from_template BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/135_webhook_sync_silo_profile_column.down.sql b/migrations/135_webhook_sync_silo_profile_column.down.sql deleted file mode 100644 index 136aae0b..00000000 --- a/migrations/135_webhook_sync_silo_profile_column.down.sql +++ /dev/null @@ -1,30 +0,0 @@ -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'silo_profile_id' - ) AND NOT EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'continuum_profile_id' - ) THEN - ALTER TABLE public.webhook_sync_actor_mappings - RENAME COLUMN silo_profile_id TO continuum_profile_id; - ELSIF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'silo_profile_id' - ) THEN - UPDATE public.webhook_sync_actor_mappings - SET continuum_profile_id = COALESCE(continuum_profile_id, silo_profile_id) - WHERE continuum_profile_id IS NULL - AND silo_profile_id IS NOT NULL; - END IF; -END $$; diff --git a/migrations/135_webhook_sync_silo_profile_column.up.sql b/migrations/135_webhook_sync_silo_profile_column.up.sql deleted file mode 100644 index d8ced74b..00000000 --- a/migrations/135_webhook_sync_silo_profile_column.up.sql +++ /dev/null @@ -1,30 +0,0 @@ -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'continuum_profile_id' - ) AND NOT EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'silo_profile_id' - ) THEN - ALTER TABLE public.webhook_sync_actor_mappings - RENAME COLUMN continuum_profile_id TO silo_profile_id; - ELSIF EXISTS ( - SELECT 1 - FROM information_schema.columns - WHERE table_schema = 'public' - AND table_name = 'webhook_sync_actor_mappings' - AND column_name = 'continuum_profile_id' - ) THEN - UPDATE public.webhook_sync_actor_mappings - SET silo_profile_id = COALESCE(silo_profile_id, continuum_profile_id) - WHERE silo_profile_id IS NULL - AND continuum_profile_id IS NOT NULL; - END IF; -END $$; diff --git a/migrations/136_webhook_sync_rename_actor_to_profile.down.sql b/migrations/136_webhook_sync_rename_actor_to_profile.down.sql deleted file mode 100644 index 5717e11d..00000000 --- a/migrations/136_webhook_sync_rename_actor_to_profile.down.sql +++ /dev/null @@ -1,26 +0,0 @@ -UPDATE public.webhook_sync_event_logs -SET attrs = (attrs - 'external_user_id' - 'external_user_name') - || jsonb_strip_nulls(jsonb_build_object( - 'actor_id', attrs->'external_user_id', - 'actor_name', attrs->'external_user_name' - )) -WHERE attrs ? 'external_user_id' OR attrs ? 'external_user_name'; - -ALTER TABLE public.webhook_sync_item_state - RENAME COLUMN external_user_id TO external_actor_id; - -ALTER INDEX public.idx_webhook_sync_profile_mappings_profile - RENAME TO idx_webhook_sync_actor_mappings_profile; -ALTER INDEX public.idx_webhook_sync_profile_mappings_connection - RENAME TO idx_webhook_sync_actor_mappings_connection; - -ALTER TABLE public.webhook_sync_profile_mappings - RENAME CONSTRAINT webhook_sync_profile_mappings_connection_user_unique - TO webhook_sync_actor_mappings_connection_actor_unique; - -ALTER TABLE public.webhook_sync_profile_mappings - RENAME COLUMN external_user_name TO external_actor_name; -ALTER TABLE public.webhook_sync_profile_mappings - RENAME COLUMN external_user_id TO external_actor_id; - -ALTER TABLE public.webhook_sync_profile_mappings RENAME TO webhook_sync_actor_mappings; diff --git a/migrations/136_webhook_sync_rename_actor_to_profile.up.sql b/migrations/136_webhook_sync_rename_actor_to_profile.up.sql deleted file mode 100644 index 774b67ed..00000000 --- a/migrations/136_webhook_sync_rename_actor_to_profile.up.sql +++ /dev/null @@ -1,26 +0,0 @@ -ALTER TABLE public.webhook_sync_actor_mappings RENAME TO webhook_sync_profile_mappings; - -ALTER TABLE public.webhook_sync_profile_mappings - RENAME COLUMN external_actor_id TO external_user_id; -ALTER TABLE public.webhook_sync_profile_mappings - RENAME COLUMN external_actor_name TO external_user_name; - -ALTER TABLE public.webhook_sync_profile_mappings - RENAME CONSTRAINT webhook_sync_actor_mappings_connection_actor_unique - TO webhook_sync_profile_mappings_connection_user_unique; - -ALTER INDEX public.idx_webhook_sync_actor_mappings_connection - RENAME TO idx_webhook_sync_profile_mappings_connection; -ALTER INDEX public.idx_webhook_sync_actor_mappings_profile - RENAME TO idx_webhook_sync_profile_mappings_profile; - -ALTER TABLE public.webhook_sync_item_state - RENAME COLUMN external_actor_id TO external_user_id; - -UPDATE public.webhook_sync_event_logs -SET attrs = (attrs - 'actor_id' - 'actor_name') - || jsonb_strip_nulls(jsonb_build_object( - 'external_user_id', attrs->'actor_id', - 'external_user_name', attrs->'actor_name' - )) -WHERE attrs ? 'actor_id' OR attrs ? 'actor_name'; diff --git a/migrations/137_recap_preview_markers.down.sql b/migrations/137_recap_preview_markers.down.sql deleted file mode 100644 index 24ae4897..00000000 --- a/migrations/137_recap_preview_markers.down.sql +++ /dev/null @@ -1,21 +0,0 @@ -DELETE FROM server_settings WHERE key = 'introdb.api_key'; - -ALTER TABLE user_profiles - DROP COLUMN IF EXISTS auto_play_next_preview, - DROP COLUMN IF EXISTS auto_skip_recap; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS preview_markers_detected_at, - DROP COLUMN IF EXISTS preview_markers_algorithm, - DROP COLUMN IF EXISTS preview_markers_confidence, - DROP COLUMN IF EXISTS preview_markers_provider, - DROP COLUMN IF EXISTS preview_markers_source, - DROP COLUMN IF EXISTS preview_end, - DROP COLUMN IF EXISTS preview_start, - DROP COLUMN IF EXISTS recap_markers_detected_at, - DROP COLUMN IF EXISTS recap_markers_algorithm, - DROP COLUMN IF EXISTS recap_markers_confidence, - DROP COLUMN IF EXISTS recap_markers_provider, - DROP COLUMN IF EXISTS recap_markers_source, - DROP COLUMN IF EXISTS recap_end, - DROP COLUMN IF EXISTS recap_start; diff --git a/migrations/138_search_number_word_normalization.down.sql b/migrations/138_search_number_word_normalization.down.sql deleted file mode 100644 index 0a841bef..00000000 --- a/migrations/138_search_number_word_normalization.down.sql +++ /dev/null @@ -1,38 +0,0 @@ --- Restore the pre-138 normalization: collapse non-alnum to spaces, lowercase, --- and strip standalone "and" tokens, but do not normalize number words or --- ordinal digit suffixes. - -DROP INDEX IF EXISTS public.idx_media_items_search_title_fields; -DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; - -ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; - -CREATE OR REPLACE FUNCTION public.normalize_search_text(input text) -RETURNS text -LANGUAGE sql -IMMUTABLE -PARALLEL SAFE -AS $$ - SELECT BTRIM(REGEXP_REPLACE( - ' ' || BTRIM(LOWER(REGEXP_REPLACE(COALESCE(input, ''), '[^[:alnum:]]+', ' ', 'g'))) || ' ', - ' (and )+', - ' ', - 'g' - )); -$$; - -ALTER TABLE public.media_items - ADD COLUMN title_normalized text - GENERATED ALWAYS AS (public.normalize_search_text(title)) STORED; - -CREATE INDEX IF NOT EXISTS idx_media_items_title_normalized_trgm - ON public.media_items USING gin (title_normalized public.gin_trgm_ops); - -CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields - ON public.media_items USING gin (( - setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(title, ''))), 'A') || - setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(original_title, ''))), 'A') || - setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(sort_title, ''))), 'B') - )); - -DROP FUNCTION IF EXISTS public.normalize_search_number_token(text); diff --git a/migrations/139_media_requests.down.sql b/migrations/139_media_requests.down.sql deleted file mode 100644 index de261138..00000000 --- a/migrations/139_media_requests.down.sql +++ /dev/null @@ -1,10 +0,0 @@ -DROP TABLE IF EXISTS public.media_request_events; -DROP INDEX IF EXISTS public.idx_media_requests_outcome_created; -DROP INDEX IF EXISTS public.idx_media_requests_status_created; -DROP INDEX IF EXISTS public.idx_media_requests_profile_created; -DROP INDEX IF EXISTS public.idx_media_requests_user_created; -DROP INDEX IF EXISTS public.idx_media_requests_active_tmdb; -DROP TABLE IF EXISTS public.media_requests; -DROP TABLE IF EXISTS public.request_integrations; -DROP TABLE IF EXISTS public.request_user_limits; -DROP TABLE IF EXISTS public.request_settings; diff --git a/migrations/140_user_permissions.down.sql b/migrations/140_user_permissions.down.sql deleted file mode 100644 index 12011ed4..00000000 --- a/migrations/140_user_permissions.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.users - DROP COLUMN IF EXISTS permissions; diff --git a/migrations/140_user_permissions.up.sql b/migrations/140_user_permissions.up.sql deleted file mode 100644 index 0921ec97..00000000 --- a/migrations/140_user_permissions.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE public.users - ADD COLUMN IF NOT EXISTS permissions text[] NOT NULL DEFAULT '{}'::text[]; - -UPDATE public.users -SET permissions = '{}'::text[] -WHERE permissions IS NULL; diff --git a/migrations/141_episode_title_sort_index.down.sql b/migrations/141_episode_title_sort_index.down.sql deleted file mode 100644 index 959a7406..00000000 --- a/migrations/141_episode_title_sort_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.idx_episodes_sort_key_content; diff --git a/migrations/142_episode_catalog_entries.down.sql b/migrations/142_episode_catalog_entries.down.sql deleted file mode 100644 index e93f6816..00000000 --- a/migrations/142_episode_catalog_entries.down.sql +++ /dev/null @@ -1,42 +0,0 @@ -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_series ON public.media_items; -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_episodes ON public.episodes; -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files_update ON public.media_files; -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files_insert_delete ON public.media_files; -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files ON public.media_files; -DROP TRIGGER IF EXISTS trg_episode_catalog_entries_episode_libraries ON public.episode_libraries; - -DROP FUNCTION IF EXISTS public.episode_catalog_entries_series_trigger(); -DROP FUNCTION IF EXISTS public.episode_catalog_entries_episodes_trigger(); -DROP FUNCTION IF EXISTS public.episode_catalog_entries_media_files_trigger(); -DROP FUNCTION IF EXISTS public.episode_catalog_entries_episode_libraries_trigger(); -DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entries_for_series(text); -DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entries_for_episode(text); -DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entry(text, integer); -DROP FUNCTION IF EXISTS public.episode_catalog_resolution_rank(text); -DROP FUNCTION IF EXISTS public.episode_catalog_normalized_resolution(text); -DROP FUNCTION IF EXISTS public.episode_catalog_rating_rank(text); - -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_subtitle_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_audio_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_resolution_codes_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_countries_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_networks_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_studios_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_genres_gin; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_non_dolby_vision; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_dolby_vision; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_non_hdr; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_hdr; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_status; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_bitrate; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_resolution; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_tmdb; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_imdb; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_runtime; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_content_rating; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_year; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_air_date; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_added; -DROP INDEX IF EXISTS public.idx_episode_catalog_entries_title; - -DROP TABLE IF EXISTS public.episode_catalog_entries; diff --git a/migrations/162_media_items_air_timezone.down.sql b/migrations/162_media_items_air_timezone.down.sql deleted file mode 100644 index be2d1a6e..00000000 --- a/migrations/162_media_items_air_timezone.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_items DROP COLUMN IF EXISTS air_timezone; diff --git a/migrations/162_media_items_air_timezone.up.sql b/migrations/162_media_items_air_timezone.up.sql deleted file mode 100644 index b4530669..00000000 --- a/migrations/162_media_items_air_timezone.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE media_items ADD COLUMN IF NOT EXISTS air_timezone text; diff --git a/migrations/165_case_insensitive_username_email.down.sql b/migrations/165_case_insensitive_username_email.down.sql deleted file mode 100644 index b1911233..00000000 --- a/migrations/165_case_insensitive_username_email.down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Revert username/email to case-sensitive text. The citext extension is left --- installed: dropping it is unnecessary and would fail if any other object --- ever depends on it. -ALTER TABLE public.users - ALTER COLUMN username TYPE text USING username::text, - ALTER COLUMN email TYPE text USING email::text; diff --git a/migrations/167_trending_discover_snapshots.down.sql b/migrations/167_trending_discover_snapshots.down.sql deleted file mode 100644 index 72a6d6d4..00000000 --- a/migrations/167_trending_discover_snapshots.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.trending_discover_snapshots; diff --git a/migrations/168_subtitle_ai_jobs.down.sql b/migrations/168_subtitle_ai_jobs.down.sql deleted file mode 100644 index 868b62fd..00000000 --- a/migrations/168_subtitle_ai_jobs.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.subtitle_ai_jobs; diff --git a/migrations/169_request_multi_instance.down.sql b/migrations/169_request_multi_instance.down.sql deleted file mode 100644 index 21285744..00000000 --- a/migrations/169_request_multi_instance.down.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Restore per-fulfillment columns on media_requests. -ALTER TABLE public.media_requests - ADD COLUMN IF NOT EXISTS integration_kind text NOT NULL DEFAULT '', - ADD COLUMN IF NOT EXISTS external_id text NOT NULL DEFAULT '', - ADD COLUMN IF NOT EXISTS external_status text NOT NULL DEFAULT ''; - --- Copy back the 1080p target's fulfillment fields (lossy: 4K/anime targets dropped). -UPDATE public.media_requests mr -SET integration_kind = t.integration_kind, - external_id = t.external_id, - external_status = t.external_status -FROM public.media_request_targets t -WHERE t.request_id = mr.id AND t.quality = '1080p'; - -ALTER TABLE public.media_requests DROP COLUMN IF EXISTS is_anime; - -DROP TABLE IF EXISTS public.media_request_targets; - --- Collapse request_integrations back to kind-PK (lossy: keep one default per kind). -DELETE FROM public.request_integrations a -USING public.request_integrations b -WHERE a.kind = b.kind AND a.id <> b.id AND b.is_default AND NOT a.is_default; --- If a kind has no default, keep an arbitrary row and drop the rest. -DELETE FROM public.request_integrations a -USING public.request_integrations b -WHERE a.kind = b.kind AND a.ctid < b.ctid; - -DROP INDEX IF EXISTS idx_request_integrations_default_per_kind; -DROP INDEX IF EXISTS idx_request_integrations_default4k_per_kind; - -ALTER TABLE public.request_integrations DROP CONSTRAINT request_integrations_pkey; -ALTER TABLE public.request_integrations ADD PRIMARY KEY (kind); -ALTER TABLE public.request_integrations - DROP COLUMN IF EXISTS id, - DROP COLUMN IF EXISTS name, - DROP COLUMN IF EXISTS is_4k, - DROP COLUMN IF EXISTS is_default, - DROP COLUMN IF EXISTS is_default_4k, - DROP COLUMN IF EXISTS anime_enabled, - DROP COLUMN IF EXISTS anime_quality_profile_id, - DROP COLUMN IF EXISTS anime_root_folder, - DROP COLUMN IF EXISTS anime_tags; diff --git a/migrations/170_request_force_dual_quality.down.sql b/migrations/170_request_force_dual_quality.down.sql deleted file mode 100644 index cf81072d..00000000 --- a/migrations/170_request_force_dual_quality.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.request_settings DROP COLUMN IF EXISTS force_dual_quality; diff --git a/migrations/170_request_force_dual_quality.up.sql b/migrations/170_request_force_dual_quality.up.sql deleted file mode 100644 index 1afb8bcf..00000000 --- a/migrations/170_request_force_dual_quality.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.request_settings - ADD COLUMN IF NOT EXISTS force_dual_quality boolean NOT NULL DEFAULT false; diff --git a/migrations/171_autoscan.down.sql b/migrations/171_autoscan.down.sql deleted file mode 100644 index 7ebdcf54..00000000 --- a/migrations/171_autoscan.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TABLE IF EXISTS public.autoscan_sources; -DROP TABLE IF EXISTS public.autoscan_connections; -DROP TABLE IF EXISTS public.autoscan_settings; diff --git a/migrations/172_autoscan_source_config.down.sql b/migrations/172_autoscan_source_config.down.sql deleted file mode 100644 index 5849d154..00000000 --- a/migrations/172_autoscan_source_config.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE public.autoscan_sources - DROP COLUMN IF EXISTS source_config; diff --git a/migrations/173_autoscan_events.down.sql b/migrations/173_autoscan_events.down.sql deleted file mode 100644 index dfe71278..00000000 --- a/migrations/173_autoscan_events.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP INDEX IF EXISTS public.idx_scan_runs_autoscan_event; - -ALTER TABLE public.scan_runs - DROP COLUMN IF EXISTS autoscan_event_id; - -DROP INDEX IF EXISTS public.idx_autoscan_events_completed; -DROP INDEX IF EXISTS public.idx_autoscan_events_source_completed; -DROP TABLE IF EXISTS public.autoscan_events; diff --git a/migrations/174_autoscan_source_label.down.sql b/migrations/174_autoscan_source_label.down.sql deleted file mode 100644 index c0f53878..00000000 --- a/migrations/174_autoscan_source_label.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE public.autoscan_sources DROP COLUMN label; diff --git a/migrations/175_movie_release_date_desc_index.down.sql b/migrations/175_movie_release_date_desc_index.down.sql deleted file mode 100644 index 0dac3912..00000000 --- a/migrations/175_movie_release_date_desc_index.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS public.idx_media_items_movie_release_date_desc_content; diff --git a/migrations/175_movie_release_date_desc_index.up.sql b/migrations/175_movie_release_date_desc_index.up.sql deleted file mode 100644 index b734e0f8..00000000 --- a/migrations/175_movie_release_date_desc_index.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX IF NOT EXISTS idx_media_items_movie_release_date_desc_content -ON public.media_items USING btree (release_date DESC NULLS LAST, content_id ASC) -WHERE type = 'movie'; diff --git a/migrations/176_playback_session_position.down.sql b/migrations/176_playback_session_position.down.sql deleted file mode 100644 index d1dec280..00000000 --- a/migrations/176_playback_session_position.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE playback_sessions_sync - DROP COLUMN IF EXISTS position_seconds; diff --git a/migrations/176_playback_session_position.up.sql b/migrations/176_playback_session_position.up.sql deleted file mode 100644 index 5bb3cf35..00000000 --- a/migrations/176_playback_session_position.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE playback_sessions_sync - ADD COLUMN IF NOT EXISTS position_seconds DOUBLE PRECISION NOT NULL DEFAULT 0; diff --git a/migrations/177_playback_session_transcode_hw_accel.down.sql b/migrations/177_playback_session_transcode_hw_accel.down.sql deleted file mode 100644 index 53738e05..00000000 --- a/migrations/177_playback_session_transcode_hw_accel.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE playback_sessions_sync - DROP COLUMN IF EXISTS transcode_hw_accel; diff --git a/migrations/177_playback_session_transcode_hw_accel.up.sql b/migrations/177_playback_session_transcode_hw_accel.up.sql deleted file mode 100644 index 4c2e2d5f..00000000 --- a/migrations/177_playback_session_transcode_hw_accel.up.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE playback_sessions_sync - ADD COLUMN IF NOT EXISTS transcode_hw_accel text; diff --git a/migrations/178_admin_job_cancelled_status.up.sql b/migrations/178_admin_job_cancelled_status.up.sql deleted file mode 100644 index ae0fcfa2..00000000 --- a/migrations/178_admin_job_cancelled_status.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE public.admin_jobs - DROP CONSTRAINT IF EXISTS admin_jobs_status_check; - -ALTER TABLE public.admin_jobs - ADD CONSTRAINT admin_jobs_status_check - CHECK ((status = ANY (ARRAY[ - 'queued'::text, - 'running'::text, - 'completed'::text, - 'failed'::text, - 'cancelled'::text - ]))); diff --git a/migrations/179_media_file_match_suppression.down.sql b/migrations/179_media_file_match_suppression.down.sql deleted file mode 100644 index 2a123119..00000000 --- a/migrations/179_media_file_match_suppression.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS idx_media_files_unmatched_match_ready; - -ALTER TABLE media_files - DROP COLUMN IF EXISTS match_suppressed_at; diff --git a/migrations/180_user_devices.down.sql b/migrations/180_user_devices.down.sql deleted file mode 100644 index 2cd42e37..00000000 --- a/migrations/180_user_devices.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.user_devices; diff --git a/migrations/181_marker_provider_config.down.sql b/migrations/181_marker_provider_config.down.sql deleted file mode 100644 index 96d62846..00000000 --- a/migrations/181_marker_provider_config.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.marker_provider_config; diff --git a/migrations/182_marker_contributions.down.sql b/migrations/182_marker_contributions.down.sql deleted file mode 100644 index 6bd18d2a..00000000 --- a/migrations/182_marker_contributions.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS public.marker_contributions; diff --git a/migrations/183_marker_edit_audit.down.sql b/migrations/183_marker_edit_audit.down.sql deleted file mode 100644 index 422c2689..00000000 --- a/migrations/183_marker_edit_audit.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -DROP TABLE IF EXISTS public.marker_edit_audit; - -UPDATE public.users -SET permissions = array_remove(permissions, 'marker_edit') -WHERE role <> 'admin'; - -ALTER TABLE public.users - ALTER COLUMN permissions SET DEFAULT '{}'::text[]; diff --git a/migrations/embed.go b/migrations/embed.go index 49a11a9b..ed92d334 100644 --- a/migrations/embed.go +++ b/migrations/embed.go @@ -3,5 +3,5 @@ package migrations import "embed" -//go:embed *.sql +//go:embed sql/*.sql var FS embed.FS diff --git a/migrations/001_schema.up.sql b/migrations/sql/001_schema.sql similarity index 99% rename from migrations/001_schema.up.sql rename to migrations/sql/001_schema.sql index 9a5336d7..cc48aa24 100644 --- a/migrations/001_schema.up.sql +++ b/migrations/sql/001_schema.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- -- PostgreSQL database dump -- @@ -2468,3 +2470,4 @@ ON CONFLICT (key) DO NOTHING; -- Restore search_path after pg_dump's set_config('search_path', '', false) on line 15 SELECT pg_catalog.set_config('search_path', 'public', false); +-- +goose StatementEnd diff --git a/migrations/002_task_framework.up.sql b/migrations/sql/002_task_framework.sql similarity index 95% rename from migrations/002_task_framework.up.sql rename to migrations/sql/002_task_framework.sql index 3af5fed5..de075d11 100644 --- a/migrations/002_task_framework.up.sql +++ b/migrations/sql/002_task_framework.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Execution history for completed task runs. CREATE TABLE task_executions ( id BIGSERIAL PRIMARY KEY, @@ -25,3 +27,4 @@ CREATE TABLE task_triggers ( ); CREATE INDEX idx_task_triggers_key ON task_triggers (task_key); +-- +goose StatementEnd diff --git a/migrations/sql/003_replace_kometa_with_tmdb.sql b/migrations/sql/003_replace_kometa_with_tmdb.sql new file mode 100644 index 00000000..2ba18e55 --- /dev/null +++ b/migrations/sql/003_replace_kometa_with_tmdb.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +-- Replace any existing kometa collections with manual type. +UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'kometa'; + +-- Drop old CHECK constraint and add updated one (kometa → tmdb). +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; +ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check + CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'tmdb'])); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Convert any tmdb collections to manual before reverting the constraint. +UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'tmdb'; + +-- Revert CHECK constraint to pre-migration state (manual, mdblist, kometa). +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; +ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check + CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'kometa'])); +-- +goose StatementEnd diff --git a/migrations/sql/004_collection_image_thumbhash.sql b/migrations/sql/004_collection_image_thumbhash.sql new file mode 100644 index 00000000..acb9ecb6 --- /dev/null +++ b/migrations/sql/004_collection_image_thumbhash.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections ADD COLUMN poster_thumbhash TEXT NOT NULL DEFAULT ''; +ALTER TABLE library_collections ADD COLUMN backdrop_thumbhash TEXT NOT NULL DEFAULT ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_thumbhash; +ALTER TABLE library_collections DROP COLUMN IF EXISTS backdrop_thumbhash; +-- +goose StatementEnd diff --git a/migrations/005_history_import.up.sql b/migrations/sql/005_history_import.sql similarity index 98% rename from migrations/005_history_import.up.sql rename to migrations/sql/005_history_import.sql index ae0469d5..3864c19f 100644 --- a/migrations/005_history_import.up.sql +++ b/migrations/sql/005_history_import.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.history_import_sources ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL, @@ -64,3 +66,4 @@ CREATE INDEX idx_history_import_connect_sessions_user_expires CREATE INDEX idx_history_import_connect_sessions_expiry ON public.history_import_connect_sessions (expires_at); +-- +goose StatementEnd diff --git a/migrations/006_episode_external_ids.up.sql b/migrations/sql/006_episode_external_ids.sql similarity index 91% rename from migrations/006_episode_external_ids.up.sql rename to migrations/sql/006_episode_external_ids.sql index e2372e34..3e0f97ac 100644 --- a/migrations/006_episode_external_ids.up.sql +++ b/migrations/sql/006_episode_external_ids.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.episodes ADD COLUMN IF NOT EXISTS imdb_id text DEFAULT ''::text NOT NULL, ADD COLUMN IF NOT EXISTS tmdb_id text DEFAULT ''::text NOT NULL, @@ -14,3 +16,4 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_tmdb_id CREATE UNIQUE INDEX IF NOT EXISTS idx_episodes_tvdb_id ON public.episodes USING btree (tvdb_id) WHERE (tvdb_id <> ''::text); +-- +goose StatementEnd diff --git a/migrations/007_people_name_lower_index.up.sql b/migrations/sql/007_people_name_lower_index.sql similarity index 60% rename from migrations/007_people_name_lower_index.up.sql rename to migrations/sql/007_people_name_lower_index.sql index 144ff974..73eab945 100644 --- a/migrations/007_people_name_lower_index.up.sql +++ b/migrations/sql/007_people_name_lower_index.sql @@ -1 +1,4 @@ +-- +goose Up +-- +goose StatementBegin CREATE INDEX IF NOT EXISTS idx_people_name_lower ON public.people USING btree (LOWER(name)); +-- +goose StatementEnd diff --git a/migrations/008_activity_session_details.up.sql b/migrations/sql/008_activity_session_details.sql similarity index 90% rename from migrations/008_activity_session_details.up.sql rename to migrations/sql/008_activity_session_details.sql index 4c00c49e..1aa5a717 100644 --- a/migrations/008_activity_session_details.up.sql +++ b/migrations/sql/008_activity_session_details.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.playback_sessions_sync ADD COLUMN IF NOT EXISTS client_ip inet, ADD COLUMN IF NOT EXISTS audio_track_index integer NOT NULL DEFAULT 0, @@ -8,3 +10,4 @@ ALTER TABLE public.playback_sessions_sync ADD COLUMN IF NOT EXISTS target_video_codec text, ADD COLUMN IF NOT EXISTS target_audio_codec text, ADD COLUMN IF NOT EXISTS target_bitrate_kbps integer; +-- +goose StatementEnd diff --git a/migrations/009_repair_schema_drift.up.sql b/migrations/sql/009_repair_schema_drift.sql similarity index 94% rename from migrations/009_repair_schema_drift.up.sql rename to migrations/sql/009_repair_schema_drift.sql index 2ceeb284..f131e272 100644 --- a/migrations/009_repair_schema_drift.up.sql +++ b/migrations/sql/009_repair_schema_drift.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Repair historical schema drift where some databases recorded migrations as -- applied without fully creating their schema changes. @@ -31,3 +33,4 @@ CREATE INDEX IF NOT EXISTS idx_task_triggers_key ALTER TABLE public.playback_sessions_sync ADD COLUMN IF NOT EXISTS transcode_node_url text; +-- +goose StatementEnd diff --git a/migrations/010_session_tracking.up.sql b/migrations/sql/010_session_tracking.sql similarity index 63% rename from migrations/010_session_tracking.up.sql rename to migrations/sql/010_session_tracking.sql index 620c4988..f5bfe396 100644 --- a/migrations/010_session_tracking.up.sql +++ b/migrations/sql/010_session_tracking.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Node heartbeat table for detecting crashed nodes. CREATE TABLE IF NOT EXISTS node_heartbeats ( node_id TEXT PRIMARY KEY, @@ -12,3 +14,11 @@ ALTER TABLE playback_sessions_sync ALTER TABLE playback_sessions_sync ADD COLUMN IF NOT EXISTS has_websocket BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync DROP COLUMN IF EXISTS has_websocket; +ALTER TABLE playback_sessions_sync DROP COLUMN IF EXISTS is_paused; +DROP TABLE IF EXISTS node_heartbeats; +-- +goose StatementEnd diff --git a/migrations/011_logging_foundation.up.sql b/migrations/sql/011_logging_foundation.sql similarity index 96% rename from migrations/011_logging_foundation.up.sql rename to migrations/sql/011_logging_foundation.sql index ff0e1a80..d2e05f86 100644 --- a/migrations/011_logging_foundation.up.sql +++ b/migrations/sql/011_logging_foundation.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS request_id text, ADD COLUMN IF NOT EXISTS node_id text, @@ -28,3 +30,4 @@ CREATE INDEX IF NOT EXISTS idx_operational_logs_session_id ON public.operational ALTER TABLE ONLY public.operational_logs ADD CONSTRAINT operational_logs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; +-- +goose StatementEnd diff --git a/migrations/012_playback_session_log_grouping.up.sql b/migrations/sql/012_playback_session_log_grouping.sql similarity index 88% rename from migrations/012_playback_session_log_grouping.up.sql rename to migrations/sql/012_playback_session_log_grouping.sql index ab53265f..246aadab 100644 --- a/migrations/012_playback_session_log_grouping.up.sql +++ b/migrations/sql/012_playback_session_log_grouping.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS playback_session_id text; @@ -9,3 +11,4 @@ ALTER TABLE public.operational_logs CREATE INDEX IF NOT EXISTS idx_operational_logs_playback_session_id ON public.operational_logs USING btree (playback_session_id, "timestamp" DESC); +-- +goose StatementEnd diff --git a/migrations/sql/013_item_refresh_jobs.sql b/migrations/sql/013_item_refresh_jobs.sql new file mode 100644 index 00000000..c32b611b --- /dev/null +++ b/migrations/sql/013_item_refresh_jobs.sql @@ -0,0 +1,20 @@ +-- +goose Up +-- +goose StatementBegin +DROP INDEX IF EXISTS public.admin_jobs_active_job_type_idx; + +CREATE UNIQUE INDEX admin_jobs_active_job_type_idx +ON public.admin_jobs USING btree (job_type) +WHERE ( + status = ANY (ARRAY['queued'::text, 'running'::text]) + AND job_type = ANY (ARRAY['catalog_export'::text, 'catalog_import'::text]) +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.admin_jobs_active_job_type_idx; + +CREATE UNIQUE INDEX admin_jobs_active_job_type_idx +ON public.admin_jobs USING btree (job_type) +WHERE (status = ANY (ARRAY['queued'::text, 'running'::text])); +-- +goose StatementEnd diff --git a/migrations/014_recommendations_embedding_lock_in.up.sql b/migrations/sql/014_recommendations_embedding_lock_in.sql similarity index 95% rename from migrations/014_recommendations_embedding_lock_in.up.sql rename to migrations/sql/014_recommendations_embedding_lock_in.sql index 3d1fc2d1..caaeac7f 100644 --- a/migrations/014_recommendations_embedding_lock_in.up.sql +++ b/migrations/sql/014_recommendations_embedding_lock_in.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin DROP INDEX IF EXISTS public.idx_media_item_embeddings_hnsw; DROP INDEX IF EXISTS public.idx_user_taste_profiles_hnsw; DROP INDEX IF EXISTS public.idx_taste_clusters_embedding; @@ -37,3 +39,4 @@ WHERE key IN ( 'recommendations.openai_api_key', 'recommendations.openai_model' ); +-- +goose StatementEnd diff --git a/migrations/015_admin_smart_collections.up.sql b/migrations/sql/015_admin_smart_collections.sql similarity index 59% rename from migrations/015_admin_smart_collections.up.sql rename to migrations/sql/015_admin_smart_collections.sql index fe02294d..e40671e5 100644 --- a/migrations/015_admin_smart_collections.up.sql +++ b/migrations/sql/015_admin_smart_collections.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE library_collections ADD COLUMN collection_mode TEXT NOT NULL DEFAULT 'manual', ADD COLUMN query_definition JSONB NOT NULL DEFAULT '{}'::jsonb, @@ -20,3 +22,24 @@ ON CONFLICT (collection_id, library_id) DO NOTHING; CREATE INDEX idx_library_collection_libraries_library ON library_collection_libraries (library_id, collection_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE library_collections lc +SET library_id = lcl.library_id +FROM ( + SELECT DISTINCT ON (collection_id) collection_id, library_id + FROM library_collection_libraries + ORDER BY collection_id, library_id +) lcl +WHERE lc.id = lcl.collection_id; + +DROP INDEX IF EXISTS idx_library_collection_libraries_library; +DROP TABLE IF EXISTS library_collection_libraries; + +ALTER TABLE library_collections + DROP COLUMN IF EXISTS sort_config, + DROP COLUMN IF EXISTS query_definition, + DROP COLUMN IF EXISTS collection_mode; +-- +goose StatementEnd diff --git a/migrations/016_user_smart_collections.up.sql b/migrations/sql/016_user_smart_collections.sql similarity index 57% rename from migrations/016_user_smart_collections.up.sql rename to migrations/sql/016_user_smart_collections.sql index 7efe2620..3c964857 100644 --- a/migrations/016_user_smart_collections.up.sql +++ b/migrations/sql/016_user_smart_collections.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE user_personal_collections ADD COLUMN creator_profile_id TEXT NOT NULL DEFAULT '', ADD COLUMN collection_mode TEXT NOT NULL DEFAULT 'manual', @@ -25,3 +27,26 @@ ON CONFLICT (user_id, collection_id, profile_id) DO NOTHING; CREATE INDEX idx_user_collection_profiles_lookup ON user_personal_collection_profiles (user_id, profile_id, collection_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE user_personal_collections upc +SET profile_id = ucpp.profile_id +FROM ( + SELECT DISTINCT ON (user_id, collection_id) user_id, collection_id, profile_id + FROM user_personal_collection_profiles + ORDER BY user_id, collection_id, profile_id +) ucpp +WHERE upc.user_id = ucpp.user_id AND upc.id = ucpp.collection_id; + +DROP INDEX IF EXISTS idx_user_collection_profiles_lookup; +DROP TABLE IF EXISTS user_personal_collection_profiles; + +ALTER TABLE user_personal_collections + DROP COLUMN IF EXISTS sort_config, + DROP COLUMN IF EXISTS query_definition, + DROP COLUMN IF EXISTS is_shared, + DROP COLUMN IF EXISTS collection_mode, + DROP COLUMN IF EXISTS creator_profile_id; +-- +goose StatementEnd diff --git a/migrations/017_admin_user_impersonation.up.sql b/migrations/sql/017_admin_user_impersonation.sql similarity index 90% rename from migrations/017_admin_user_impersonation.up.sql rename to migrations/sql/017_admin_user_impersonation.sql index 6be40625..dbcea697 100644 --- a/migrations/017_admin_user_impersonation.up.sql +++ b/migrations/sql/017_admin_user_impersonation.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.auth_sessions ADD COLUMN impersonator_user_id integer, ADD COLUMN impersonation_started_at timestamptz; @@ -12,3 +14,4 @@ CREATE INDEX IF NOT EXISTS idx_auth_sessions_impersonator_user_id CREATE INDEX IF NOT EXISTS idx_activity_log_impersonator_user_id ON public.activity_log USING btree (impersonator_user_id, "timestamp" DESC) WHERE impersonator_user_id IS NOT NULL; +-- +goose StatementEnd diff --git a/migrations/018_admin_job_public_catalog_url.up.sql b/migrations/sql/018_admin_job_public_catalog_url.sql similarity index 67% rename from migrations/018_admin_job_public_catalog_url.up.sql rename to migrations/sql/018_admin_job_public_catalog_url.sql index 8d9f0b20..51ac5b85 100644 --- a/migrations/018_admin_job_public_catalog_url.up.sql +++ b/migrations/sql/018_admin_job_public_catalog_url.sql @@ -1,3 +1,6 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.admin_jobs ADD COLUMN public_url text DEFAULT ''::text NOT NULL, ADD COLUMN published_at timestamptz; +-- +goose StatementEnd diff --git a/migrations/019_catalog_release_date.up.sql b/migrations/sql/019_catalog_release_date.sql similarity index 52% rename from migrations/019_catalog_release_date.up.sql rename to migrations/sql/019_catalog_release_date.sql index 4c17d41b..5d34b7b7 100644 --- a/migrations/019_catalog_release_date.up.sql +++ b/migrations/sql/019_catalog_release_date.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.media_items ADD COLUMN IF NOT EXISTS release_date date; @@ -5,3 +7,10 @@ UPDATE public.media_items SET release_date = NULLIF(first_air_date, '')::date WHERE release_date IS NULL AND first_air_date IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.media_items + DROP COLUMN IF EXISTS release_date; +-- +goose StatementEnd diff --git a/migrations/020_watch_history_source.up.sql b/migrations/sql/020_watch_history_source.sql similarity index 83% rename from migrations/020_watch_history_source.up.sql rename to migrations/sql/020_watch_history_source.sql index 3cb09e56..bd89ce14 100644 --- a/migrations/020_watch_history_source.up.sql +++ b/migrations/sql/020_watch_history_source.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.user_watch_history ADD COLUMN IF NOT EXISTS source text; @@ -10,3 +12,4 @@ ALTER TABLE public.user_watch_history ALTER TABLE public.user_watch_history ALTER COLUMN source SET NOT NULL; +-- +goose StatementEnd diff --git a/migrations/021_user_library_playback_preferences.up.sql b/migrations/sql/021_user_library_playback_preferences.sql similarity index 88% rename from migrations/021_user_library_playback_preferences.up.sql rename to migrations/sql/021_user_library_playback_preferences.sql index 95d48c10..1b6a9493 100644 --- a/migrations/021_user_library_playback_preferences.up.sql +++ b/migrations/sql/021_user_library_playback_preferences.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS user_library_playback_preferences ( user_id INTEGER NOT NULL, profile_id TEXT NOT NULL, @@ -10,3 +12,4 @@ CREATE TABLE IF NOT EXISTS user_library_playback_preferences ( CONSTRAINT user_library_playback_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); +-- +goose StatementEnd diff --git a/migrations/022_skipped_media_roots.up.sql b/migrations/sql/022_skipped_media_roots.sql similarity index 72% rename from migrations/022_skipped_media_roots.up.sql rename to migrations/sql/022_skipped_media_roots.sql index ee7d5677..0ab7db4a 100644 --- a/migrations/022_skipped_media_roots.up.sql +++ b/migrations/sql/022_skipped_media_roots.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS skipped_media_roots ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, root_path TEXT NOT NULL, @@ -8,3 +10,9 @@ CREATE TABLE IF NOT EXISTS skipped_media_roots ( last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (media_folder_id, root_path) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS skipped_media_roots; +-- +goose StatementEnd diff --git a/migrations/sql/023_opslog_component_level_index.sql b/migrations/sql/023_opslog_component_level_index.sql new file mode 100644 index 00000000..f04a7116 --- /dev/null +++ b/migrations/sql/023_opslog_component_level_index.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_operational_logs_component_level + ON public.operational_logs (component, level, "timestamp" DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_operational_logs_component_level; +-- +goose StatementEnd diff --git a/migrations/024_original_language.up.sql b/migrations/sql/024_original_language.sql similarity index 58% rename from migrations/024_original_language.up.sql rename to migrations/sql/024_original_language.sql index c6ed55d2..c4b8e517 100644 --- a/migrations/024_original_language.up.sql +++ b/migrations/sql/024_original_language.sql @@ -1,2 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.media_items ADD COLUMN original_language TEXT NOT NULL DEFAULT ''; +-- +goose StatementEnd diff --git a/migrations/sql/025_merge_collection_mode_type.sql b/migrations/sql/025_merge_collection_mode_type.sql new file mode 100644 index 00000000..665b25a6 --- /dev/null +++ b/migrations/sql/025_merge_collection_mode_type.sql @@ -0,0 +1,40 @@ +-- +goose Up +-- +goose StatementBegin +-- Merge collection_mode into collection_type for library collections. + +-- Drop the old constraint first so the backfill can write 'smart'. +ALTER TABLE library_collections DROP CONSTRAINT library_collections_collection_type_check; + +-- Backfill: smart mode → smart type. +UPDATE library_collections SET collection_type = 'smart' WHERE collection_mode = 'smart'; + +-- Drop the mode column. +ALTER TABLE library_collections DROP COLUMN collection_mode; + +-- Re-add constraint with 'smart' included. +ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check + CHECK (collection_type = ANY (ARRAY['manual', 'smart', 'mdblist', 'tmdb'])); + +-- Rename collection_mode → collection_type for user personal collections. +ALTER TABLE user_personal_collections RENAME COLUMN collection_mode TO collection_type; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Reverse: rename collection_type back to collection_mode for user personal collections. +ALTER TABLE user_personal_collections RENAME COLUMN collection_type TO collection_mode; + +-- Re-add collection_mode column to library collections. +ALTER TABLE library_collections ADD COLUMN collection_mode TEXT NOT NULL DEFAULT 'manual'; + +-- Backfill: smart type → smart mode. +UPDATE library_collections SET collection_mode = 'smart' WHERE collection_type = 'smart'; + +-- Revert smart type back to manual. +UPDATE library_collections SET collection_type = 'manual' WHERE collection_type = 'smart'; + +-- Restore original check constraint without 'smart'. +ALTER TABLE library_collections DROP CONSTRAINT library_collections_collection_type_check; +ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check + CHECK (collection_type = ANY (ARRAY['manual', 'mdblist', 'tmdb'])); +-- +goose StatementEnd diff --git a/migrations/026_match_attempted_queue.up.sql b/migrations/sql/026_match_attempted_queue.sql similarity index 54% rename from migrations/026_match_attempted_queue.up.sql rename to migrations/sql/026_match_attempted_queue.sql index ef120bbf..a1dcd989 100644 --- a/migrations/026_match_attempted_queue.up.sql +++ b/migrations/sql/026_match_attempted_queue.sql @@ -1,6 +1,17 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.media_files ADD COLUMN match_attempted_at timestamp with time zone; CREATE INDEX idx_media_files_unmatched_match_queue ON public.media_files USING btree (match_attempted_at ASC NULLS FIRST, id ASC) WHERE (((content_id IS NULL) OR (content_id = ''::text)) AND (missing_since IS NULL)); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_files_unmatched_match_queue; + +ALTER TABLE public.media_files + DROP COLUMN IF EXISTS match_attempted_at; +-- +goose StatementEnd diff --git a/migrations/027_user_home_item_dismissals.up.sql b/migrations/sql/027_user_home_item_dismissals.sql similarity index 80% rename from migrations/027_user_home_item_dismissals.up.sql rename to migrations/sql/027_user_home_item_dismissals.sql index 1ae06ee6..125bc4bd 100644 --- a/migrations/027_user_home_item_dismissals.up.sql +++ b/migrations/sql/027_user_home_item_dismissals.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS user_home_item_dismissals ( user_id INTEGER NOT NULL, profile_id TEXT NOT NULL, @@ -15,3 +17,9 @@ CREATE TABLE IF NOT EXISTS user_home_item_dismissals ( CREATE INDEX IF NOT EXISTS idx_user_home_item_dismissals_lookup ON user_home_item_dismissals(user_id, profile_id, surface); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS user_home_item_dismissals; +-- +goose StatementEnd diff --git a/migrations/028_log_partitioning.up.sql b/migrations/sql/028_log_partitioning.sql similarity index 57% rename from migrations/028_log_partitioning.up.sql rename to migrations/sql/028_log_partitioning.sql index ae7dff74..483ab768 100644 --- a/migrations/028_log_partitioning.up.sql +++ b/migrations/sql/028_log_partitioning.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.operational_logs_partitioned ( id bigint GENERATED ALWAYS AS IDENTITY, "timestamp" timestamp with time zone DEFAULT now() NOT NULL, @@ -236,3 +238,182 @@ CREATE INDEX idx_activity_log_impersonator_user_id ALTER TABLE public.activity_log ADD CONSTRAINT activity_log_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +CREATE TABLE public.operational_logs_unpartitioned ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + "timestamp" timestamp with time zone DEFAULT now() NOT NULL, + level text NOT NULL, + component text NOT NULL, + message text NOT NULL, + request_id text, + user_id integer, + session_id text, + client_ip inet, + node_id text, + attrs jsonb DEFAULT '{}'::jsonb NOT NULL, + playback_session_id text +); + +INSERT INTO public.operational_logs_unpartitioned ( + id, + "timestamp", + level, + component, + message, + request_id, + user_id, + session_id, + client_ip, + node_id, + attrs, + playback_session_id +) +OVERRIDING SYSTEM VALUE +SELECT + id, + "timestamp", + level, + component, + message, + request_id, + user_id, + session_id, + client_ip, + node_id, + attrs, + playback_session_id +FROM public.operational_logs; + +DO $$ +DECLARE + seq_name text; + max_id bigint; +BEGIN + SELECT pg_get_serial_sequence('public.operational_logs_unpartitioned', 'id') INTO seq_name; + SELECT MAX(id) INTO max_id FROM public.operational_logs_unpartitioned; + IF max_id IS NULL THEN + EXECUTE format('SELECT setval(%L, 1, false)', seq_name); + ELSE + EXECUTE format('SELECT setval(%L, %s, true)', seq_name, max_id); + END IF; +END +$$; + +ALTER TABLE public.operational_logs RENAME TO operational_logs_partitioned_old; +ALTER TABLE public.operational_logs_unpartitioned RENAME TO operational_logs; +DROP TABLE public.operational_logs_partitioned_old; + +CREATE INDEX idx_operational_logs_timestamp + ON public.operational_logs USING btree ("timestamp" DESC); +CREATE INDEX idx_operational_logs_level + ON public.operational_logs USING btree (level, "timestamp" DESC); +CREATE INDEX idx_operational_logs_component + ON public.operational_logs USING btree (component, "timestamp" DESC); +CREATE INDEX idx_operational_logs_request_id + ON public.operational_logs USING btree (request_id, "timestamp" DESC); +CREATE INDEX idx_operational_logs_user_id + ON public.operational_logs USING btree (user_id, "timestamp" DESC); +CREATE INDEX idx_operational_logs_session_id + ON public.operational_logs USING btree (session_id, "timestamp" DESC); +CREATE INDEX idx_operational_logs_playback_session_id + ON public.operational_logs USING btree (playback_session_id, "timestamp" DESC); +CREATE INDEX idx_operational_logs_component_level + ON public.operational_logs USING btree (component, level, "timestamp" DESC); + +ALTER TABLE public.operational_logs + ADD CONSTRAINT operational_logs_user_id_fkey + FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + +ALTER TABLE public.activity_log + ALTER COLUMN id DROP DEFAULT; +ALTER SEQUENCE public.activity_log_id_seq + OWNED BY NONE; + +CREATE TABLE public.activity_log_unpartitioned ( + id bigint NOT NULL DEFAULT nextval('public.activity_log_id_seq'::regclass), + "timestamp" timestamp with time zone DEFAULT now() NOT NULL, + client_ip inet NOT NULL, + user_id integer, + session_id text, + method text NOT NULL, + path text NOT NULL, + status_code integer, + user_agent text, + duration_ms integer, + request_id text, + node_id text, + path_pattern text, + playback_session_id text, + impersonator_user_id integer, + PRIMARY KEY (id) +); + +INSERT INTO public.activity_log_unpartitioned ( + id, + "timestamp", + client_ip, + user_id, + session_id, + method, + path, + status_code, + user_agent, + duration_ms, + request_id, + node_id, + path_pattern, + playback_session_id, + impersonator_user_id +) +SELECT + id, + "timestamp", + client_ip, + user_id, + session_id, + method, + path, + status_code, + user_agent, + duration_ms, + request_id, + node_id, + path_pattern, + playback_session_id, + impersonator_user_id +FROM public.activity_log; + +SELECT setval( + 'public.activity_log_id_seq', + COALESCE((SELECT MAX(id) FROM public.activity_log_unpartitioned), 1), + EXISTS (SELECT 1 FROM public.activity_log_unpartitioned) +); + +ALTER TABLE public.activity_log RENAME TO activity_log_partitioned_old; +ALTER TABLE public.activity_log_unpartitioned RENAME TO activity_log; +DROP TABLE public.activity_log_partitioned_old; + +ALTER SEQUENCE public.activity_log_id_seq + OWNED BY public.activity_log.id; + +CREATE INDEX idx_activity_log_client_ip + ON public.activity_log USING btree (client_ip, "timestamp" DESC); +CREATE INDEX idx_activity_log_timestamp + ON public.activity_log USING btree ("timestamp" DESC); +CREATE INDEX idx_activity_log_user_id + ON public.activity_log USING btree (user_id, "timestamp" DESC); +CREATE INDEX idx_activity_log_playback_session_id + ON public.activity_log USING btree (playback_session_id, "timestamp" DESC); +CREATE INDEX idx_activity_log_request_id + ON public.activity_log USING btree (request_id, "timestamp" DESC); +CREATE INDEX idx_activity_log_impersonator_user_id + ON public.activity_log USING btree (impersonator_user_id, "timestamp" DESC) + WHERE impersonator_user_id IS NOT NULL; + +ALTER TABLE public.activity_log + ADD CONSTRAINT activity_log_user_id_fkey + FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; +-- +goose StatementEnd diff --git a/migrations/sql/029_show_forced_subtitles.sql b/migrations/sql/029_show_forced_subtitles.sql new file mode 100644 index 00000000..23cf1c79 --- /dev/null +++ b/migrations/sql/029_show_forced_subtitles.sql @@ -0,0 +1,23 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE user_profiles + ADD COLUMN show_forced_subtitles BOOLEAN NOT NULL DEFAULT true; + +ALTER TABLE user_library_playback_preferences + ADD COLUMN show_forced_subtitles BOOLEAN; + +ALTER TABLE user_subtitle_preferences + ADD COLUMN show_forced_subtitles BOOLEAN; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE user_subtitle_preferences + DROP COLUMN show_forced_subtitles; + +ALTER TABLE user_library_playback_preferences + DROP COLUMN show_forced_subtitles; + +ALTER TABLE user_profiles + DROP COLUMN show_forced_subtitles; +-- +goose StatementEnd diff --git a/migrations/sql/030_file_first_fallback_library_visibility.sql b/migrations/sql/030_file_first_fallback_library_visibility.sql new file mode 100644 index 00000000..f9e4ad7e --- /dev/null +++ b/migrations/sql/030_file_first_fallback_library_visibility.sql @@ -0,0 +1,25 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE media_items + ADD COLUMN episode_metadata_incomplete BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN episode_metadata_last_checked_at TIMESTAMPTZ; + +ALTER TABLE seasons + ADD COLUMN metadata_source TEXT NOT NULL DEFAULT 'provider'; + +ALTER TABLE episodes + ADD COLUMN metadata_source TEXT NOT NULL DEFAULT 'provider'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE episodes + DROP COLUMN metadata_source; + +ALTER TABLE seasons + DROP COLUMN metadata_source; + +ALTER TABLE media_items + DROP COLUMN episode_metadata_last_checked_at, + DROP COLUMN episode_metadata_incomplete; +-- +goose StatementEnd diff --git a/migrations/031_plugin_runtime.up.sql b/migrations/sql/031_plugin_runtime.sql similarity index 80% rename from migrations/031_plugin_runtime.up.sql rename to migrations/sql/031_plugin_runtime.sql index 8541118c..2ba12ad6 100644 --- a/migrations/031_plugin_runtime.up.sql +++ b/migrations/sql/031_plugin_runtime.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE plugin_repositories ( id BIGSERIAL PRIMARY KEY, url TEXT NOT NULL UNIQUE, @@ -108,3 +110,36 @@ CREATE UNIQUE INDEX idx_media_analyzer_bindings_folder_installation ALTER TABLE media_files ADD COLUMN markers_source TEXT, ADD COLUMN markers_confidence DOUBLE PRECISION; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_files + DROP COLUMN markers_confidence, + DROP COLUMN markers_source; + +DROP INDEX IF EXISTS idx_media_analyzer_bindings_folder_installation; +DROP TABLE IF EXISTS media_analyzer_bindings; + +DROP INDEX IF EXISTS idx_plugin_auth_identities_installation_subject; +DROP TABLE IF EXISTS plugin_auth_identities; + +DROP INDEX IF EXISTS idx_plugin_auth_bindings_installation_capability; +DROP TABLE IF EXISTS plugin_auth_bindings; + +DROP INDEX IF EXISTS idx_plugin_task_bindings_installation_capability; +DROP TABLE IF EXISTS plugin_task_bindings; + +DROP INDEX IF EXISTS idx_plugin_runtime_configs_installation_key; +DROP TABLE IF EXISTS plugin_runtime_configs; + +DROP INDEX IF EXISTS idx_plugin_capabilities_installation_capability; +DROP INDEX IF EXISTS idx_plugin_capabilities_installation; +DROP TABLE IF EXISTS plugin_capabilities; + +DROP INDEX IF EXISTS idx_plugin_installations_plugin_version; +DROP TABLE IF EXISTS plugin_installations; + +DROP INDEX IF EXISTS idx_plugin_repositories_enabled; +DROP TABLE IF EXISTS plugin_repositories; +-- +goose StatementEnd diff --git a/migrations/032_plugin_archives.up.sql b/migrations/sql/032_plugin_archives.sql similarity index 80% rename from migrations/032_plugin_archives.up.sql rename to migrations/sql/032_plugin_archives.sql index 22b8d64d..13bd5b73 100644 --- a/migrations/032_plugin_archives.up.sql +++ b/migrations/sql/032_plugin_archives.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE plugin_archives ( plugin_installation_id BIGINT PRIMARY KEY REFERENCES plugin_installations(id) ON DELETE CASCADE, manifest_json BYTEA NOT NULL, @@ -9,3 +11,9 @@ CREATE TABLE plugin_archives ( created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS plugin_archives; +-- +goose StatementEnd diff --git a/migrations/sql/033_plugin_update_policy.sql b/migrations/sql/033_plugin_update_policy.sql new file mode 100644 index 00000000..ce0bb369 --- /dev/null +++ b/migrations/sql/033_plugin_update_policy.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE plugin_installations + ADD COLUMN update_policy TEXT NOT NULL DEFAULT 'auto', + ADD COLUMN available_version TEXT; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE plugin_installations + DROP COLUMN update_policy, + DROP COLUMN available_version; +-- +goose StatementEnd diff --git a/migrations/034_plugin_auth_local_passwords.up.sql b/migrations/sql/034_plugin_auth_local_passwords.sql similarity index 54% rename from migrations/034_plugin_auth_local_passwords.up.sql rename to migrations/sql/034_plugin_auth_local_passwords.sql index 28a7f43d..a7d53212 100644 --- a/migrations/034_plugin_auth_local_passwords.up.sql +++ b/migrations/sql/034_plugin_auth_local_passwords.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.users ADD COLUMN local_password_login_enabled BOOLEAN NOT NULL DEFAULT TRUE; @@ -7,3 +9,10 @@ WHERE id IN ( SELECT DISTINCT user_id FROM public.plugin_auth_identities ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.users + DROP COLUMN IF EXISTS local_password_login_enabled; +-- +goose StatementEnd diff --git a/migrations/035_metadata_cache_images_setting.up.sql b/migrations/sql/035_metadata_cache_images_setting.sql similarity index 64% rename from migrations/035_metadata_cache_images_setting.up.sql rename to migrations/sql/035_metadata_cache_images_setting.sql index 615a5066..2c7ecd09 100644 --- a/migrations/035_metadata_cache_images_setting.up.sql +++ b/migrations/sql/035_metadata_cache_images_setting.sql @@ -1,3 +1,6 @@ +-- +goose Up +-- +goose StatementBegin INSERT INTO server_settings (key, value) VALUES ('metadata.cache_images', 'false') ON CONFLICT (key) DO NOTHING; +-- +goose StatementEnd diff --git a/migrations/036_stale_media_ids.up.sql b/migrations/sql/036_stale_media_ids.sql similarity index 67% rename from migrations/036_stale_media_ids.up.sql rename to migrations/sql/036_stale_media_ids.sql index c851d42d..cd729ee5 100644 --- a/migrations/036_stale_media_ids.up.sql +++ b/migrations/sql/036_stale_media_ids.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE stale_media_ids ( content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, provider TEXT NOT NULL, @@ -6,3 +8,9 @@ CREATE TABLE stale_media_ids ( last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (content_id, provider) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS stale_media_ids; +-- +goose StatementEnd diff --git a/migrations/037_provider_chain_content_levels.up.sql b/migrations/sql/037_provider_chain_content_levels.sql similarity index 50% rename from migrations/037_provider_chain_content_levels.up.sql rename to migrations/sql/037_provider_chain_content_levels.sql index d53a49c4..ac4a88fb 100644 --- a/migrations/037_provider_chain_content_levels.up.sql +++ b/migrations/sql/037_provider_chain_content_levels.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Add content_level column to support per-level provider priorities. -- Empty string means legacy flat chain (applies to all levels). ALTER TABLE library_provider_chains @@ -10,3 +12,20 @@ ALTER TABLE library_provider_chains ALTER TABLE library_provider_chains ADD PRIMARY KEY (media_folder_id, provider_id, content_level); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Remove per-level entries, keeping only legacy flat entries. +DELETE FROM library_provider_chains WHERE content_level != ''; + +ALTER TABLE library_provider_chains + DROP CONSTRAINT library_provider_chains_pkey; + +ALTER TABLE library_provider_chains + ADD PRIMARY KEY (media_folder_id, provider_id); + +ALTER TABLE library_provider_chains + DROP COLUMN enabled, + DROP COLUMN content_level; +-- +goose StatementEnd diff --git a/migrations/038_media_item_roots.up.sql b/migrations/sql/038_media_item_roots.sql similarity index 74% rename from migrations/038_media_item_roots.up.sql rename to migrations/sql/038_media_item_roots.sql index 2bf7a44c..2e9d8ef0 100644 --- a/migrations/038_media_item_roots.up.sql +++ b/migrations/sql/038_media_item_roots.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE media_item_roots ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, canonical_root_path TEXT NOT NULL, @@ -9,3 +11,9 @@ CREATE TABLE media_item_roots ( CREATE INDEX idx_media_item_roots_content_id ON media_item_roots (content_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS media_item_roots; +-- +goose StatementEnd diff --git a/migrations/039_plex_history_import.up.sql b/migrations/sql/039_plex_history_import.sql similarity index 64% rename from migrations/039_plex_history_import.up.sql rename to migrations/sql/039_plex_history_import.sql index 4d51f4a3..3801b575 100644 --- a/migrations/039_plex_history_import.up.sql +++ b/migrations/sql/039_plex_history_import.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Expand source_type to allow 'plex' ALTER TABLE history_import_sources DROP CONSTRAINT history_import_sources_source_type_check, @@ -33,3 +35,24 @@ CREATE INDEX idx_history_import_plex_sessions_user_expires CREATE INDEX idx_history_import_plex_sessions_expiry ON history_import_plex_sessions (expires_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS history_import_plex_sessions; + +ALTER TABLE history_import_runs + DROP CONSTRAINT history_import_runs_connection_mode_check, + ADD CONSTRAINT history_import_runs_connection_mode_check + CHECK (connection_mode IN ('connect', 'predefined', 'custom')); + +ALTER TABLE history_import_runs + DROP CONSTRAINT history_import_runs_source_type_check, + ADD CONSTRAINT history_import_runs_source_type_check + CHECK (source_type IN ('emby')); + +ALTER TABLE history_import_sources + DROP CONSTRAINT history_import_sources_source_type_check, + ADD CONSTRAINT history_import_sources_source_type_check + CHECK (source_type IN ('emby')); +-- +goose StatementEnd diff --git a/migrations/040_history_import_run_heartbeats.up.sql b/migrations/sql/040_history_import_run_heartbeats.sql similarity index 57% rename from migrations/040_history_import_run_heartbeats.up.sql rename to migrations/sql/040_history_import_run_heartbeats.sql index e3d35054..12568035 100644 --- a/migrations/040_history_import_run_heartbeats.up.sql +++ b/migrations/sql/040_history_import_run_heartbeats.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE history_import_runs ADD COLUMN last_heartbeat_at timestamptz; @@ -9,3 +11,12 @@ WHERE status = 'running' CREATE INDEX idx_history_import_runs_running_heartbeat ON history_import_runs (status, last_heartbeat_at) WHERE status = 'running'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_history_import_runs_running_heartbeat; + +ALTER TABLE history_import_runs + DROP COLUMN IF EXISTS last_heartbeat_at; +-- +goose StatementEnd diff --git a/migrations/sql/041_jellyfin_history_import.sql b/migrations/sql/041_jellyfin_history_import.sql new file mode 100644 index 00000000..3be0c064 --- /dev/null +++ b/migrations/sql/041_jellyfin_history_import.sql @@ -0,0 +1,16 @@ +-- +goose Up +-- +goose StatementBegin +-- Expand run source_type to allow 'jellyfin' +ALTER TABLE history_import_runs + DROP CONSTRAINT history_import_runs_source_type_check, + ADD CONSTRAINT history_import_runs_source_type_check + CHECK (source_type IN ('emby', 'jellyfin', 'plex')); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE history_import_runs + DROP CONSTRAINT history_import_runs_source_type_check, + ADD CONSTRAINT history_import_runs_source_type_check + CHECK (source_type IN ('emby', 'plex')); +-- +goose StatementEnd diff --git a/migrations/042_downloads.up.sql b/migrations/sql/042_downloads.sql similarity index 89% rename from migrations/042_downloads.up.sql rename to migrations/sql/042_downloads.sql index 1f87ad9b..d3e699e9 100644 --- a/migrations/042_downloads.up.sql +++ b/migrations/sql/042_downloads.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.downloads ( id text NOT NULL, user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -22,3 +24,9 @@ CREATE INDEX downloads_user_id_idx ON public.downloads (user_id); CREATE INDEX downloads_user_status_idx ON public.downloads (user_id, status); CREATE INDEX downloads_user_created_at_idx ON public.downloads (user_id, created_at DESC); CREATE INDEX downloads_batch_id_idx ON public.downloads (batch_id) WHERE batch_id IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.downloads; +-- +goose StatementEnd diff --git a/migrations/sql/043_empty_trash_setting.sql b/migrations/sql/043_empty_trash_setting.sql new file mode 100644 index 00000000..acd1f23b --- /dev/null +++ b/migrations/sql/043_empty_trash_setting.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +INSERT INTO server_settings (key, value) +VALUES ('scanner.empty_trash_after_scan', 'true') +ON CONFLICT (key) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM server_settings WHERE key = 'scanner.empty_trash_after_scan'; +-- +goose StatementEnd diff --git a/migrations/044_embedding_orphan_cleanup.up.sql b/migrations/sql/044_embedding_orphan_cleanup.sql similarity index 63% rename from migrations/044_embedding_orphan_cleanup.up.sql rename to migrations/sql/044_embedding_orphan_cleanup.sql index 5e23797c..7e6ef221 100644 --- a/migrations/044_embedding_orphan_cleanup.up.sql +++ b/migrations/sql/044_embedding_orphan_cleanup.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Remove orphaned embeddings for items that no longer exist. DELETE FROM media_item_embeddings WHERE media_item_id NOT IN (SELECT content_id FROM media_items); @@ -6,3 +8,10 @@ WHERE media_item_id NOT IN (SELECT content_id FROM media_items); ALTER TABLE media_item_embeddings ADD CONSTRAINT media_item_embeddings_media_item_id_fkey FOREIGN KEY (media_item_id) REFERENCES media_items(content_id) ON DELETE CASCADE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_item_embeddings + DROP CONSTRAINT IF EXISTS media_item_embeddings_media_item_id_fkey; +-- +goose StatementEnd diff --git a/migrations/045_library_sort_order.up.sql b/migrations/sql/045_library_sort_order.sql similarity index 64% rename from migrations/045_library_sort_order.up.sql rename to migrations/sql/045_library_sort_order.sql index 1916c1f0..dd1435cf 100644 --- a/migrations/045_library_sort_order.up.sql +++ b/migrations/sql/045_library_sort_order.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_folders ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; @@ -5,3 +7,9 @@ ALTER TABLE media_folders UPDATE media_folders SET sort_order = sub.pos FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY id) - 1 AS pos FROM media_folders) sub WHERE media_folders.id = sub.id; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_folders DROP COLUMN sort_order; +-- +goose StatementEnd diff --git a/migrations/046_drop_media_analyzer_bindings.down.sql b/migrations/sql/046_drop_media_analyzer_bindings.sql similarity index 71% rename from migrations/046_drop_media_analyzer_bindings.down.sql rename to migrations/sql/046_drop_media_analyzer_bindings.sql index 4a13c654..a3866daf 100644 --- a/migrations/046_drop_media_analyzer_bindings.down.sql +++ b/migrations/sql/046_drop_media_analyzer_bindings.sql @@ -1,3 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_media_analyzer_bindings_folder_installation; +DROP TABLE IF EXISTS media_analyzer_bindings; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin CREATE TABLE media_analyzer_bindings ( id BIGSERIAL PRIMARY KEY, media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, @@ -9,3 +17,4 @@ CREATE TABLE media_analyzer_bindings ( CREATE UNIQUE INDEX idx_media_analyzer_bindings_folder_installation ON media_analyzer_bindings (media_folder_id, plugin_installation_id); +-- +goose StatementEnd diff --git a/migrations/047_remove_metadata_providers.down.sql b/migrations/sql/047_remove_metadata_providers.sql similarity index 51% rename from migrations/047_remove_metadata_providers.down.sql rename to migrations/sql/047_remove_metadata_providers.sql index 52c0cbf0..bf3bcc53 100644 --- a/migrations/047_remove_metadata_providers.down.sql +++ b/migrations/sql/047_remove_metadata_providers.sql @@ -1,3 +1,62 @@ +-- +goose Up +-- +goose StatementBegin +-- Remove the metadata_providers mirror table. +-- library_provider_chains now references plugin capabilities directly +-- via the composite natural key (plugin_installation_id, capability_id). + +BEGIN; + +-- Step 1: Add new columns (nullable initially for the data migration). +ALTER TABLE library_provider_chains + ADD COLUMN plugin_installation_id BIGINT, + ADD COLUMN capability_id TEXT, + ADD COLUMN capability_type TEXT NOT NULL DEFAULT 'metadata_provider.v1'; + +-- Step 2: Populate from metadata_providers settings. +UPDATE library_provider_chains lpc +SET plugin_installation_id = (mp.settings->>'plugin_installation_id')::bigint, + capability_id = mp.settings->>'capability_id' +FROM metadata_providers mp +WHERE lpc.provider_id = mp.id; + +-- Step 3: Delete orphaned rows that couldn't be mapped (provider was +-- uninstalled but chain entry lingered). These are invalid anyway. +DELETE FROM library_provider_chains +WHERE plugin_installation_id IS NULL OR capability_id IS NULL; + +-- Step 4: Make new columns NOT NULL now that data is populated. +ALTER TABLE library_provider_chains + ALTER COLUMN plugin_installation_id SET NOT NULL, + ALTER COLUMN capability_id SET NOT NULL; + +-- Step 5: Drop old constraints and column. +ALTER TABLE library_provider_chains + DROP CONSTRAINT library_provider_chains_provider_id_fkey; + +ALTER TABLE library_provider_chains + DROP CONSTRAINT library_provider_chains_pkey; + +ALTER TABLE library_provider_chains + DROP COLUMN provider_id; + +-- Step 6: New PK and FK. +ALTER TABLE library_provider_chains + ADD PRIMARY KEY (media_folder_id, plugin_installation_id, capability_id, content_level); + +ALTER TABLE library_provider_chains + ADD CONSTRAINT library_provider_chains_installation_fkey + FOREIGN KEY (plugin_installation_id) + REFERENCES plugin_installations(id) ON DELETE CASCADE; + +-- Step 7: Drop the mirror table. +DROP TABLE IF EXISTS metadata_providers; +DROP SEQUENCE IF EXISTS metadata_providers_id_seq; + +COMMIT; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin -- Reverse: recreate metadata_providers and remap chain entries back. -- Note: display_name may not round-trip perfectly. @@ -65,3 +124,4 @@ ALTER TABLE library_provider_chains FOREIGN KEY (provider_id) REFERENCES metadata_providers(id) ON DELETE CASCADE; COMMIT; +-- +goose StatementEnd diff --git a/migrations/048_library_refresh_jobs.up.sql b/migrations/sql/048_library_refresh_jobs.sql similarity index 56% rename from migrations/048_library_refresh_jobs.up.sql rename to migrations/sql/048_library_refresh_jobs.sql index 9ef7279f..cc5264f6 100644 --- a/migrations/048_library_refresh_jobs.up.sql +++ b/migrations/sql/048_library_refresh_jobs.sql @@ -1,6 +1,14 @@ +-- +goose Up +-- +goose StatementBegin CREATE UNIQUE INDEX admin_jobs_active_library_refresh_idx ON public.admin_jobs USING btree (job_type, ((request_payload->>'library_id'))) WHERE ( status = ANY (ARRAY['queued'::text, 'running'::text]) AND job_type = 'library_refresh'::text ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.admin_jobs_active_library_refresh_idx; +-- +goose StatementEnd diff --git a/migrations/049_admin_history_import.up.sql b/migrations/sql/049_admin_history_import.sql similarity index 73% rename from migrations/049_admin_history_import.up.sql rename to migrations/sql/049_admin_history_import.sql index 8be38b57..db595c3e 100644 --- a/migrations/049_admin_history_import.up.sql +++ b/migrations/sql/049_admin_history_import.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Add encrypted admin token to history import sources. -- The token is encrypted at rest (AES-256-GCM) and never returned in API responses. ALTER TABLE history_import_sources @@ -41,3 +43,25 @@ ALTER TABLE history_import_runs DROP CONSTRAINT history_import_runs_connection_mode_check, ADD CONSTRAINT history_import_runs_connection_mode_check CHECK (connection_mode IN ('connect', 'predefined', 'custom', 'plex_oauth', 'admin_token')); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE history_import_runs + DROP CONSTRAINT history_import_runs_connection_mode_check, + ADD CONSTRAINT history_import_runs_connection_mode_check + CHECK (connection_mode IN ('connect', 'predefined', 'custom', 'plex_oauth')); + +DROP INDEX IF EXISTS idx_history_import_runs_mapping; + +ALTER TABLE history_import_runs + DROP COLUMN IF EXISTS mapping_id; + +DROP INDEX IF EXISTS idx_history_import_user_mappings_user; +DROP INDEX IF EXISTS idx_history_import_user_mappings_source; + +DROP TABLE IF EXISTS history_import_user_mappings; + +ALTER TABLE history_import_sources + DROP COLUMN IF EXISTS encrypted_admin_token; +-- +goose StatementEnd diff --git a/migrations/sql/050_rename_encrypted_admin_token.sql b/migrations/sql/050_rename_encrypted_admin_token.sql new file mode 100644 index 00000000..a5e4d20d --- /dev/null +++ b/migrations/sql/050_rename_encrypted_admin_token.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +-- Rename column: encryption was removed; plain "admin_token" is clearer. +ALTER TABLE history_import_sources RENAME COLUMN encrypted_admin_token TO admin_token; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE history_import_sources RENAME COLUMN admin_token TO encrypted_admin_token; +-- +goose StatementEnd diff --git a/migrations/051_allow_jellyfin_sources.up.sql b/migrations/sql/051_allow_jellyfin_sources.sql similarity index 56% rename from migrations/051_allow_jellyfin_sources.up.sql rename to migrations/sql/051_allow_jellyfin_sources.sql index 4a43329c..4a01b740 100644 --- a/migrations/051_allow_jellyfin_sources.up.sql +++ b/migrations/sql/051_allow_jellyfin_sources.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Allow 'jellyfin' as a source_type for history import sources. -- Previously only 'emby' and 'plex' were allowed because jellyfin self-service -- imports used inline credentials rather than predefined sources. @@ -5,3 +7,12 @@ ALTER TABLE history_import_sources DROP CONSTRAINT history_import_sources_source_type_check, ADD CONSTRAINT history_import_sources_source_type_check CHECK (source_type IN ('emby', 'jellyfin', 'plex')); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE history_import_sources + DROP CONSTRAINT history_import_sources_source_type_check, + ADD CONSTRAINT history_import_sources_source_type_check + CHECK (source_type IN ('emby', 'plex')); +-- +goose StatementEnd diff --git a/migrations/sql/052_library_metadata_language.sql b/migrations/sql/052_library_metadata_language.sql new file mode 100644 index 00000000..d0a98d7d --- /dev/null +++ b/migrations/sql/052_library_metadata_language.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE media_folders ADD COLUMN metadata_language text NOT NULL DEFAULT 'en'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_folders DROP COLUMN metadata_language; +-- +goose StatementEnd diff --git a/migrations/053_user_history_hidden_items.up.sql b/migrations/sql/053_user_history_hidden_items.sql similarity index 69% rename from migrations/053_user_history_hidden_items.up.sql rename to migrations/sql/053_user_history_hidden_items.sql index 126db996..ed56e078 100644 --- a/migrations/053_user_history_hidden_items.up.sql +++ b/migrations/sql/053_user_history_hidden_items.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.user_history_hidden_items ( user_id integer NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, profile_id text NOT NULL, @@ -9,3 +11,11 @@ CREATE TABLE public.user_history_hidden_items ( CREATE INDEX idx_user_history_hidden_items_profile ON public.user_history_hidden_items (user_id, profile_id, hidden_before DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_history_hidden_items_profile; + +DROP TABLE IF EXISTS public.user_history_hidden_items; +-- +goose StatementEnd diff --git a/migrations/054_catalog_filter_support_indexes.up.sql b/migrations/sql/054_catalog_filter_support_indexes.sql similarity index 54% rename from migrations/054_catalog_filter_support_indexes.up.sql rename to migrations/sql/054_catalog_filter_support_indexes.sql index 1b5f52c2..707897d5 100644 --- a/migrations/054_catalog_filter_support_indexes.up.sql +++ b/migrations/sql/054_catalog_filter_support_indexes.sql @@ -1,6 +1,16 @@ +-- +goose Up +-- +goose StatementBegin CREATE INDEX IF NOT EXISTS idx_item_people_content_kind_person ON public.item_people USING btree (content_id, kind, person_id); CREATE INDEX IF NOT EXISTS idx_user_watch_history_profile_item_completed ON public.user_watch_history USING btree (user_id, profile_id, media_item_id, watched_at DESC) WHERE completed = TRUE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_user_watch_history_profile_item_completed; + +DROP INDEX IF EXISTS public.idx_item_people_content_kind_person; +-- +goose StatementEnd diff --git a/migrations/055_metadata_localizations.up.sql b/migrations/sql/055_metadata_localizations.sql similarity index 72% rename from migrations/055_metadata_localizations.up.sql rename to migrations/sql/055_metadata_localizations.sql index 463f4649..b9e4bfde 100644 --- a/migrations/055_metadata_localizations.up.sql +++ b/migrations/sql/055_metadata_localizations.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_items ADD COLUMN default_metadata_language text NOT NULL DEFAULT 'en'; @@ -49,3 +51,24 @@ CREATE TABLE episode_localizations ( CREATE INDEX idx_media_item_localizations_language ON media_item_localizations(language); CREATE INDEX idx_season_localizations_language ON season_localizations(language); CREATE INDEX idx_episode_localizations_language ON episode_localizations(language); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_episode_localizations_language; +DROP INDEX IF EXISTS idx_season_localizations_language; +DROP INDEX IF EXISTS idx_media_item_localizations_language; + +DROP TABLE IF EXISTS episode_localizations; +DROP TABLE IF EXISTS season_localizations; +DROP TABLE IF EXISTS media_item_localizations; + +ALTER TABLE episodes + DROP COLUMN IF EXISTS default_metadata_language; + +ALTER TABLE seasons + DROP COLUMN IF EXISTS default_metadata_language; + +ALTER TABLE media_items + DROP COLUMN IF EXISTS default_metadata_language; +-- +goose StatementEnd diff --git a/migrations/sql/056_user_max_profiles.sql b/migrations/sql/056_user_max_profiles.sql new file mode 100644 index 00000000..9b66c219 --- /dev/null +++ b/migrations/sql/056_user_max_profiles.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.users + ADD COLUMN max_profiles integer DEFAULT 0 NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.users + DROP COLUMN IF EXISTS max_profiles; +-- +goose StatementEnd diff --git a/migrations/057_calendar_indexes.up.sql b/migrations/sql/057_calendar_indexes.sql similarity index 62% rename from migrations/057_calendar_indexes.up.sql rename to migrations/sql/057_calendar_indexes.sql index e8cc1c8e..2f82b13a 100644 --- a/migrations/057_calendar_indexes.up.sql +++ b/migrations/sql/057_calendar_indexes.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Calendar feature: add indexes on date columns for efficient range queries. CREATE INDEX IF NOT EXISTS idx_episodes_air_date ON episodes (air_date) @@ -10,3 +12,11 @@ CREATE INDEX IF NOT EXISTS idx_seasons_air_date CREATE INDEX IF NOT EXISTS idx_media_items_movie_release_date ON media_items (release_date, content_id) WHERE type = 'movie' AND release_date IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_episodes_air_date; +DROP INDEX IF EXISTS idx_seasons_air_date; +DROP INDEX IF EXISTS idx_media_items_movie_release_date; +-- +goose StatementEnd diff --git a/migrations/sql/058_add_air_time.sql b/migrations/sql/058_add_air_time.sql new file mode 100644 index 00000000..246c9ca0 --- /dev/null +++ b/migrations/sql/058_add_air_time.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE media_items ADD COLUMN air_time text; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_items DROP COLUMN air_time; +-- +goose StatementEnd diff --git a/migrations/059_plex_sync.up.sql b/migrations/sql/059_plex_sync.sql similarity index 90% rename from migrations/059_plex_sync.up.sql rename to migrations/sql/059_plex_sync.sql index 23822058..99e35b80 100644 --- a/migrations/059_plex_sync.up.sql +++ b/migrations/sql/059_plex_sync.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.plex_sync_connections ( id uuid PRIMARY KEY, user_id integer NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -71,3 +73,12 @@ CREATE INDEX idx_plex_sync_item_bindings_connection_last_seen CREATE INDEX idx_plex_sync_item_state_mapping_updated ON public.plex_sync_item_state (mapping_id, updated_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.plex_sync_item_state; +DROP TABLE IF EXISTS public.plex_sync_item_bindings; +DROP TABLE IF EXISTS public.plex_sync_actor_mappings; +DROP TABLE IF EXISTS public.plex_sync_connections; +-- +goose StatementEnd diff --git a/migrations/060_webhook_sync.up.sql b/migrations/sql/060_webhook_sync.sql similarity index 93% rename from migrations/060_webhook_sync.up.sql rename to migrations/sql/060_webhook_sync.sql index 8b81a589..ee2de107 100644 --- a/migrations/060_webhook_sync.up.sql +++ b/migrations/sql/060_webhook_sync.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.webhook_sync_connections ( id uuid PRIMARY KEY, user_id integer NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, @@ -132,3 +134,11 @@ LEFT JOIN public.plex_sync_item_bindings b ON b.connection_id = m.connection_id AND b.media_item_id = s.media_item_id ON CONFLICT (connection_id, external_actor_id, external_item_id) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.webhook_sync_item_state; +DROP TABLE IF EXISTS public.webhook_sync_actor_mappings; +DROP TABLE IF EXISTS public.webhook_sync_connections; +-- +goose StatementEnd diff --git a/migrations/061_user_series_playback_preferences.up.sql b/migrations/sql/061_user_series_playback_preferences.sql similarity index 72% rename from migrations/061_user_series_playback_preferences.up.sql rename to migrations/sql/061_user_series_playback_preferences.sql index 0c66428a..ae583a2b 100644 --- a/migrations/061_user_series_playback_preferences.up.sql +++ b/migrations/sql/061_user_series_playback_preferences.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS user_series_playback_preferences ( user_id INTEGER NOT NULL, profile_id TEXT NOT NULL, @@ -10,3 +12,9 @@ CREATE TABLE IF NOT EXISTS user_series_playback_preferences ( CONSTRAINT user_series_playback_preferences_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS user_series_playback_preferences; +-- +goose StatementEnd diff --git a/migrations/062_playback_requested_media_file.up.sql b/migrations/sql/062_playback_requested_media_file.sql similarity index 50% rename from migrations/062_playback_requested_media_file.up.sql rename to migrations/sql/062_playback_requested_media_file.sql index aac43575..172afa71 100644 --- a/migrations/062_playback_requested_media_file.up.sql +++ b/migrations/sql/062_playback_requested_media_file.sql @@ -1,6 +1,15 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE playback_sessions_sync ADD COLUMN IF NOT EXISTS requested_media_file_id integer; UPDATE playback_sessions_sync SET requested_media_file_id = media_file_id WHERE requested_media_file_id IS NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync + DROP COLUMN IF EXISTS requested_media_file_id; +-- +goose StatementEnd diff --git a/migrations/063_collection_sync_schedule.up.sql b/migrations/sql/063_collection_sync_schedule.sql similarity index 50% rename from migrations/063_collection_sync_schedule.up.sql rename to migrations/sql/063_collection_sync_schedule.sql index d3e74483..c008afe0 100644 --- a/migrations/063_collection_sync_schedule.up.sql +++ b/migrations/sql/063_collection_sync_schedule.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE library_collections ADD COLUMN IF NOT EXISTS sync_schedule TEXT, ADD COLUMN IF NOT EXISTS next_sync_at TIMESTAMPTZ; @@ -5,3 +7,13 @@ ALTER TABLE library_collections CREATE INDEX IF NOT EXISTS idx_library_collections_next_sync_due ON library_collections (next_sync_at) WHERE sync_schedule IS NOT NULL AND next_sync_at IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_library_collections_next_sync_due; + +ALTER TABLE library_collections + DROP COLUMN IF EXISTS next_sync_at, + DROP COLUMN IF EXISTS sync_schedule; +-- +goose StatementEnd diff --git a/migrations/sql/064_collection_poster_auto_generated.sql b/migrations/sql/064_collection_poster_auto_generated.sql new file mode 100644 index 00000000..dee57f64 --- /dev/null +++ b/migrations/sql/064_collection_poster_auto_generated.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections + ADD COLUMN IF NOT EXISTS poster_auto_generated BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_auto_generated; +-- +goose StatementEnd diff --git a/migrations/sql/065_user_track_signatures.sql b/migrations/sql/065_user_track_signatures.sql new file mode 100644 index 00000000..d9b62522 --- /dev/null +++ b/migrations/sql/065_user_track_signatures.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE user_audio_preferences + ADD COLUMN IF NOT EXISTS audio_track_signature JSONB NOT NULL DEFAULT '{}'::jsonb; + +ALTER TABLE user_subtitle_preferences + ADD COLUMN IF NOT EXISTS subtitle_track_signature JSONB NOT NULL DEFAULT '{}'::jsonb; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE user_audio_preferences + DROP COLUMN IF EXISTS audio_track_signature; + +ALTER TABLE user_subtitle_preferences + DROP COLUMN IF EXISTS subtitle_track_signature; +-- +goose StatementEnd diff --git a/migrations/sql/066_media_chapters_and_library_thumbnail_settings.sql b/migrations/sql/066_media_chapters_and_library_thumbnail_settings.sql new file mode 100644 index 00000000..c141e7d2 --- /dev/null +++ b/migrations/sql/066_media_chapters_and_library_thumbnail_settings.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.media_files + ADD COLUMN chapters jsonb; + +ALTER TABLE public.media_folders + ADD COLUMN chapter_thumbnails_enabled boolean NOT NULL DEFAULT false; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.media_folders + DROP COLUMN IF EXISTS chapter_thumbnails_enabled; + +ALTER TABLE public.media_files + DROP COLUMN IF EXISTS chapters; +-- +goose StatementEnd diff --git a/migrations/sql/067_media_file_chapter_thumbnail_failure_state.sql b/migrations/sql/067_media_file_chapter_thumbnail_failure_state.sql new file mode 100644 index 00000000..5195333b --- /dev/null +++ b/migrations/sql/067_media_file_chapter_thumbnail_failure_state.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.media_files + ADD COLUMN chapter_thumbnail_retry_after timestamptz, + ADD COLUMN chapter_thumbnail_failure_count integer NOT NULL DEFAULT 0, + ADD COLUMN chapter_thumbnail_last_error text; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.media_files + DROP COLUMN IF EXISTS chapter_thumbnail_last_error, + DROP COLUMN IF EXISTS chapter_thumbnail_failure_count, + DROP COLUMN IF EXISTS chapter_thumbnail_retry_after; +-- +goose StatementEnd diff --git a/migrations/068_webhook_sync_event_logs.up.sql b/migrations/sql/068_webhook_sync_event_logs.sql similarity index 79% rename from migrations/068_webhook_sync_event_logs.up.sql rename to migrations/sql/068_webhook_sync_event_logs.sql index 4cf4128f..e36541c5 100644 --- a/migrations/068_webhook_sync_event_logs.up.sql +++ b/migrations/sql/068_webhook_sync_event_logs.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.webhook_sync_event_logs ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, connection_id uuid NOT NULL REFERENCES public.webhook_sync_connections(id) ON DELETE CASCADE, @@ -13,3 +15,9 @@ CREATE TABLE public.webhook_sync_event_logs ( CREATE INDEX idx_webhook_sync_event_logs_connection_received ON public.webhook_sync_event_logs (connection_id, received_at DESC, id DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.webhook_sync_event_logs; +-- +goose StatementEnd diff --git a/migrations/069_watch_together_rooms.up.sql b/migrations/sql/069_watch_together_rooms.sql similarity index 81% rename from migrations/069_watch_together_rooms.up.sql rename to migrations/sql/069_watch_together_rooms.sql index 3c8093b9..2df6ed5c 100644 --- a/migrations/069_watch_together_rooms.up.sql +++ b/migrations/sql/069_watch_together_rooms.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE watch_together_rooms ( id TEXT PRIMARY KEY, code TEXT NOT NULL UNIQUE, @@ -23,3 +25,10 @@ CREATE TABLE watch_together_rooms ( CREATE INDEX idx_watch_together_rooms_status_created_at ON watch_together_rooms (status, created_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_watch_together_rooms_status_created_at; +DROP TABLE IF EXISTS watch_together_rooms; +-- +goose StatementEnd diff --git a/migrations/070_watch_together_room_first.up.sql b/migrations/sql/070_watch_together_room_first.sql similarity index 51% rename from migrations/070_watch_together_room_first.up.sql rename to migrations/sql/070_watch_together_room_first.sql index d218cc9c..ca863207 100644 --- a/migrations/070_watch_together_room_first.up.sql +++ b/migrations/sql/070_watch_together_room_first.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE watch_together_rooms ADD COLUMN phase TEXT NOT NULL DEFAULT 'lobby', ADD COLUMN selection_mode TEXT NOT NULL DEFAULT 'host_pick', @@ -36,3 +38,41 @@ ALTER TABLE watch_together_rooms DROP INDEX IF EXISTS idx_watch_together_rooms_status_created_at; CREATE INDEX idx_watch_together_rooms_phase_created_at ON watch_together_rooms (phase, created_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE watch_together_rooms + ADD COLUMN content_id TEXT, + ADD COLUMN file_id INTEGER, + ADD COLUMN library_id INTEGER, + ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; + +UPDATE watch_together_rooms +SET content_id = COALESCE(selected_content_id, ''), + file_id = selected_file_id, + library_id = selected_library_id, + status = CASE + WHEN phase = 'ended' THEN 'closed' + ELSE 'active' + END; + +ALTER TABLE watch_together_rooms + ALTER COLUMN content_id SET NOT NULL, + DROP CONSTRAINT IF EXISTS watch_together_rooms_phase_check, + DROP CONSTRAINT IF EXISTS watch_together_rooms_selection_mode_check, + DROP COLUMN IF EXISTS phase, + DROP COLUMN IF EXISTS selection_mode, + DROP COLUMN IF EXISTS selection_revision, + DROP COLUMN IF EXISTS selected_content_id, + DROP COLUMN IF EXISTS selected_file_id, + DROP COLUMN IF EXISTS selected_library_id; + +ALTER TABLE watch_together_rooms + ADD CONSTRAINT watch_together_rooms_status_check + CHECK (status IN ('active', 'closed')); + +DROP INDEX IF EXISTS idx_watch_together_rooms_phase_created_at; +CREATE INDEX idx_watch_together_rooms_status_created_at + ON watch_together_rooms (status, created_at DESC); +-- +goose StatementEnd diff --git a/migrations/071_watch_together_suggestions.up.sql b/migrations/sql/071_watch_together_suggestions.sql similarity index 81% rename from migrations/071_watch_together_suggestions.up.sql rename to migrations/sql/071_watch_together_suggestions.sql index 5ca4e954..67a08846 100644 --- a/migrations/071_watch_together_suggestions.up.sql +++ b/migrations/sql/071_watch_together_suggestions.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE watch_together_suggestions ( id TEXT PRIMARY KEY, room_id TEXT NOT NULL REFERENCES watch_together_rooms(id) ON DELETE CASCADE, @@ -23,3 +25,12 @@ CREATE TABLE watch_together_votes ( CREATE INDEX idx_wt_suggestions_room ON watch_together_suggestions(room_id, vote_count DESC, created_at); CREATE INDEX idx_wt_votes_suggestion ON watch_together_votes(suggestion_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_wt_votes_suggestion; +DROP INDEX IF EXISTS idx_wt_suggestions_room; +DROP TABLE IF EXISTS watch_together_votes; +DROP TABLE IF EXISTS watch_together_suggestions; +-- +goose StatementEnd diff --git a/migrations/072_watch_together_playback_state.up.sql b/migrations/sql/072_watch_together_playback_state.sql similarity index 64% rename from migrations/072_watch_together_playback_state.up.sql rename to migrations/sql/072_watch_together_playback_state.sql index 6cf4471a..c535bbc5 100644 --- a/migrations/072_watch_together_playback_state.up.sql +++ b/migrations/sql/072_watch_together_playback_state.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE watch_together_rooms ADD COLUMN playback_state TEXT NOT NULL DEFAULT 'idle', ADD COLUMN resume_on_ready BOOLEAN NOT NULL DEFAULT FALSE; @@ -13,3 +15,12 @@ SET playback_state = CASE ALTER TABLE watch_together_rooms ADD CONSTRAINT watch_together_rooms_playback_state_check CHECK (playback_state IN ('idle', 'waiting', 'paused', 'playing')); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE watch_together_rooms + DROP CONSTRAINT IF EXISTS watch_together_rooms_playback_state_check, + DROP COLUMN IF EXISTS resume_on_ready, + DROP COLUMN IF EXISTS playback_state; +-- +goose StatementEnd diff --git a/migrations/073_device_login_requests.up.sql b/migrations/sql/073_device_login_requests.sql similarity index 86% rename from migrations/073_device_login_requests.up.sql rename to migrations/sql/073_device_login_requests.sql index 8d969044..4de29ec6 100644 --- a/migrations/073_device_login_requests.up.sql +++ b/migrations/sql/073_device_login_requests.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE device_login_requests ( id UUID PRIMARY KEY, device_code_hash TEXT NOT NULL UNIQUE, @@ -26,3 +28,9 @@ CREATE INDEX idx_device_login_requests_expires_at CREATE INDEX idx_device_login_requests_status_expires_at ON device_login_requests (status, expires_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS device_login_requests; +-- +goose StatementEnd diff --git a/migrations/074_scanned_media_roots.up.sql b/migrations/sql/074_scanned_media_roots.sql similarity index 87% rename from migrations/074_scanned_media_roots.up.sql rename to migrations/sql/074_scanned_media_roots.sql index da486790..3aa1603d 100644 --- a/migrations/074_scanned_media_roots.up.sql +++ b/migrations/sql/074_scanned_media_roots.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS scanned_media_roots ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, root_path TEXT NOT NULL, @@ -23,3 +25,9 @@ CREATE INDEX IF NOT EXISTS idx_scanned_media_roots_state_last_seen CREATE INDEX IF NOT EXISTS idx_scanned_media_roots_type_last_seen ON scanned_media_roots (media_folder_id, inferred_type, last_seen_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS scanned_media_roots; +-- +goose StatementEnd diff --git a/migrations/075_media_root_overrides.up.sql b/migrations/sql/075_media_root_overrides.sql similarity index 81% rename from migrations/075_media_root_overrides.up.sql rename to migrations/sql/075_media_root_overrides.sql index 4e15b37b..1df62e74 100644 --- a/migrations/075_media_root_overrides.up.sql +++ b/migrations/sql/075_media_root_overrides.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS media_root_overrides ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, root_path TEXT NOT NULL, @@ -14,3 +16,9 @@ CREATE TABLE IF NOT EXISTS media_root_overrides ( updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (media_folder_id, root_path) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS media_root_overrides; +-- +goose StatementEnd diff --git a/migrations/076_media_file_root_and_variant_metadata.up.sql b/migrations/sql/076_media_file_root_and_variant_metadata.sql similarity index 57% rename from migrations/076_media_file_root_and_variant_metadata.up.sql rename to migrations/sql/076_media_file_root_and_variant_metadata.sql index 2b72f487..89a8ff1a 100644 --- a/migrations/076_media_file_root_and_variant_metadata.up.sql +++ b/migrations/sql/076_media_file_root_and_variant_metadata.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_files ADD COLUMN IF NOT EXISTS canonical_root_path TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS edition_raw TEXT NOT NULL DEFAULT '', @@ -16,3 +18,23 @@ CREATE INDEX IF NOT EXISTS idx_media_files_folder_root CREATE INDEX IF NOT EXISTS idx_media_files_variant_group ON media_files (content_id, edition_key, presentation_group_key, resolution); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_media_files_variant_group; +DROP INDEX IF EXISTS idx_media_files_folder_root; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS multi_episode_end, + DROP COLUMN IF EXISTS multi_episode_start, + DROP COLUMN IF EXISTS presentation_part_total, + DROP COLUMN IF EXISTS presentation_part_index, + DROP COLUMN IF EXISTS presentation_group_key, + DROP COLUMN IF EXISTS presentation_kind, + DROP COLUMN IF EXISTS edition_source, + DROP COLUMN IF EXISTS edition_confidence, + DROP COLUMN IF EXISTS edition_key, + DROP COLUMN IF EXISTS edition_raw, + DROP COLUMN IF EXISTS canonical_root_path; +-- +goose StatementEnd diff --git a/migrations/sql/077_user_progress_edition_hints.sql b/migrations/sql/077_user_progress_edition_hints.sql new file mode 100644 index 00000000..7de0ac6a --- /dev/null +++ b/migrations/sql/077_user_progress_edition_hints.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE user_watch_progress + ADD COLUMN IF NOT EXISTS last_edition_key TEXT; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE user_watch_progress + DROP COLUMN IF EXISTS last_edition_key; +-- +goose StatementEnd diff --git a/migrations/078_content_groups_and_locations.up.sql b/migrations/sql/078_content_groups_and_locations.sql similarity index 85% rename from migrations/078_content_groups_and_locations.up.sql rename to migrations/sql/078_content_groups_and_locations.sql index e489e77d..ca1824b9 100644 --- a/migrations/078_content_groups_and_locations.up.sql +++ b/migrations/sql/078_content_groups_and_locations.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_files ADD COLUMN IF NOT EXISTS observed_root_path TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS content_group_key TEXT NOT NULL DEFAULT '', @@ -99,3 +101,26 @@ CREATE TABLE IF NOT EXISTS media_group_overrides ( updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (media_folder_id, group_key_version, content_group_key) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS media_group_overrides; +DROP TABLE IF EXISTS media_item_groups; +DROP TABLE IF EXISTS media_group_locations; +DROP TABLE IF EXISTS observed_media_locations; +DROP TABLE IF EXISTS scanned_media_groups; + +DROP INDEX IF EXISTS idx_media_files_folder_observed_root; +DROP INDEX IF EXISTS idx_media_files_folder_group; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS identity_json, + DROP COLUMN IF EXISTS identity_confidence, + DROP COLUMN IF EXISTS base_type, + DROP COLUMN IF EXISTS base_year, + DROP COLUMN IF EXISTS base_title, + DROP COLUMN IF EXISTS group_key_version, + DROP COLUMN IF EXISTS content_group_key, + DROP COLUMN IF EXISTS observed_root_path; +-- +goose StatementEnd diff --git a/migrations/079_media_item_provider_ids.up.sql b/migrations/sql/079_media_item_provider_ids.sql similarity index 88% rename from migrations/079_media_item_provider_ids.up.sql rename to migrations/sql/079_media_item_provider_ids.sql index e2a35714..7930c235 100644 --- a/migrations/079_media_item_provider_ids.up.sql +++ b/migrations/sql/079_media_item_provider_ids.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS media_item_provider_ids ( content_id TEXT NOT NULL REFERENCES media_items(content_id) ON DELETE CASCADE, provider TEXT NOT NULL, @@ -54,3 +56,10 @@ ORDER BY content_id ASC, provider ASC; CREATE INDEX IF NOT EXISTS idx_media_item_provider_ids_provider_id ON media_item_provider_ids (provider, provider_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_media_item_provider_ids_provider_id; +DROP TABLE IF EXISTS media_item_provider_ids; +-- +goose StatementEnd diff --git a/migrations/080_series_match_queue.up.sql b/migrations/sql/080_series_match_queue.sql similarity index 75% rename from migrations/080_series_match_queue.up.sql rename to migrations/sql/080_series_match_queue.sql index 9165395a..3688fa73 100644 --- a/migrations/080_series_match_queue.up.sql +++ b/migrations/sql/080_series_match_queue.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS series_match_queue ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, group_key_version INTEGER NOT NULL, @@ -15,3 +17,11 @@ CREATE INDEX IF NOT EXISTS idx_series_match_queue_attempt CREATE INDEX IF NOT EXISTS idx_series_match_queue_folder ON series_match_queue (media_folder_id, group_key_version, content_group_key); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_series_match_queue_folder; +DROP INDEX IF EXISTS idx_series_match_queue_attempt; +DROP TABLE IF EXISTS series_match_queue; +-- +goose StatementEnd diff --git a/migrations/081_series_root_match_queue.up.sql b/migrations/sql/081_series_root_match_queue.sql similarity index 81% rename from migrations/081_series_root_match_queue.up.sql rename to migrations/sql/081_series_root_match_queue.sql index faca5531..ee30f8a8 100644 --- a/migrations/081_series_root_match_queue.up.sql +++ b/migrations/sql/081_series_root_match_queue.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS series_root_match_queue ( media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, observed_root_path TEXT NOT NULL, @@ -14,3 +16,9 @@ CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_attempt CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_folder ON series_root_match_queue (media_folder_id, observed_root_path); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS series_root_match_queue; +-- +goose StatementEnd diff --git a/migrations/sql/082_media_files_file_modified_at.sql b/migrations/sql/082_media_files_file_modified_at.sql new file mode 100644 index 00000000..ae26fba4 --- /dev/null +++ b/migrations/sql/082_media_files_file_modified_at.sql @@ -0,0 +1,16 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE media_files + ADD COLUMN IF NOT EXISTS file_modified_at TIMESTAMPTZ; + +CREATE INDEX IF NOT EXISTS idx_media_files_folder_file_modified_at + ON media_files (media_folder_id, file_modified_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_media_files_folder_file_modified_at; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS file_modified_at; +-- +goose StatementEnd diff --git a/migrations/083_movie_match_queue.up.sql b/migrations/sql/083_movie_match_queue.sql similarity index 82% rename from migrations/083_movie_match_queue.up.sql rename to migrations/sql/083_movie_match_queue.sql index 9387fadf..3b85b33c 100644 --- a/migrations/083_movie_match_queue.up.sql +++ b/migrations/sql/083_movie_match_queue.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS movie_match_queue ( media_file_id INTEGER PRIMARY KEY REFERENCES media_files(id) ON DELETE CASCADE, media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, @@ -14,3 +16,9 @@ CREATE INDEX IF NOT EXISTS idx_movie_match_queue_available CREATE INDEX IF NOT EXISTS idx_movie_match_queue_folder ON movie_match_queue (media_folder_id, media_file_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS movie_match_queue; +-- +goose StatementEnd diff --git a/migrations/sql/084_series_root_match_queue_available_at.sql b/migrations/sql/084_series_root_match_queue_available_at.sql new file mode 100644 index 00000000..1f180720 --- /dev/null +++ b/migrations/sql/084_series_root_match_queue_available_at.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE series_root_match_queue + ADD COLUMN IF NOT EXISTS available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +DROP INDEX IF EXISTS idx_series_root_match_queue_attempt; + +CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_available + ON series_root_match_queue (available_at, last_attempted_at, media_folder_id, observed_root_path); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_series_root_match_queue_available; + +CREATE INDEX IF NOT EXISTS idx_series_root_match_queue_attempt + ON series_root_match_queue (last_attempted_at ASC NULLS FIRST, media_folder_id, observed_root_path); + +ALTER TABLE series_root_match_queue + DROP COLUMN IF EXISTS available_at; +-- +goose StatementEnd diff --git a/migrations/085_scan_runs.up.sql b/migrations/sql/085_scan_runs.sql similarity index 88% rename from migrations/085_scan_runs.up.sql rename to migrations/sql/085_scan_runs.sql index 0244d46a..22980e9d 100644 --- a/migrations/085_scan_runs.up.sql +++ b/migrations/sql/085_scan_runs.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS scan_runs ( id TEXT PRIMARY KEY, media_folder_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, @@ -25,3 +27,9 @@ CREATE INDEX IF NOT EXISTS idx_scan_runs_folder_requested CREATE UNIQUE INDEX IF NOT EXISTS idx_scan_runs_active_scope ON scan_runs (media_folder_id, mode, path) WHERE status = ANY (ARRAY['accepted'::text, 'running'::text]); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS scan_runs; +-- +goose StatementEnd diff --git a/migrations/086_s3_public_private_split.up.sql b/migrations/sql/086_s3_public_private_split.sql similarity index 84% rename from migrations/086_s3_public_private_split.up.sql rename to migrations/sql/086_s3_public_private_split.sql index 604cb6d0..3fe4bd54 100644 --- a/migrations/086_s3_public_private_split.up.sql +++ b/migrations/sql/086_s3_public_private_split.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin INSERT INTO server_settings (key, value) SELECT 's3.public_endpoint', value FROM server_settings WHERE key = 's3.operational_endpoint' ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; @@ -73,3 +75,30 @@ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; INSERT INTO server_settings (key, value) SELECT 's3.public_token_ttl', value FROM server_settings WHERE key = 's3.operational_token_ttl' ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM server_settings +WHERE key IN ( + 's3.public_endpoint', + 's3.public_read_endpoint', + 's3.public_region', + 's3.public_path_style', + 's3.public_bucket', + 's3.public_key_prefix', + 's3.public_access_key', + 's3.public_secret_key', + 's3.public_url_auth', + 's3.public_token_secret', + 's3.public_token_param', + 's3.public_token_ttl', + 's3.private_endpoint', + 's3.private_region', + 's3.private_path_style', + 's3.private_bucket', + 's3.private_key_prefix', + 's3.private_access_key', + 's3.private_secret_key' +); +-- +goose StatementEnd diff --git a/migrations/087_catalog_query_performance_indexes.up.sql b/migrations/sql/087_catalog_query_performance_indexes.sql similarity index 63% rename from migrations/087_catalog_query_performance_indexes.up.sql rename to migrations/sql/087_catalog_query_performance_indexes.sql index ad3b6554..6175e00f 100644 --- a/migrations/087_catalog_query_performance_indexes.up.sql +++ b/migrations/sql/087_catalog_query_performance_indexes.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE INDEX IF NOT EXISTS idx_media_items_studios ON public.media_items USING gin (studios); @@ -22,3 +24,21 @@ WHERE (missing_since IS NULL); CREATE INDEX IF NOT EXISTS idx_episodes_series_air_date ON public.episodes USING btree (series_id, air_date DESC) WHERE (air_date IS NOT NULL); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_episodes_series_air_date; + +DROP INDEX IF EXISTS public.idx_media_files_active_content_folder; + +DROP INDEX IF EXISTS public.idx_media_items_content_rating; + +DROP INDEX IF EXISTS public.idx_media_items_original_language; + +DROP INDEX IF EXISTS public.idx_media_items_countries; + +DROP INDEX IF EXISTS public.idx_media_items_networks; + +DROP INDEX IF EXISTS public.idx_media_items_studios; +-- +goose StatementEnd diff --git a/migrations/088_media_item_provider_id_types.up.sql b/migrations/sql/088_media_item_provider_id_types.sql similarity index 58% rename from migrations/088_media_item_provider_id_types.up.sql rename to migrations/sql/088_media_item_provider_id_types.sql index dc693e58..882d1ab8 100644 --- a/migrations/088_media_item_provider_id_types.up.sql +++ b/migrations/sql/088_media_item_provider_id_types.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_item_provider_ids ADD COLUMN IF NOT EXISTS item_type TEXT; @@ -41,3 +43,35 @@ ALTER TABLE media_item_provider_ids ALTER TABLE media_item_provider_ids ADD CONSTRAINT media_item_provider_ids_provider_provider_id_item_type_key UNIQUE (provider, provider_id, item_type); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM ( + SELECT provider, provider_id + FROM media_item_provider_ids + GROUP BY provider, provider_id + HAVING COUNT(*) > 1 + ) duplicate_ids + ) THEN + RAISE EXCEPTION 'cross-type provider ids exist; resolve duplicates before rolling back migration 088'; + END IF; +END $$; + +ALTER TABLE media_item_provider_ids + DROP CONSTRAINT IF EXISTS media_item_provider_ids_provider_provider_id_item_type_key; + +ALTER TABLE media_item_provider_ids + DROP COLUMN IF EXISTS item_type; + +ALTER TABLE media_item_provider_ids + ADD CONSTRAINT media_item_provider_ids_provider_provider_id_key + UNIQUE (provider, provider_id); + +CREATE INDEX IF NOT EXISTS idx_media_item_provider_ids_provider_id + ON media_item_provider_ids (provider, provider_id); +-- +goose StatementEnd diff --git a/migrations/089_metadata_refresh_debt.up.sql b/migrations/sql/089_metadata_refresh_debt.sql similarity index 95% rename from migrations/089_metadata_refresh_debt.up.sql rename to migrations/sql/089_metadata_refresh_debt.sql index 186725f4..473f5ec3 100644 --- a/migrations/089_metadata_refresh_debt.up.sql +++ b/migrations/sql/089_metadata_refresh_debt.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS metadata_refresh_debt ( content_id TEXT PRIMARY KEY REFERENCES media_items(content_id) ON DELETE CASCADE, priority INTEGER NOT NULL DEFAULT 0, @@ -116,3 +118,9 @@ FROM ( ) mi WHERE reason_mask <> 0 ON CONFLICT (content_id) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS metadata_refresh_debt; +-- +goose StatementEnd diff --git a/migrations/sql/090_episode_catalog_active_file_index.sql b/migrations/sql/090_episode_catalog_active_file_index.sql new file mode 100644 index 00000000..be29d5c6 --- /dev/null +++ b/migrations/sql/090_episode_catalog_active_file_index.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_media_files_active_episode_folder +ON public.media_files USING btree (episode_id, media_folder_id) +WHERE ((episode_id IS NOT NULL) AND (missing_since IS NULL)); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_files_active_episode_folder; +-- +goose StatementEnd diff --git a/migrations/sql/091_episode_release_date_sort_index.sql b/migrations/sql/091_episode_release_date_sort_index.sql new file mode 100644 index 00000000..58fa56d4 --- /dev/null +++ b/migrations/sql/091_episode_release_date_sort_index.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_episodes_air_date_content +ON public.episodes USING btree (air_date DESC, content_id) +WHERE (air_date IS NOT NULL); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_episodes_air_date_content; +-- +goose StatementEnd diff --git a/migrations/092_episode_libraries.up.sql b/migrations/sql/092_episode_libraries.sql similarity index 80% rename from migrations/092_episode_libraries.up.sql rename to migrations/sql/092_episode_libraries.sql index fcb14ad7..03c7fb1c 100644 --- a/migrations/092_episode_libraries.up.sql +++ b/migrations/sql/092_episode_libraries.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS public.episode_libraries ( episode_id text NOT NULL, media_folder_id integer NOT NULL, @@ -21,3 +23,13 @@ ON public.episode_libraries USING btree (media_folder_id, episode_id); CREATE INDEX IF NOT EXISTS idx_episode_libraries_folder_first_seen ON public.episode_libraries USING btree (media_folder_id, first_seen_at DESC, episode_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_episode_libraries_folder_first_seen; + +DROP INDEX IF EXISTS public.idx_episode_libraries_folder_episode; + +DROP TABLE IF EXISTS public.episode_libraries; +-- +goose StatementEnd diff --git a/migrations/093_user_max_profiles_floor.up.sql b/migrations/sql/093_user_max_profiles_floor.sql similarity index 57% rename from migrations/093_user_max_profiles_floor.up.sql rename to migrations/sql/093_user_max_profiles_floor.sql index b7c2a8ae..1ea501ce 100644 --- a/migrations/093_user_max_profiles_floor.up.sql +++ b/migrations/sql/093_user_max_profiles_floor.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin UPDATE public.users SET max_profiles = 5 WHERE max_profiles < 1; @@ -10,3 +12,11 @@ UPDATE public.server_settings SET value = '5' WHERE key = 'defaults.max_profiles' AND (value IS NULL OR value = '' OR value = '0'); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.users + DROP CONSTRAINT IF EXISTS users_max_profiles_min_check, + ALTER COLUMN max_profiles SET DEFAULT 0; +-- +goose StatementEnd diff --git a/migrations/094_profiles_is_primary.up.sql b/migrations/sql/094_profiles_is_primary.sql similarity index 66% rename from migrations/094_profiles_is_primary.up.sql rename to migrations/sql/094_profiles_is_primary.sql index 029deacf..b5141628 100644 --- a/migrations/094_profiles_is_primary.up.sql +++ b/migrations/sql/094_profiles_is_primary.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE user_profiles ADD COLUMN is_primary BOOLEAN NOT NULL DEFAULT false; @@ -18,3 +20,12 @@ WHERE user_profiles.id = ranked.id CREATE UNIQUE INDEX user_profiles_primary_per_user ON user_profiles (user_id) WHERE is_primary; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS user_profiles_primary_per_user; + +ALTER TABLE user_profiles + DROP COLUMN IF EXISTS is_primary; +-- +goose StatementEnd diff --git a/migrations/sql/095_jellyfin_compat_session_ttl_default.sql b/migrations/sql/095_jellyfin_compat_session_ttl_default.sql new file mode 100644 index 00000000..5550f3a9 --- /dev/null +++ b/migrations/sql/095_jellyfin_compat_session_ttl_default.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +UPDATE server_settings +SET value = '87600h' +WHERE key = 'jellyfin_compat.session_ttl' + AND value = '24h'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE server_settings +SET value = '24h' +WHERE key = 'jellyfin_compat.session_ttl' + AND value = '87600h'; +-- +goose StatementEnd diff --git a/migrations/096_user_device_settings.up.sql b/migrations/sql/096_user_device_settings.sql similarity index 78% rename from migrations/096_user_device_settings.up.sql rename to migrations/sql/096_user_device_settings.sql index ced66ac8..231b9a95 100644 --- a/migrations/096_user_device_settings.up.sql +++ b/migrations/sql/096_user_device_settings.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS public.user_device_settings ( user_id integer NOT NULL, device_id text NOT NULL, @@ -13,3 +15,9 @@ CREATE TABLE IF NOT EXISTS public.user_device_settings ( CREATE INDEX IF NOT EXISTS idx_user_device_settings_user_key ON public.user_device_settings (user_id, key); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.user_device_settings; +-- +goose StatementEnd diff --git a/migrations/097_user_device_settings_profile_scope.up.sql b/migrations/sql/097_user_device_settings_profile_scope.sql similarity index 60% rename from migrations/097_user_device_settings_profile_scope.up.sql rename to migrations/sql/097_user_device_settings_profile_scope.sql index 46f30654..c1594cdc 100644 --- a/migrations/097_user_device_settings_profile_scope.up.sql +++ b/migrations/sql/097_user_device_settings_profile_scope.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.user_device_settings RENAME TO user_device_settings_legacy; INSERT INTO public.user_profiles ( @@ -78,3 +80,50 @@ DROP TABLE public.user_device_settings_legacy; ALTER TABLE public.user_device_settings_profile_scoped RENAME TO user_device_settings; ALTER INDEX public.idx_user_device_settings_profile_scoped_user_key RENAME TO idx_user_device_settings_user_key; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.user_device_settings RENAME TO user_device_settings_profile_scoped_legacy; + +CREATE TABLE IF NOT EXISTS public.user_device_settings_legacy_restore ( + user_id integer NOT NULL, + device_id text NOT NULL, + key text NOT NULL, + value text NOT NULL, + device_name text NOT NULL DEFAULT '', + device_platform text NOT NULL DEFAULT '', + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT user_device_settings_legacy_restore_pkey PRIMARY KEY (user_id, device_id, key), + CONSTRAINT user_device_settings_legacy_restore_user_id_fkey + FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE +); + +INSERT INTO public.user_device_settings_legacy_restore ( + user_id, + device_id, + key, + value, + device_name, + device_platform, + updated_at +) +SELECT DISTINCT ON (user_id, device_id, key) + user_id, + device_id, + key, + value, + device_name, + device_platform, + updated_at +FROM public.user_device_settings_profile_scoped_legacy +ORDER BY user_id, device_id, key, updated_at DESC, profile_id ASC; + +CREATE INDEX IF NOT EXISTS idx_user_device_settings_legacy_restore_user_key + ON public.user_device_settings_legacy_restore (user_id, key); + +DROP TABLE public.user_device_settings_profile_scoped_legacy; + +ALTER TABLE public.user_device_settings_legacy_restore RENAME TO user_device_settings; +ALTER INDEX public.idx_user_device_settings_legacy_restore_user_key RENAME TO idx_user_device_settings_user_key; +-- +goose StatementEnd diff --git a/migrations/098_playback_settings_device_scope.up.sql b/migrations/sql/098_playback_settings_device_scope.sql similarity index 63% rename from migrations/098_playback_settings_device_scope.up.sql rename to migrations/sql/098_playback_settings_device_scope.sql index bd285218..58771951 100644 --- a/migrations/098_playback_settings_device_scope.up.sql +++ b/migrations/sql/098_playback_settings_device_scope.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin WITH legacy_playback_settings AS ( SELECT user_id, key, value FROM public.user_settings @@ -41,3 +43,27 @@ FROM legacy_playback_settings AS legacy JOIN known_profile_devices AS device ON device.user_id = legacy.user_id ON CONFLICT (user_id, profile_id, device_id, key) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +INSERT INTO public.user_settings ( + user_id, + key, + value +) +SELECT DISTINCT ON (user_id, key) + user_id, + key, + value +FROM public.user_device_settings +WHERE key IN ( + 'playback.preferred_quality', + 'playback.audio_language', + 'playback.auto_skip_intro', + 'playback.auto_skip_credits', + 'playback.auto_play_next' +) +ORDER BY user_id, key, updated_at DESC, profile_id ASC, device_id ASC +ON CONFLICT (user_id, key) DO NOTHING; +-- +goose StatementEnd diff --git a/migrations/sql/099_media_item_keywords.sql b/migrations/sql/099_media_item_keywords.sql new file mode 100644 index 00000000..04fe3e6e --- /dev/null +++ b/migrations/sql/099_media_item_keywords.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.media_items +ADD COLUMN IF NOT EXISTS keywords TEXT[] NOT NULL DEFAULT '{}'::TEXT[]; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.media_items +DROP COLUMN IF EXISTS keywords; +-- +goose StatementEnd diff --git a/migrations/100_metadata_refresh_debt_targets.up.sql b/migrations/sql/100_metadata_refresh_debt_targets.sql similarity index 71% rename from migrations/100_metadata_refresh_debt_targets.up.sql rename to migrations/sql/100_metadata_refresh_debt_targets.sql index c56132d3..007e203c 100644 --- a/migrations/100_metadata_refresh_debt_targets.up.sql +++ b/migrations/sql/100_metadata_refresh_debt_targets.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE metadata_refresh_debt ADD COLUMN IF NOT EXISTS target_type TEXT NOT NULL DEFAULT 'item'; @@ -68,3 +70,28 @@ WHERE target_type = 'item' DELETE FROM metadata_refresh_debt WHERE target_type = 'item' AND reason_mask = 0; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM metadata_refresh_debt +WHERE target_type <> 'item'; + +ALTER TABLE metadata_refresh_debt + DROP CONSTRAINT IF EXISTS metadata_refresh_debt_pkey; + +ALTER TABLE metadata_refresh_debt + DROP CONSTRAINT IF EXISTS metadata_refresh_debt_target_type_check; + +ALTER TABLE metadata_refresh_debt + ADD PRIMARY KEY (content_id); + +ALTER TABLE metadata_refresh_debt + ADD CONSTRAINT metadata_refresh_debt_content_id_fkey + FOREIGN KEY (content_id) REFERENCES media_items(content_id) ON DELETE CASCADE; + +DROP INDEX IF EXISTS idx_metadata_refresh_debt_target_due; + +ALTER TABLE metadata_refresh_debt + DROP COLUMN IF EXISTS target_type; +-- +goose StatementEnd diff --git a/migrations/sql/101_watch_history_identity.sql b/migrations/sql/101_watch_history_identity.sql new file mode 100644 index 00000000..51455917 --- /dev/null +++ b/migrations/sql/101_watch_history_identity.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.user_watch_history + ADD COLUMN watch_identity JSONB NOT NULL DEFAULT '{}'::jsonb; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.user_watch_history DROP COLUMN watch_identity; +-- +goose StatementEnd diff --git a/migrations/102_catalog_perf_indexes.up.sql b/migrations/sql/102_catalog_perf_indexes.sql similarity index 64% rename from migrations/102_catalog_perf_indexes.up.sql rename to migrations/sql/102_catalog_perf_indexes.sql index 16195fc7..250030d6 100644 --- a/migrations/102_catalog_perf_indexes.up.sql +++ b/migrations/sql/102_catalog_perf_indexes.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Person name lookups (actor/director filters + ingest dedup) CREATE INDEX IF NOT EXISTS idx_people_name_lower ON public.people USING btree (LOWER(name)); @@ -49,3 +51,31 @@ ON public.item_people USING btree (person_id, kind); DROP INDEX IF EXISTS public.idx_episodes_series; DROP INDEX IF EXISTS public.idx_seasons_series; DROP INDEX IF EXISTS public.idx_api_keys_api_key; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Recreate dropped duplicate indexes (matching original 001 schema) +CREATE INDEX IF NOT EXISTS idx_api_keys_api_key +ON public.api_keys USING btree (api_key); + +CREATE INDEX IF NOT EXISTS idx_seasons_series +ON public.seasons USING btree (series_id, season_number); + +CREATE INDEX IF NOT EXISTS idx_episodes_series +ON public.episodes USING btree (series_id, season_number, episode_number); + +-- Drop new indexes +DROP INDEX IF EXISTS public.idx_item_people_person_kind; +DROP INDEX IF EXISTS public.idx_uwp_profile_in_progress; +DROP INDEX IF EXISTS public.idx_uwp_profile_completed; +DROP INDEX IF EXISTS public.idx_media_items_keywords; +DROP INDEX IF EXISTS public.idx_media_items_tvdb_id; +DROP INDEX IF EXISTS public.idx_media_items_imdb_id; +DROP INDEX IF EXISTS public.idx_media_items_tmdb_id; +DROP INDEX IF EXISTS public.idx_media_items_release_date; +DROP INDEX IF EXISTS public.idx_media_items_sort_key; +DROP INDEX IF EXISTS public.idx_people_name_trgm; +-- idx_people_name_lower is owned by migration 007; do not drop on 102 rollback. +-- Note: pg_trgm extension is left installed; harmless if unused. +-- +goose StatementEnd diff --git a/migrations/103_media_items_last_air_date_denorm.up.sql b/migrations/sql/103_media_items_last_air_date_denorm.sql similarity index 75% rename from migrations/103_media_items_last_air_date_denorm.up.sql rename to migrations/sql/103_media_items_last_air_date_denorm.sql index cb458c16..a3c0fe0b 100644 --- a/migrations/103_media_items_last_air_date_denorm.up.sql +++ b/migrations/sql/103_media_items_last_air_date_denorm.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Denormalized "last aired episode date" on media_items. Maintained by -- the episode upsert path. Replaces the per-row correlated subquery in -- catalog/air_date_sql.go (audit 2026-05-01 §2.1, hot path #1). @@ -20,3 +22,10 @@ WHERE mi.content_id = sub.series_id CREATE INDEX IF NOT EXISTS idx_media_items_last_air_date_at ON public.media_items USING btree (last_air_date_at DESC NULLS LAST) WHERE type = 'series'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_items_last_air_date_at; +ALTER TABLE public.media_items DROP COLUMN IF EXISTS last_air_date_at; +-- +goose StatementEnd diff --git a/migrations/104_media_files_language_arrays.up.sql b/migrations/sql/104_media_files_language_arrays.sql similarity index 82% rename from migrations/104_media_files_language_arrays.up.sql rename to migrations/sql/104_media_files_language_arrays.sql index 5b113b5f..6887c9a1 100644 --- a/migrations/104_media_files_language_arrays.up.sql +++ b/migrations/sql/104_media_files_language_arrays.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- OPERATOR NOTE: Adding STORED generated columns rewrites the entire -- media_files table under an ACCESS EXCLUSIVE lock. On a 1.4M-row -- production table this can take several minutes and blocks all reads @@ -8,7 +10,7 @@ -- dominated by jsonb_array_elements over the JSONB columns. Expect -- ~1-3 minutes on a 1.4M-row table with ~3 audio tracks each. -- --- Migration is reversible via the paired .down.sql file. +-- Migration is reversible via the Goose Down section. -- Generated stored arrays of audio/subtitle language codes pulled from -- the JSONB tracks columns. Replaces per-row jsonb_array_elements unnest @@ -51,3 +53,13 @@ ON public.media_files USING gin (audio_language_codes); CREATE INDEX IF NOT EXISTS idx_media_files_subtitle_lang_gin ON public.media_files USING gin (subtitle_language_codes); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_files_subtitle_lang_gin; +DROP INDEX IF EXISTS public.idx_media_files_audio_lang_gin; +ALTER TABLE public.media_files DROP COLUMN IF EXISTS subtitle_language_codes; +ALTER TABLE public.media_files DROP COLUMN IF EXISTS audio_language_codes; +DROP FUNCTION IF EXISTS public.jsonb_track_language_codes(jsonb); +-- +goose StatementEnd diff --git a/migrations/105_media_items_title_normalized.up.sql b/migrations/sql/105_media_items_title_normalized.sql similarity index 78% rename from migrations/105_media_items_title_normalized.up.sql rename to migrations/sql/105_media_items_title_normalized.sql index 090353d9..c3515036 100644 --- a/migrations/105_media_items_title_normalized.up.sql +++ b/migrations/sql/105_media_items_title_normalized.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- OPERATOR NOTE: Adding a STORED generated column rewrites the entire -- media_items table under an ACCESS EXCLUSIVE lock. On a 219K-row -- production table this can take ~30s and blocks all reads and writes. @@ -18,3 +20,10 @@ ADD COLUMN IF NOT EXISTS title_normalized text CREATE INDEX IF NOT EXISTS idx_media_items_title_normalized_trgm ON public.media_items USING gin (title_normalized public.gin_trgm_ops); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; +ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; +-- +goose StatementEnd diff --git a/migrations/sql/107_recently_added_section_index.sql b/migrations/sql/107_recently_added_section_index.sql new file mode 100644 index 00000000..01662c8d --- /dev/null +++ b/migrations/sql/107_recently_added_section_index.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_item_libraries_folder_seen_content +ON public.media_item_libraries USING btree (media_folder_id, first_seen_at DESC, content_id ASC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_item_libraries_folder_seen_content; +-- +goose StatementEnd diff --git a/migrations/108_intro_marker_detection.up.sql b/migrations/sql/108_intro_marker_detection.sql similarity index 79% rename from migrations/108_intro_marker_detection.up.sql rename to migrations/sql/108_intro_marker_detection.sql index e2f6ef01..16f96325 100644 --- a/migrations/108_intro_marker_detection.up.sql +++ b/migrations/sql/108_intro_marker_detection.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_folders ADD COLUMN IF NOT EXISTS intro_detection_enabled boolean NOT NULL DEFAULT false; @@ -51,3 +53,20 @@ CREATE TABLE IF NOT EXISTS intro_season_analysis_state ( analyzed_at timestamp with time zone NOT NULL DEFAULT now(), PRIMARY KEY (season_id, media_folder_id, analysis_group_key, algorithm_version, config_hash) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS intro_season_analysis_state; +DROP INDEX IF EXISTS idx_media_intro_fingerprints_hash; +DROP TABLE IF EXISTS media_intro_fingerprints; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS intro_markers_detected_at, + DROP COLUMN IF EXISTS intro_markers_algorithm, + DROP COLUMN IF EXISTS intro_markers_confidence, + DROP COLUMN IF EXISTS intro_markers_source; + +ALTER TABLE media_folders + DROP COLUMN IF EXISTS intro_detection_enabled; +-- +goose StatementEnd diff --git a/migrations/109_online_marker_sources.up.sql b/migrations/sql/109_online_marker_sources.sql similarity index 53% rename from migrations/109_online_marker_sources.up.sql rename to migrations/sql/109_online_marker_sources.sql index 9ed4a133..8b3f2b8b 100644 --- a/migrations/109_online_marker_sources.up.sql +++ b/migrations/sql/109_online_marker_sources.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_files ADD COLUMN IF NOT EXISTS intro_markers_provider text, ADD COLUMN IF NOT EXISTS credits_markers_source text, @@ -11,3 +13,18 @@ ON CONFLICT (key) DO NOTHING; INSERT INTO server_settings (key, value) VALUES ('markers.lazy_playback', 'false') ON CONFLICT (key) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM server_settings +WHERE key IN ('markers.mode', 'markers.lazy_playback'); + +ALTER TABLE media_files + DROP COLUMN IF EXISTS credits_markers_detected_at, + DROP COLUMN IF EXISTS credits_markers_algorithm, + DROP COLUMN IF EXISTS credits_markers_confidence, + DROP COLUMN IF EXISTS credits_markers_provider, + DROP COLUMN IF EXISTS credits_markers_source, + DROP COLUMN IF EXISTS intro_markers_provider; +-- +goose StatementEnd diff --git a/migrations/110_watch_provider_foundation.up.sql b/migrations/sql/110_watch_provider_foundation.sql similarity index 92% rename from migrations/110_watch_provider_foundation.up.sql rename to migrations/sql/110_watch_provider_foundation.sql index bb8048f3..0c436190 100644 --- a/migrations/110_watch_provider_foundation.up.sql +++ b/migrations/sql/110_watch_provider_foundation.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.watch_provider_connections ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), provider text NOT NULL, @@ -125,3 +127,13 @@ CREATE TABLE public.watch_provider_scrobble_sessions ( CREATE INDEX idx_watch_provider_scrobble_sessions_open ON public.watch_provider_scrobble_sessions (connection_id, started_at ASC) WHERE stop_sent_at IS NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.watch_provider_scrobble_sessions; +DROP TABLE IF EXISTS public.watch_provider_history_exports; +DROP TABLE IF EXISTS public.watch_provider_sync_runs; +DROP TABLE IF EXISTS public.watch_provider_auth_sessions; +DROP TABLE IF EXISTS public.watch_provider_connections; +-- +goose StatementEnd diff --git a/migrations/sql/111_watch_provider_sync_hourly.sql b/migrations/sql/111_watch_provider_sync_hourly.sql new file mode 100644 index 00000000..8f760db5 --- /dev/null +++ b/migrations/sql/111_watch_provider_sync_hourly.sql @@ -0,0 +1,17 @@ +-- +goose Up +-- +goose StatementBegin +UPDATE public.task_triggers +SET interval = 60 * 60 * 1000 +WHERE task_key = 'sync_watch_providers' + AND type = 'interval' + AND interval = 15 * 60 * 1000; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE public.task_triggers +SET interval = 15 * 60 * 1000 +WHERE task_key = 'sync_watch_providers' + AND type = 'interval' + AND interval = 60 * 60 * 1000; +-- +goose StatementEnd diff --git a/migrations/sql/112_watch_provider_unwatched_export.sql b/migrations/sql/112_watch_provider_unwatched_export.sql new file mode 100644 index 00000000..c1650383 --- /dev/null +++ b/migrations/sql/112_watch_provider_unwatched_export.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.watch_provider_connections + ADD COLUMN export_unwatched_enabled boolean NOT NULL DEFAULT false; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.watch_provider_connections + DROP COLUMN IF EXISTS export_unwatched_enabled; +-- +goose StatementEnd diff --git a/migrations/113_watch_provider_scrobble_session_fields.up.sql b/migrations/sql/113_watch_provider_scrobble_session_fields.sql similarity index 57% rename from migrations/113_watch_provider_scrobble_session_fields.up.sql rename to migrations/sql/113_watch_provider_scrobble_session_fields.sql index e3c46277..c627b442 100644 --- a/migrations/113_watch_provider_scrobble_session_fields.up.sql +++ b/migrations/sql/113_watch_provider_scrobble_session_fields.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.watch_provider_scrobble_sessions ADD COLUMN IF NOT EXISTS provider_item_key text NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT '', @@ -11,3 +13,21 @@ ALTER TABLE public.watch_provider_scrobble_sessions ADD COLUMN IF NOT EXISTS episode_number integer NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS duration_seconds double precision NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS completed boolean NOT NULL DEFAULT false; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.watch_provider_scrobble_sessions + DROP COLUMN IF EXISTS completed, + DROP COLUMN IF EXISTS duration_seconds, + DROP COLUMN IF EXISTS episode_number, + DROP COLUMN IF EXISTS season_number, + DROP COLUMN IF EXISTS series_tvdb_id, + DROP COLUMN IF EXISTS series_tmdb_id, + DROP COLUMN IF EXISTS series_imdb_id, + DROP COLUMN IF EXISTS tvdb_id, + DROP COLUMN IF EXISTS tmdb_id, + DROP COLUMN IF EXISTS imdb_id, + DROP COLUMN IF EXISTS kind, + DROP COLUMN IF EXISTS provider_item_key; +-- +goose StatementEnd diff --git a/migrations/sql/114_watch_provider_sync_cursors.sql b/migrations/sql/114_watch_provider_sync_cursors.sql new file mode 100644 index 00000000..4e9f9b07 --- /dev/null +++ b/migrations/sql/114_watch_provider_sync_cursors.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.watch_provider_connections + ADD COLUMN sync_cursors jsonb NOT NULL DEFAULT '{}'::jsonb; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.watch_provider_connections + DROP COLUMN sync_cursors; +-- +goose StatementEnd diff --git a/migrations/115_watch_provider_favorites.up.sql b/migrations/sql/115_watch_provider_favorites.sql similarity index 73% rename from migrations/115_watch_provider_favorites.up.sql rename to migrations/sql/115_watch_provider_favorites.sql index 6c7109a5..4a964152 100644 --- a/migrations/115_watch_provider_favorites.up.sql +++ b/migrations/sql/115_watch_provider_favorites.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.watch_provider_connections ADD COLUMN import_favorites_enabled boolean NOT NULL DEFAULT false, ADD COLUMN export_favorites_enabled boolean NOT NULL DEFAULT false, @@ -42,3 +44,22 @@ CREATE INDEX idx_watch_provider_favorite_items_connection_remote CREATE INDEX idx_watch_provider_favorite_items_connection_local ON public.watch_provider_favorite_items (connection_id, local_present); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.watch_provider_favorite_items; + +ALTER TABLE public.watch_provider_sync_runs + DROP COLUMN IF EXISTS favorite_removals_sent, + DROP COLUMN IF EXISTS outbound_favorites_sent, + DROP COLUMN IF EXISTS outbound_favorites_found, + DROP COLUMN IF EXISTS inbound_favorites_imported, + DROP COLUMN IF EXISTS inbound_favorites_found; + +ALTER TABLE public.watch_provider_connections + DROP COLUMN IF EXISTS last_favorites_sync_at, + DROP COLUMN IF EXISTS sync_favorite_removals_enabled, + DROP COLUMN IF EXISTS export_favorites_enabled, + DROP COLUMN IF EXISTS import_favorites_enabled; +-- +goose StatementEnd diff --git a/migrations/116_add_trakt_library_collection_type.down.sql b/migrations/sql/116_add_trakt_library_collection_type.sql similarity index 54% rename from migrations/116_add_trakt_library_collection_type.down.sql rename to migrations/sql/116_add_trakt_library_collection_type.sql index fd1494b9..71aa4f90 100644 --- a/migrations/116_add_trakt_library_collection_type.down.sql +++ b/migrations/sql/116_add_trakt_library_collection_type.sql @@ -1,3 +1,14 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_collection_type_check; + +ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check + CHECK (collection_type = ANY (ARRAY['manual', 'smart', 'mdblist', 'tmdb', 'trakt'])); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin UPDATE library_collections SET collection_type = 'manual', source_config = '{}'::jsonb, @@ -12,3 +23,4 @@ ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_co ALTER TABLE library_collections ADD CONSTRAINT library_collections_collection_type_check CHECK (collection_type = ANY (ARRAY['manual', 'smart', 'mdblist', 'tmdb'])); +-- +goose StatementEnd diff --git a/migrations/117_library_collection_management_metadata.up.sql b/migrations/sql/117_library_collection_management_metadata.sql similarity index 60% rename from migrations/117_library_collection_management_metadata.up.sql rename to migrations/sql/117_library_collection_management_metadata.sql index 146968a1..c3dd52d4 100644 --- a/migrations/117_library_collection_management_metadata.up.sql +++ b/migrations/sql/117_library_collection_management_metadata.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE library_collections ADD COLUMN IF NOT EXISTS management_mode TEXT NOT NULL DEFAULT 'manual', ADD COLUMN IF NOT EXISTS management_source TEXT NOT NULL DEFAULT '', @@ -11,3 +13,16 @@ ALTER TABLE library_collections ADD CONSTRAINT library_collections_management_mo CREATE UNIQUE INDEX IF NOT EXISTS idx_library_collections_section_management_key ON library_collections (management_key) WHERE management_mode = 'section' AND management_key <> ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_library_collections_section_management_key; + +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; + +ALTER TABLE library_collections + DROP COLUMN IF EXISTS management_key, + DROP COLUMN IF EXISTS management_source, + DROP COLUMN IF EXISTS management_mode; +-- +goose StatementEnd diff --git a/migrations/118_user_collection_imports.up.sql b/migrations/sql/118_user_collection_imports.sql similarity index 66% rename from migrations/118_user_collection_imports.up.sql rename to migrations/sql/118_user_collection_imports.sql index 2b2aa490..89a47923 100644 --- a/migrations/118_user_collection_imports.up.sql +++ b/migrations/sql/118_user_collection_imports.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Allow personal user collections to act as TMDB/Trakt/MDBList imports. -- -- Mirrors the source/sync columns we already keep on library_collections so a @@ -19,3 +21,20 @@ ALTER TABLE user_personal_collections CREATE INDEX IF NOT EXISTS idx_user_collections_next_sync_due ON user_personal_collections (next_sync_at) WHERE sync_schedule IS NOT NULL AND next_sync_at IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_collections_next_sync_due; + +ALTER TABLE user_personal_collections + DROP COLUMN IF EXISTS item_count, + DROP COLUMN IF EXISTS last_sync_message, + DROP COLUMN IF EXISTS last_sync_status, + DROP COLUMN IF EXISTS last_sync_at, + DROP COLUMN IF EXISTS next_sync_at, + DROP COLUMN IF EXISTS sync_schedule, + DROP COLUMN IF EXISTS source_config, + DROP COLUMN IF EXISTS source_url, + DROP COLUMN IF EXISTS description; +-- +goose StatementEnd diff --git a/migrations/119_user_collection_server_visibility.up.sql b/migrations/sql/119_user_collection_server_visibility.sql similarity index 66% rename from migrations/119_user_collection_server_visibility.up.sql rename to migrations/sql/119_user_collection_server_visibility.sql index 7dfe7bc9..c10489c9 100644 --- a/migrations/119_user_collection_server_visibility.up.sql +++ b/migrations/sql/119_user_collection_server_visibility.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Allow users to opt their personal collections into the per-library -- "Collections" tab so a profile can publish a curated shelf alongside -- the admin/library collections. The default is FALSE so existing @@ -9,3 +11,12 @@ ALTER TABLE user_personal_collections CREATE INDEX IF NOT EXISTS idx_user_collections_server_visible ON user_personal_collections (include_in_server_collections) WHERE include_in_server_collections = TRUE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_collections_server_visible; + +ALTER TABLE user_personal_collections + DROP COLUMN IF EXISTS include_in_server_collections; +-- +goose StatementEnd diff --git a/migrations/120_user_collection_poster.up.sql b/migrations/sql/120_user_collection_poster.sql similarity index 60% rename from migrations/120_user_collection_poster.up.sql rename to migrations/sql/120_user_collection_poster.sql index d6e72e49..e9e9408c 100644 --- a/migrations/120_user_collection_poster.up.sql +++ b/migrations/sql/120_user_collection_poster.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Allow users to attach a custom poster to their personal collections, -- mirroring the admin/library collection artwork pipeline. Both columns -- default to '' so existing rows remain unchanged. @@ -5,3 +7,11 @@ ALTER TABLE user_personal_collections ADD COLUMN IF NOT EXISTS poster_url TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS poster_thumbhash TEXT NOT NULL DEFAULT ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE user_personal_collections + DROP COLUMN IF EXISTS poster_thumbhash, + DROP COLUMN IF EXISTS poster_url; +-- +goose StatementEnd diff --git a/migrations/121_canonicalize_language_codes.up.sql b/migrations/sql/121_canonicalize_language_codes.sql similarity index 77% rename from migrations/121_canonicalize_language_codes.up.sql rename to migrations/sql/121_canonicalize_language_codes.sql index 16cccb44..11de6ca6 100644 --- a/migrations/121_canonicalize_language_codes.up.sql +++ b/migrations/sql/121_canonicalize_language_codes.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Canonicalize language and country codes across the catalog. Different -- ingest paths historically wrote ISO 639-1 ("en"), ISO 639-2/B ("eng"), -- and case variants verbatim, accumulating duplicates of the same logical @@ -103,3 +105,46 @@ ON public.media_files USING gin (audio_language_codes); CREATE INDEX IF NOT EXISTS idx_media_files_subtitle_lang_gin ON public.media_files USING gin (subtitle_language_codes); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Rollback canonicalization. Original code values were lossy on the way +-- down (multiple raw forms collapsed to one), so the data rewrite cannot +-- be reversed. This rollback restores the previous LOWER-only function +-- and re-creates the STORED columns; downstream UI will see whatever +-- canonical values the up migration left behind. + +DROP INDEX IF EXISTS idx_media_files_audio_lang_gin; +DROP INDEX IF EXISTS idx_media_files_subtitle_lang_gin; + +ALTER TABLE public.media_files DROP COLUMN IF EXISTS audio_language_codes; +ALTER TABLE public.media_files DROP COLUMN IF EXISTS subtitle_language_codes; + +CREATE OR REPLACE FUNCTION public.jsonb_track_language_codes(tracks jsonb) +RETURNS text[] +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +AS $$ + SELECT array_agg(LOWER(elem->>'language')) + FROM jsonb_array_elements(COALESCE(tracks, '[]'::jsonb)) AS elem + WHERE (elem->>'language') IS NOT NULL AND (elem->>'language') <> '' +$$; + +ALTER TABLE public.media_files +ADD COLUMN IF NOT EXISTS audio_language_codes text[] + GENERATED ALWAYS AS (public.jsonb_track_language_codes(audio_tracks)) STORED; + +ALTER TABLE public.media_files +ADD COLUMN IF NOT EXISTS subtitle_language_codes text[] + GENERATED ALWAYS AS (public.jsonb_track_language_codes(subtitle_tracks)) STORED; + +CREATE INDEX IF NOT EXISTS idx_media_files_audio_lang_gin +ON public.media_files USING gin (audio_language_codes); + +CREATE INDEX IF NOT EXISTS idx_media_files_subtitle_lang_gin +ON public.media_files USING gin (subtitle_language_codes); + +DROP FUNCTION IF EXISTS public.canonical_language_code(text); +-- +goose StatementEnd diff --git a/migrations/122_user_personal_collections_sort_order.up.sql b/migrations/sql/122_user_personal_collections_sort_order.sql similarity index 70% rename from migrations/122_user_personal_collections_sort_order.up.sql rename to migrations/sql/122_user_personal_collections_sort_order.sql index 5ce4e000..a0abf53a 100644 --- a/migrations/122_user_personal_collections_sort_order.up.sql +++ b/migrations/sql/122_user_personal_collections_sort_order.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE user_personal_collections ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0; @@ -13,3 +15,10 @@ WHERE user_personal_collections.user_id = sub.user_id CREATE INDEX idx_user_personal_collections_sort_order ON user_personal_collections (user_id, sort_order, created_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_personal_collections_sort_order; +ALTER TABLE user_personal_collections DROP COLUMN sort_order; +-- +goose StatementEnd diff --git a/migrations/123_collection_groups.up.sql b/migrations/sql/123_collection_groups.sql similarity index 74% rename from migrations/123_collection_groups.up.sql rename to migrations/sql/123_collection_groups.sql index c6db8b98..fcba737a 100644 --- a/migrations/123_collection_groups.up.sql +++ b/migrations/sql/123_collection_groups.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Per-library admin collection groups. CREATE TABLE library_collection_groups ( library_id INTEGER NOT NULL REFERENCES media_folders(id) ON DELETE CASCADE, @@ -30,3 +32,15 @@ ALTER TABLE user_personal_collections -- prefix matches the row-ownership filter on every query path. CREATE INDEX idx_user_personal_collections_group_order ON user_personal_collections (user_id, group_label, sort_order); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_personal_collections_group_order; + +ALTER TABLE user_personal_collections DROP COLUMN IF EXISTS group_label; +ALTER TABLE library_collections DROP COLUMN IF EXISTS group_label; + +DROP TABLE IF EXISTS user_collection_groups; +DROP TABLE IF EXISTS library_collection_groups; +-- +goose StatementEnd diff --git a/migrations/124_collection_group_identity_and_membership.up.sql b/migrations/sql/124_collection_group_identity_and_membership.sql similarity index 75% rename from migrations/124_collection_group_identity_and_membership.up.sql rename to migrations/sql/124_collection_group_identity_and_membership.sql index 6ed746e8..ba6d5184 100644 --- a/migrations/124_collection_group_identity_and_membership.up.sql +++ b/migrations/sql/124_collection_group_identity_and_membership.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Upgrade collection groups from label-only buckets to opaque-id groups while -- moving library collection placement onto the per-library membership table. @@ -221,3 +223,72 @@ ALTER TABLE user_personal_collections CREATE INDEX idx_user_personal_collections_group_id_order ON user_personal_collections (user_id, group_id, sort_order); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_user_personal_collections_group_id_order; + +ALTER TABLE user_personal_collections + DROP CONSTRAINT IF EXISTS user_personal_collections_group_fkey; + +UPDATE user_personal_collections upc +SET group_label = COALESCE(g.label, '') +FROM user_collection_groups g +WHERE g.user_id = upc.user_id + AND g.id = upc.group_id; + +ALTER TABLE user_personal_collections + DROP COLUMN IF EXISTS group_id; + +ALTER TABLE user_collection_groups + DROP CONSTRAINT IF EXISTS user_collection_groups_user_slug_unique, + DROP CONSTRAINT IF EXISTS user_collection_groups_user_id_unique, + DROP CONSTRAINT IF EXISTS user_collection_groups_default_sort_mode_check, + DROP COLUMN IF EXISTS default_sort_mode, + DROP COLUMN IF EXISTS slug, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS id; + +ALTER TABLE media_folders + DROP COLUMN IF EXISTS collection_ungrouped_sort_order; + +DROP INDEX IF EXISTS idx_library_collection_libraries_group_order; + +ALTER TABLE library_collection_libraries + DROP CONSTRAINT IF EXISTS library_collection_libraries_group_library_fkey; + +UPDATE library_collections lc +SET group_label = COALESCE(g.label, '') +FROM library_collection_libraries lcl +LEFT JOIN library_collection_groups g + ON g.id = lcl.group_id + AND g.library_id = lcl.library_id +WHERE lcl.collection_id = lc.id + AND lc.library_id = lcl.library_id; + +ALTER TABLE library_collection_libraries + DROP COLUMN IF EXISTS updated_at, + DROP COLUMN IF EXISTS created_at, + DROP COLUMN IF EXISTS sort_order, + DROP COLUMN IF EXISTS group_id; + +DROP INDEX IF EXISTS idx_library_collection_groups_order; +DROP INDEX IF EXISTS idx_library_collection_groups_user_unique; + +DELETE FROM library_collection_groups +WHERE kind = 'user_collections' + OR label = 'user-collections'; + +ALTER TABLE library_collection_groups + DROP CONSTRAINT IF EXISTS library_collection_groups_library_slug_unique, + DROP CONSTRAINT IF EXISTS library_collection_groups_id_library_unique, + DROP CONSTRAINT IF EXISTS library_collection_groups_id_unique, + DROP CONSTRAINT IF EXISTS library_collection_groups_default_sort_mode_check, + DROP CONSTRAINT IF EXISTS library_collection_groups_kind_check, + DROP COLUMN IF EXISTS default_sort_mode, + DROP COLUMN IF EXISTS kind, + DROP COLUMN IF EXISTS slug, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS id; +-- +goose StatementEnd diff --git a/migrations/sql/125_collection_smart_count_cache.sql b/migrations/sql/125_collection_smart_count_cache.sql new file mode 100644 index 00000000..b0f94459 --- /dev/null +++ b/migrations/sql/125_collection_smart_count_cache.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections + ADD COLUMN item_count_cached INTEGER NOT NULL DEFAULT 0, + ADD COLUMN item_count_cached_at TIMESTAMPTZ; + +CREATE INDEX idx_library_collections_smart_count_sort + ON library_collections (collection_type, item_count_cached DESC, title); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_library_collections_smart_count_sort; + +ALTER TABLE library_collections + DROP COLUMN IF EXISTS item_count_cached_at, + DROP COLUMN IF EXISTS item_count_cached; +-- +goose StatementEnd diff --git a/migrations/126_oauth_session.up.sql b/migrations/sql/126_oauth_session.sql similarity index 80% rename from migrations/126_oauth_session.up.sql rename to migrations/sql/126_oauth_session.sql index 950d0ec2..8190c6b7 100644 --- a/migrations/126_oauth_session.up.sql +++ b/migrations/sql/126_oauth_session.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE oauth_session ( state TEXT PRIMARY KEY, install_id TEXT NOT NULL, @@ -21,3 +23,10 @@ CREATE TABLE oauth_completion ( ); CREATE INDEX oauth_completion_expires_idx ON oauth_completion (expires_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS oauth_completion; +DROP TABLE IF EXISTS oauth_session; +-- +goose StatementEnd diff --git a/migrations/127_search_synonym_and_ampersand.up.sql b/migrations/sql/127_search_synonym_and_ampersand.sql similarity index 67% rename from migrations/127_search_synonym_and_ampersand.up.sql rename to migrations/sql/127_search_synonym_and_ampersand.sql index 5aa150ae..72497139 100644 --- a/migrations/127_search_synonym_and_ampersand.up.sql +++ b/migrations/sql/127_search_synonym_and_ampersand.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- OPERATOR NOTE: This migration drops and re-adds the title_normalized -- STORED generated column on media_items, which rewrites the entire table -- under an ACCESS EXCLUSIVE lock. On a 219K-row production table this can @@ -64,3 +66,38 @@ CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(original_title, ''))), 'A') || setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(sort_title, ''))), 'B') )); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Restore the pre-127 normalization: collapse non-alnum to spaces and +-- lowercase, but DO NOT strip standalone "and" tokens. "&" and "and" will +-- once again be inequivalent in title matching. +-- +-- The legacy idx_media_items_search (english combined title+overview) is +-- intentionally NOT restored: it was unused by the post-migration-105 query +-- path, and rolling back to a known-dead index just imposes a GIN rebuild +-- cost for no benefit. + +DROP INDEX IF EXISTS public.idx_media_items_search_title_fields; +DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; + +ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; +ALTER TABLE public.media_items + ADD COLUMN title_normalized text + GENERATED ALWAYS AS ( + BTRIM(LOWER(REGEXP_REPLACE(COALESCE(title, ''), '[^[:alnum:]]+', ' ', 'g'))) + ) STORED; + +CREATE INDEX IF NOT EXISTS idx_media_items_title_normalized_trgm + ON public.media_items USING gin (title_normalized public.gin_trgm_ops); + +CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields + ON public.media_items USING gin (( + setweight(to_tsvector('simple', COALESCE(title, '')), 'A') || + setweight(to_tsvector('simple', COALESCE(original_title, '')), 'A') || + setweight(to_tsvector('simple', COALESCE(sort_title, '')), 'B') + )); + +DROP FUNCTION IF EXISTS public.normalize_search_text(text); +-- +goose StatementEnd diff --git a/migrations/128_media_items_show_status.up.sql b/migrations/sql/128_media_items_show_status.sql similarity index 66% rename from migrations/128_media_items_show_status.up.sql rename to migrations/sql/128_media_items_show_status.sql index 72bc46fd..d290876b 100644 --- a/migrations/128_media_items_show_status.up.sql +++ b/migrations/sql/128_media_items_show_status.sql @@ -1,6 +1,15 @@ +-- +goose Up +-- +goose StatementBegin -- show_status captures the series lifecycle ("returning", "ended", "cancelled", -- "in_production") sourced from metadata providers. Empty string when unknown -- or not applicable (movies). Used by the card overlay system to render a -- Show Status badge on series cards. ALTER TABLE media_items ADD COLUMN show_status TEXT NOT NULL DEFAULT ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_items + DROP COLUMN show_status; +-- +goose StatementEnd diff --git a/migrations/sql/132_downloaded_subtitles_admin_index.sql b/migrations/sql/132_downloaded_subtitles_admin_index.sql new file mode 100644 index 00000000..8a04bf1f --- /dev/null +++ b/migrations/sql/132_downloaded_subtitles_admin_index.sql @@ -0,0 +1,14 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_downloaded_subtitles_created + ON downloaded_subtitles (created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_downloaded_subtitles_provider_created + ON downloaded_subtitles (provider, created_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_downloaded_subtitles_provider_created; +DROP INDEX IF EXISTS idx_downloaded_subtitles_created; +-- +goose StatementEnd diff --git a/migrations/sql/133_library_collection_template_bundle_management.sql b/migrations/sql/133_library_collection_template_bundle_management.sql new file mode 100644 index 00000000..d60a93c0 --- /dev/null +++ b/migrations/sql/133_library_collection_template_bundle_management.sql @@ -0,0 +1,27 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; + +ALTER TABLE library_collections ADD CONSTRAINT library_collections_management_mode_check + CHECK (management_mode = ANY (ARRAY['manual', 'section', 'template_bundle'])); + +DROP INDEX IF EXISTS idx_library_collections_section_management_key; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_library_collections_managed_management_key + ON library_collections (management_key) + WHERE management_mode <> 'manual' AND management_key <> ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_library_collections_managed_management_key; + +ALTER TABLE library_collections DROP CONSTRAINT IF EXISTS library_collections_management_mode_check; + +ALTER TABLE library_collections ADD CONSTRAINT library_collections_management_mode_check + CHECK (management_mode = ANY (ARRAY['manual', 'section'])); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_library_collections_section_management_key + ON library_collections (management_key) + WHERE management_mode = 'section' AND management_key <> ''; +-- +goose StatementEnd diff --git a/migrations/sql/134_library_collection_poster_template_origin.sql b/migrations/sql/134_library_collection_poster_template_origin.sql new file mode 100644 index 00000000..f76dff7f --- /dev/null +++ b/migrations/sql/134_library_collection_poster_template_origin.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE library_collections + ADD COLUMN IF NOT EXISTS poster_from_template BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE library_collections DROP COLUMN IF EXISTS poster_from_template; +-- +goose StatementEnd diff --git a/migrations/sql/135_webhook_sync_silo_profile_column.sql b/migrations/sql/135_webhook_sync_silo_profile_column.sql new file mode 100644 index 00000000..44d74f00 --- /dev/null +++ b/migrations/sql/135_webhook_sync_silo_profile_column.sql @@ -0,0 +1,67 @@ +-- +goose Up +-- +goose StatementBegin +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'continuum_profile_id' + ) AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'silo_profile_id' + ) THEN + ALTER TABLE public.webhook_sync_actor_mappings + RENAME COLUMN continuum_profile_id TO silo_profile_id; + ELSIF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'continuum_profile_id' + ) THEN + UPDATE public.webhook_sync_actor_mappings + SET silo_profile_id = COALESCE(silo_profile_id, continuum_profile_id) + WHERE silo_profile_id IS NULL + AND continuum_profile_id IS NOT NULL; + END IF; +END $$; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'silo_profile_id' + ) AND NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'continuum_profile_id' + ) THEN + ALTER TABLE public.webhook_sync_actor_mappings + RENAME COLUMN silo_profile_id TO continuum_profile_id; + ELSIF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'webhook_sync_actor_mappings' + AND column_name = 'silo_profile_id' + ) THEN + UPDATE public.webhook_sync_actor_mappings + SET continuum_profile_id = COALESCE(continuum_profile_id, silo_profile_id) + WHERE continuum_profile_id IS NULL + AND silo_profile_id IS NOT NULL; + END IF; +END $$; +-- +goose StatementEnd diff --git a/migrations/sql/136_webhook_sync_rename_actor_to_profile.sql b/migrations/sql/136_webhook_sync_rename_actor_to_profile.sql new file mode 100644 index 00000000..d45e7055 --- /dev/null +++ b/migrations/sql/136_webhook_sync_rename_actor_to_profile.sql @@ -0,0 +1,59 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.webhook_sync_actor_mappings RENAME TO webhook_sync_profile_mappings; + +ALTER TABLE public.webhook_sync_profile_mappings + RENAME COLUMN external_actor_id TO external_user_id; +ALTER TABLE public.webhook_sync_profile_mappings + RENAME COLUMN external_actor_name TO external_user_name; + +ALTER TABLE public.webhook_sync_profile_mappings + RENAME CONSTRAINT webhook_sync_actor_mappings_connection_actor_unique + TO webhook_sync_profile_mappings_connection_user_unique; + +ALTER INDEX public.idx_webhook_sync_actor_mappings_connection + RENAME TO idx_webhook_sync_profile_mappings_connection; +ALTER INDEX public.idx_webhook_sync_actor_mappings_profile + RENAME TO idx_webhook_sync_profile_mappings_profile; + +ALTER TABLE public.webhook_sync_item_state + RENAME COLUMN external_actor_id TO external_user_id; + +UPDATE public.webhook_sync_event_logs +SET attrs = (attrs - 'actor_id' - 'actor_name') + || jsonb_strip_nulls(jsonb_build_object( + 'external_user_id', attrs->'actor_id', + 'external_user_name', attrs->'actor_name' + )) +WHERE attrs ? 'actor_id' OR attrs ? 'actor_name'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +UPDATE public.webhook_sync_event_logs +SET attrs = (attrs - 'external_user_id' - 'external_user_name') + || jsonb_strip_nulls(jsonb_build_object( + 'actor_id', attrs->'external_user_id', + 'actor_name', attrs->'external_user_name' + )) +WHERE attrs ? 'external_user_id' OR attrs ? 'external_user_name'; + +ALTER TABLE public.webhook_sync_item_state + RENAME COLUMN external_user_id TO external_actor_id; + +ALTER INDEX public.idx_webhook_sync_profile_mappings_profile + RENAME TO idx_webhook_sync_actor_mappings_profile; +ALTER INDEX public.idx_webhook_sync_profile_mappings_connection + RENAME TO idx_webhook_sync_actor_mappings_connection; + +ALTER TABLE public.webhook_sync_profile_mappings + RENAME CONSTRAINT webhook_sync_profile_mappings_connection_user_unique + TO webhook_sync_actor_mappings_connection_actor_unique; + +ALTER TABLE public.webhook_sync_profile_mappings + RENAME COLUMN external_user_name TO external_actor_name; +ALTER TABLE public.webhook_sync_profile_mappings + RENAME COLUMN external_user_id TO external_actor_id; + +ALTER TABLE public.webhook_sync_profile_mappings RENAME TO webhook_sync_actor_mappings; +-- +goose StatementEnd diff --git a/migrations/137_recap_preview_markers.up.sql b/migrations/sql/137_recap_preview_markers.sql similarity index 54% rename from migrations/137_recap_preview_markers.up.sql rename to migrations/sql/137_recap_preview_markers.sql index 0da65186..c4253686 100644 --- a/migrations/137_recap_preview_markers.up.sql +++ b/migrations/sql/137_recap_preview_markers.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_files ADD COLUMN IF NOT EXISTS recap_start double precision, ADD COLUMN IF NOT EXISTS recap_end double precision, @@ -20,3 +22,29 @@ ALTER TABLE user_profiles INSERT INTO server_settings (key, value) VALUES ('introdb.api_key', '') ON CONFLICT (key) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM server_settings WHERE key = 'introdb.api_key'; + +ALTER TABLE user_profiles + DROP COLUMN IF EXISTS auto_play_next_preview, + DROP COLUMN IF EXISTS auto_skip_recap; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS preview_markers_detected_at, + DROP COLUMN IF EXISTS preview_markers_algorithm, + DROP COLUMN IF EXISTS preview_markers_confidence, + DROP COLUMN IF EXISTS preview_markers_provider, + DROP COLUMN IF EXISTS preview_markers_source, + DROP COLUMN IF EXISTS preview_end, + DROP COLUMN IF EXISTS preview_start, + DROP COLUMN IF EXISTS recap_markers_detected_at, + DROP COLUMN IF EXISTS recap_markers_algorithm, + DROP COLUMN IF EXISTS recap_markers_confidence, + DROP COLUMN IF EXISTS recap_markers_provider, + DROP COLUMN IF EXISTS recap_markers_source, + DROP COLUMN IF EXISTS recap_end, + DROP COLUMN IF EXISTS recap_start; +-- +goose StatementEnd diff --git a/migrations/138_search_number_word_normalization.up.sql b/migrations/sql/138_search_number_word_normalization.sql similarity index 71% rename from migrations/138_search_number_word_normalization.up.sql rename to migrations/sql/138_search_number_word_normalization.sql index 902170a6..1362b45e 100644 --- a/migrations/138_search_number_word_normalization.up.sql +++ b/migrations/sql/138_search_number_word_normalization.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- OPERATOR NOTE: This migration drops and re-adds the title_normalized -- STORED generated column on media_items, which rewrites the entire table -- under an ACCESS EXCLUSIVE lock. It also rebuilds the title search GIN @@ -114,3 +116,46 @@ CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(original_title, ''))), 'A') || setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(sort_title, ''))), 'B') )); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Restore the pre-138 normalization: collapse non-alnum to spaces, lowercase, +-- and strip standalone "and" tokens, but do not normalize number words or +-- ordinal digit suffixes. + +DROP INDEX IF EXISTS public.idx_media_items_search_title_fields; +DROP INDEX IF EXISTS public.idx_media_items_title_normalized_trgm; + +ALTER TABLE public.media_items DROP COLUMN IF EXISTS title_normalized; + +CREATE OR REPLACE FUNCTION public.normalize_search_text(input text) +RETURNS text +LANGUAGE sql +IMMUTABLE +PARALLEL SAFE +AS $$ + SELECT BTRIM(REGEXP_REPLACE( + ' ' || BTRIM(LOWER(REGEXP_REPLACE(COALESCE(input, ''), '[^[:alnum:]]+', ' ', 'g'))) || ' ', + ' (and )+', + ' ', + 'g' + )); +$$; + +ALTER TABLE public.media_items + ADD COLUMN title_normalized text + GENERATED ALWAYS AS (public.normalize_search_text(title)) STORED; + +CREATE INDEX IF NOT EXISTS idx_media_items_title_normalized_trgm + ON public.media_items USING gin (title_normalized public.gin_trgm_ops); + +CREATE INDEX IF NOT EXISTS idx_media_items_search_title_fields + ON public.media_items USING gin (( + setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(title, ''))), 'A') || + setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(original_title, ''))), 'A') || + setweight(to_tsvector('simple', public.normalize_search_text(COALESCE(sort_title, ''))), 'B') + )); + +DROP FUNCTION IF EXISTS public.normalize_search_number_token(text); +-- +goose StatementEnd diff --git a/migrations/139_media_requests.up.sql b/migrations/sql/139_media_requests.sql similarity index 88% rename from migrations/139_media_requests.up.sql rename to migrations/sql/139_media_requests.sql index fa3a130b..430f7618 100644 --- a/migrations/139_media_requests.up.sql +++ b/migrations/sql/139_media_requests.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS public.request_settings ( id boolean PRIMARY KEY DEFAULT true, requests_enabled boolean NOT NULL DEFAULT false, @@ -112,3 +114,18 @@ CREATE TABLE IF NOT EXISTS public.media_request_events ( CREATE INDEX IF NOT EXISTS idx_media_request_events_request_created ON public.media_request_events (request_id, created_at DESC); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.media_request_events; +DROP INDEX IF EXISTS public.idx_media_requests_outcome_created; +DROP INDEX IF EXISTS public.idx_media_requests_status_created; +DROP INDEX IF EXISTS public.idx_media_requests_profile_created; +DROP INDEX IF EXISTS public.idx_media_requests_user_created; +DROP INDEX IF EXISTS public.idx_media_requests_active_tmdb; +DROP TABLE IF EXISTS public.media_requests; +DROP TABLE IF EXISTS public.request_integrations; +DROP TABLE IF EXISTS public.request_user_limits; +DROP TABLE IF EXISTS public.request_settings; +-- +goose StatementEnd diff --git a/migrations/sql/140_user_permissions.sql b/migrations/sql/140_user_permissions.sql new file mode 100644 index 00000000..823c4029 --- /dev/null +++ b/migrations/sql/140_user_permissions.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.users + ADD COLUMN IF NOT EXISTS permissions text[] NOT NULL DEFAULT '{}'::text[]; + +UPDATE public.users +SET permissions = '{}'::text[] +WHERE permissions IS NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.users + DROP COLUMN IF EXISTS permissions; +-- +goose StatementEnd diff --git a/migrations/141_episode_title_sort_index.up.sql b/migrations/sql/141_episode_title_sort_index.sql similarity index 65% rename from migrations/141_episode_title_sort_index.up.sql rename to migrations/sql/141_episode_title_sort_index.sql index f249b2f4..0f5c6fb5 100644 --- a/migrations/141_episode_title_sort_index.up.sql +++ b/migrations/sql/141_episode_title_sort_index.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Episode library title browse. Matches episodeCatalogSelectBody's sort_key -- expression so PostgreSQL can satisfy ORDER BY title from the episodes index. CREATE INDEX IF NOT EXISTS idx_episodes_sort_key_content @@ -5,3 +7,9 @@ ON public.episodes USING btree ( LOWER(COALESCE(NULLIF(BTRIM(title), ''), 'Episode ' || episode_number::text)), content_id ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_episodes_sort_key_content; +-- +goose StatementEnd diff --git a/migrations/142_episode_catalog_entries.up.sql b/migrations/sql/142_episode_catalog_entries.sql similarity index 90% rename from migrations/142_episode_catalog_entries.up.sql rename to migrations/sql/142_episode_catalog_entries.sql index 0020addb..d279fc5f 100644 --- a/migrations/142_episode_catalog_entries.up.sql +++ b/migrations/sql/142_episode_catalog_entries.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS public.episode_catalog_entries ( media_folder_id integer NOT NULL, episode_id text NOT NULL, @@ -672,3 +674,50 @@ FROM ( SELECT public.refresh_episode_catalog_entry(episode_id, media_folder_id) FROM changed_entries ) refreshed; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_series ON public.media_items; +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_episodes ON public.episodes; +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files_update ON public.media_files; +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files_insert_delete ON public.media_files; +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_media_files ON public.media_files; +DROP TRIGGER IF EXISTS trg_episode_catalog_entries_episode_libraries ON public.episode_libraries; + +DROP FUNCTION IF EXISTS public.episode_catalog_entries_series_trigger(); +DROP FUNCTION IF EXISTS public.episode_catalog_entries_episodes_trigger(); +DROP FUNCTION IF EXISTS public.episode_catalog_entries_media_files_trigger(); +DROP FUNCTION IF EXISTS public.episode_catalog_entries_episode_libraries_trigger(); +DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entries_for_series(text); +DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entries_for_episode(text); +DROP FUNCTION IF EXISTS public.refresh_episode_catalog_entry(text, integer); +DROP FUNCTION IF EXISTS public.episode_catalog_resolution_rank(text); +DROP FUNCTION IF EXISTS public.episode_catalog_normalized_resolution(text); +DROP FUNCTION IF EXISTS public.episode_catalog_rating_rank(text); + +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_subtitle_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_audio_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_resolution_codes_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_countries_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_networks_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_studios_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_genres_gin; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_non_dolby_vision; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_dolby_vision; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_non_hdr; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_hdr; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_status; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_bitrate; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_resolution; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_tmdb; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_imdb; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_runtime; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_content_rating; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_year; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_air_date; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_added; +DROP INDEX IF EXISTS public.idx_episode_catalog_entries_title; + +DROP TABLE IF EXISTS public.episode_catalog_entries; +-- +goose StatementEnd diff --git a/migrations/sql/162_media_items_air_timezone.sql b/migrations/sql/162_media_items_air_timezone.sql new file mode 100644 index 00000000..8d92241d --- /dev/null +++ b/migrations/sql/162_media_items_air_timezone.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS air_timezone text; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE media_items DROP COLUMN IF EXISTS air_timezone; +-- +goose StatementEnd diff --git a/migrations/165_case_insensitive_username_email.up.sql b/migrations/sql/165_case_insensitive_username_email.sql similarity index 53% rename from migrations/165_case_insensitive_username_email.up.sql rename to migrations/sql/165_case_insensitive_username_email.sql index c4320758..fe4ec23f 100644 --- a/migrations/165_case_insensitive_username_email.up.sql +++ b/migrations/sql/165_case_insensitive_username_email.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Make user login identifiers case-insensitive. The citext column type compares -- case-insensitively, so the existing users_username_key / users_email_key unique -- indexes are rebuilt as case-insensitive and WHERE username = $1 / email = $1 @@ -7,3 +9,14 @@ CREATE EXTENSION IF NOT EXISTS citext; ALTER TABLE public.users ALTER COLUMN username TYPE citext USING username::citext, ALTER COLUMN email TYPE citext USING email::citext; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Revert username/email to case-sensitive text. The citext extension is left +-- installed: dropping it is unnecessary and would fail if any other object +-- ever depends on it. +ALTER TABLE public.users + ALTER COLUMN username TYPE text USING username::text, + ALTER COLUMN email TYPE text USING email::text; +-- +goose StatementEnd diff --git a/migrations/167_trending_discover_snapshots.up.sql b/migrations/sql/167_trending_discover_snapshots.sql similarity index 85% rename from migrations/167_trending_discover_snapshots.up.sql rename to migrations/sql/167_trending_discover_snapshots.sql index 706d5c83..00d2bce0 100644 --- a/migrations/167_trending_discover_snapshots.up.sql +++ b/migrations/sql/167_trending_discover_snapshots.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Persisted snapshot of external global trending (TMDB / Trakt) for the -- trending_discover home section. One row per canonical (source, time_window): -- content_ids are already resolved to library catalog content IDs and ordered @@ -14,3 +16,9 @@ CREATE TABLE public.trending_discover_snapshots ( last_error text NOT NULL DEFAULT '', PRIMARY KEY (source, time_window) ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.trending_discover_snapshots; +-- +goose StatementEnd diff --git a/migrations/168_subtitle_ai_jobs.up.sql b/migrations/sql/168_subtitle_ai_jobs.sql similarity index 93% rename from migrations/168_subtitle_ai_jobs.up.sql rename to migrations/sql/168_subtitle_ai_jobs.sql index d5aec09c..c1a650d7 100644 --- a/migrations/168_subtitle_ai_jobs.up.sql +++ b/migrations/sql/168_subtitle_ai_jobs.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- AI subtitle jobs: on-demand translation (and, later, Whisper ASR generation) -- of subtitle tracks. Each row is one user-triggered job. The worker runs the -- configured OpenAI-compatible engine and stores the result as an ordinary @@ -37,3 +39,9 @@ CREATE INDEX subtitle_ai_jobs_media_file_idx -- Startup recovery scan for jobs left running by a crashed process. CREATE INDEX subtitle_ai_jobs_status_idx ON public.subtitle_ai_jobs (status) WHERE status IN ('pending', 'running'); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.subtitle_ai_jobs; +-- +goose StatementEnd diff --git a/migrations/169_request_multi_instance.up.sql b/migrations/sql/169_request_multi_instance.sql similarity index 66% rename from migrations/169_request_multi_instance.up.sql rename to migrations/sql/169_request_multi_instance.sql index e2658922..96d915e5 100644 --- a/migrations/169_request_multi_instance.up.sql +++ b/migrations/sql/169_request_multi_instance.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- request_integrations: one-row-per-kind -> many instances keyed by id. ALTER TABLE public.request_integrations ADD COLUMN IF NOT EXISTS id text, @@ -84,3 +86,50 @@ ALTER TABLE public.media_requests DROP COLUMN IF EXISTS integration_kind, DROP COLUMN IF EXISTS external_id, DROP COLUMN IF EXISTS external_status; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +-- Restore per-fulfillment columns on media_requests. +ALTER TABLE public.media_requests + ADD COLUMN IF NOT EXISTS integration_kind text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS external_id text NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS external_status text NOT NULL DEFAULT ''; + +-- Copy back the 1080p target's fulfillment fields (lossy: 4K/anime targets dropped). +UPDATE public.media_requests mr +SET integration_kind = t.integration_kind, + external_id = t.external_id, + external_status = t.external_status +FROM public.media_request_targets t +WHERE t.request_id = mr.id AND t.quality = '1080p'; + +ALTER TABLE public.media_requests DROP COLUMN IF EXISTS is_anime; + +DROP TABLE IF EXISTS public.media_request_targets; + +-- Collapse request_integrations back to kind-PK (lossy: keep one default per kind). +DELETE FROM public.request_integrations a +USING public.request_integrations b +WHERE a.kind = b.kind AND a.id <> b.id AND b.is_default AND NOT a.is_default; +-- If a kind has no default, keep an arbitrary row and drop the rest. +DELETE FROM public.request_integrations a +USING public.request_integrations b +WHERE a.kind = b.kind AND a.ctid < b.ctid; + +DROP INDEX IF EXISTS idx_request_integrations_default_per_kind; +DROP INDEX IF EXISTS idx_request_integrations_default4k_per_kind; + +ALTER TABLE public.request_integrations DROP CONSTRAINT request_integrations_pkey; +ALTER TABLE public.request_integrations ADD PRIMARY KEY (kind); +ALTER TABLE public.request_integrations + DROP COLUMN IF EXISTS id, + DROP COLUMN IF EXISTS name, + DROP COLUMN IF EXISTS is_4k, + DROP COLUMN IF EXISTS is_default, + DROP COLUMN IF EXISTS is_default_4k, + DROP COLUMN IF EXISTS anime_enabled, + DROP COLUMN IF EXISTS anime_quality_profile_id, + DROP COLUMN IF EXISTS anime_root_folder, + DROP COLUMN IF EXISTS anime_tags; +-- +goose StatementEnd diff --git a/migrations/sql/170_request_force_dual_quality.sql b/migrations/sql/170_request_force_dual_quality.sql new file mode 100644 index 00000000..7c0a436c --- /dev/null +++ b/migrations/sql/170_request_force_dual_quality.sql @@ -0,0 +1,10 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.request_settings + ADD COLUMN IF NOT EXISTS force_dual_quality boolean NOT NULL DEFAULT false; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.request_settings DROP COLUMN IF EXISTS force_dual_quality; +-- +goose StatementEnd diff --git a/migrations/171_autoscan.up.sql b/migrations/sql/171_autoscan.sql similarity index 92% rename from migrations/171_autoscan.up.sql rename to migrations/sql/171_autoscan.sql index 44b6ea8a..29f3e6c4 100644 --- a/migrations/171_autoscan.up.sql +++ b/migrations/sql/171_autoscan.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Autoscan: connections-based scan-source model. The host pulls changed paths -- from installed scan_source.v1 plugins (Sonarr/Radarr, etc.) on a timer, -- applies per-source path rewrites, and enqueues targeted library rescans. @@ -60,3 +62,11 @@ CREATE TABLE public.autoscan_sources ( -- capability backs MANY sources, each bound to a different connection (e.g. 4 -- arr servers from one plugin install). Operators create sources explicitly. ); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.autoscan_sources; +DROP TABLE IF EXISTS public.autoscan_connections; +DROP TABLE IF EXISTS public.autoscan_settings; +-- +goose StatementEnd diff --git a/migrations/172_autoscan_source_config.up.sql b/migrations/sql/172_autoscan_source_config.sql similarity index 84% rename from migrations/172_autoscan_source_config.up.sql rename to migrations/sql/172_autoscan_source_config.sql index bca30926..d95c70fb 100644 --- a/migrations/172_autoscan_source_config.up.sql +++ b/migrations/sql/172_autoscan_source_config.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.autoscan_sources ADD COLUMN source_config jsonb NOT NULL DEFAULT '{}'::jsonb; @@ -21,3 +23,10 @@ WHERE s.installation_id = pi.id AND pi.plugin_id = 'silo.autoscan.cephfs' AND s.capability_id = 'cephfs' AND s.source_config = '{}'::jsonb; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.autoscan_sources + DROP COLUMN IF EXISTS source_config; +-- +goose StatementEnd diff --git a/migrations/173_autoscan_events.up.sql b/migrations/sql/173_autoscan_events.sql similarity index 75% rename from migrations/173_autoscan_events.up.sql rename to migrations/sql/173_autoscan_events.sql index d8caa720..dfb09c41 100644 --- a/migrations/173_autoscan_events.up.sql +++ b/migrations/sql/173_autoscan_events.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE public.autoscan_events ( id BIGSERIAL PRIMARY KEY, source_id uuid REFERENCES public.autoscan_sources(id) ON DELETE SET NULL, @@ -31,3 +33,16 @@ ALTER TABLE public.scan_runs CREATE INDEX idx_scan_runs_autoscan_event ON public.scan_runs (autoscan_event_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_scan_runs_autoscan_event; + +ALTER TABLE public.scan_runs + DROP COLUMN IF EXISTS autoscan_event_id; + +DROP INDEX IF EXISTS public.idx_autoscan_events_completed; +DROP INDEX IF EXISTS public.idx_autoscan_events_source_completed; +DROP TABLE IF EXISTS public.autoscan_events; +-- +goose StatementEnd diff --git a/migrations/174_autoscan_source_label.up.sql b/migrations/sql/174_autoscan_source_label.sql similarity index 57% rename from migrations/174_autoscan_source_label.up.sql rename to migrations/sql/174_autoscan_source_label.sql index 91ffdfb8..ba7f95ac 100644 --- a/migrations/174_autoscan_source_label.up.sql +++ b/migrations/sql/174_autoscan_source_label.sql @@ -1,3 +1,11 @@ +-- +goose Up +-- +goose StatementBegin -- Operator-editable display label for an autoscan source. Empty string = unset; -- the admin UI falls back to connection name / plugin display_name / capability. ALTER TABLE public.autoscan_sources ADD COLUMN label text NOT NULL DEFAULT ''; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.autoscan_sources DROP COLUMN label; +-- +goose StatementEnd diff --git a/migrations/sql/175_movie_release_date_desc_index.sql b/migrations/sql/175_movie_release_date_desc_index.sql new file mode 100644 index 00000000..bed7272c --- /dev/null +++ b/migrations/sql/175_movie_release_date_desc_index.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +CREATE INDEX IF NOT EXISTS idx_media_items_movie_release_date_desc_content +ON public.media_items USING btree (release_date DESC NULLS LAST, content_id ASC) +WHERE type = 'movie'; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS public.idx_media_items_movie_release_date_desc_content; +-- +goose StatementEnd diff --git a/migrations/sql/176_playback_session_position.sql b/migrations/sql/176_playback_session_position.sql new file mode 100644 index 00000000..716e326c --- /dev/null +++ b/migrations/sql/176_playback_session_position.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync + ADD COLUMN IF NOT EXISTS position_seconds DOUBLE PRECISION NOT NULL DEFAULT 0; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync + DROP COLUMN IF EXISTS position_seconds; +-- +goose StatementEnd diff --git a/migrations/sql/177_playback_session_transcode_hw_accel.sql b/migrations/sql/177_playback_session_transcode_hw_accel.sql new file mode 100644 index 00000000..63afaf8a --- /dev/null +++ b/migrations/sql/177_playback_session_transcode_hw_accel.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync + ADD COLUMN IF NOT EXISTS transcode_hw_accel text; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE playback_sessions_sync + DROP COLUMN IF EXISTS transcode_hw_accel; +-- +goose StatementEnd diff --git a/migrations/178_admin_job_cancelled_status.down.sql b/migrations/sql/178_admin_job_cancelled_status.sql similarity index 57% rename from migrations/178_admin_job_cancelled_status.down.sql rename to migrations/sql/178_admin_job_cancelled_status.sql index a63b7b89..c9e5597d 100644 --- a/migrations/178_admin_job_cancelled_status.down.sql +++ b/migrations/sql/178_admin_job_cancelled_status.sql @@ -1,3 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.admin_jobs + DROP CONSTRAINT IF EXISTS admin_jobs_status_check; + +ALTER TABLE public.admin_jobs + ADD CONSTRAINT admin_jobs_status_check + CHECK ((status = ANY (ARRAY[ + 'queued'::text, + 'running'::text, + 'completed'::text, + 'failed'::text, + 'cancelled'::text + ]))); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin UPDATE public.admin_jobs SET status = 'failed', message = CASE @@ -21,3 +39,4 @@ ALTER TABLE public.admin_jobs 'completed'::text, 'failed'::text ]))); +-- +goose StatementEnd diff --git a/migrations/179_media_file_match_suppression.up.sql b/migrations/sql/179_media_file_match_suppression.sql similarity index 57% rename from migrations/179_media_file_match_suppression.up.sql rename to migrations/sql/179_media_file_match_suppression.sql index e4798c2c..0691bd53 100644 --- a/migrations/179_media_file_match_suppression.up.sql +++ b/migrations/sql/179_media_file_match_suppression.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE media_files ADD COLUMN IF NOT EXISTS match_suppressed_at timestamp with time zone; @@ -5,3 +7,12 @@ CREATE INDEX IF NOT EXISTS idx_media_files_unmatched_match_ready ON media_files (media_folder_id, match_suppressed_at, match_attempted_at ASC NULLS FIRST, id ASC) WHERE (content_id IS NULL OR content_id = '') AND missing_since IS NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP INDEX IF EXISTS idx_media_files_unmatched_match_ready; + +ALTER TABLE media_files + DROP COLUMN IF EXISTS match_suppressed_at; +-- +goose StatementEnd diff --git a/migrations/180_user_devices.up.sql b/migrations/sql/180_user_devices.sql similarity index 93% rename from migrations/180_user_devices.up.sql rename to migrations/sql/180_user_devices.sql index dc696ee3..3fcd8936 100644 --- a/migrations/180_user_devices.up.sql +++ b/migrations/sql/180_user_devices.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin CREATE TABLE IF NOT EXISTS public.user_devices ( user_id integer NOT NULL, profile_id text NOT NULL, @@ -60,3 +62,9 @@ ON CONFLICT (user_id, profile_id, device_id) DO UPDATE SET ELSE user_devices.device_platform END, last_seen_at = GREATEST(user_devices.last_seen_at, excluded.last_seen_at); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.user_devices; +-- +goose StatementEnd diff --git a/migrations/181_marker_provider_config.up.sql b/migrations/sql/181_marker_provider_config.sql similarity index 83% rename from migrations/181_marker_provider_config.up.sql rename to migrations/sql/181_marker_provider_config.sql index 4d18e1b8..55079a93 100644 --- a/migrations/181_marker_provider_config.up.sql +++ b/migrations/sql/181_marker_provider_config.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Per-provider marker behavior: which providers are queried for reads (Layer B -- multi-source dispatch) and which may receive contributions (off by default). -- Credentials stay in server_settings; only behavior lives here. @@ -15,3 +17,9 @@ CREATE TABLE public.marker_provider_config ( INSERT INTO public.marker_provider_config (provider, fetch_enabled) VALUES ('introdb', true) ON CONFLICT (provider) DO NOTHING; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.marker_provider_config; +-- +goose StatementEnd diff --git a/migrations/182_marker_contributions.up.sql b/migrations/sql/182_marker_contributions.sql similarity index 89% rename from migrations/182_marker_contributions.up.sql rename to migrations/sql/182_marker_contributions.sql index 7285e340..d48391fc 100644 --- a/migrations/182_marker_contributions.up.sql +++ b/migrations/sql/182_marker_contributions.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin -- Audit log of marker contributions to online providers. The content_hash over -- the contributed value and resolved provider target gives idempotency: the same -- segment value is never resubmitted to the same provider+target, but a @@ -22,3 +24,9 @@ CREATE TABLE public.marker_contributions ( ); CREATE INDEX marker_contributions_file_idx ON public.marker_contributions (media_file_id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.marker_contributions; +-- +goose StatementEnd diff --git a/migrations/183_marker_edit_audit.up.sql b/migrations/sql/183_marker_edit_audit.sql similarity index 81% rename from migrations/183_marker_edit_audit.up.sql rename to migrations/sql/183_marker_edit_audit.sql index 135e6186..8910f354 100644 --- a/migrations/183_marker_edit_audit.up.sql +++ b/migrations/sql/183_marker_edit_audit.sql @@ -1,3 +1,5 @@ +-- +goose Up +-- +goose StatementBegin ALTER TABLE public.users ALTER COLUMN permissions SET DEFAULT ARRAY['marker_edit']::text[]; @@ -32,3 +34,16 @@ CREATE INDEX marker_edit_audit_file_created_idx CREATE INDEX marker_edit_audit_user_created_idx ON public.marker_edit_audit (user_id, created_at DESC, id DESC) WHERE user_id IS NOT NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE IF EXISTS public.marker_edit_audit; + +UPDATE public.users +SET permissions = array_remove(permissions, 'marker_edit') +WHERE role <> 'admin'; + +ALTER TABLE public.users + ALTER COLUMN permissions SET DEFAULT '{}'::text[]; +-- +goose StatementEnd diff --git a/migrations/template_bundle_management_test.go b/migrations/template_bundle_management_test.go index 063f67d1..1aba1415 100644 --- a/migrations/template_bundle_management_test.go +++ b/migrations/template_bundle_management_test.go @@ -13,16 +13,17 @@ func normalizeSQL(s string) string { } func TestTemplateBundleManagementMigrationContract(t *testing.T) { - upBytes, err := FS.ReadFile("133_library_collection_template_bundle_management.up.sql") + migrationBytes, err := FS.ReadFile("sql/133_library_collection_template_bundle_management.sql") if err != nil { - t.Fatalf("read up migration: %v", err) + t.Fatalf("read migration: %v", err) } - downBytes, err := FS.ReadFile("133_library_collection_template_bundle_management.down.sql") - if err != nil { - t.Fatalf("read down migration: %v", err) + migration := string(migrationBytes) + parts := strings.Split(migration, "-- +goose Down") + if len(parts) != 2 { + t.Fatalf("expected one goose down section") } - up := normalizeSQL(string(upBytes)) - down := normalizeSQL(string(downBytes)) + up := normalizeSQL(parts[0]) + down := normalizeSQL(parts[1]) for _, want := range []string{ "'template_bundle'",