Packages
phoenix_kit
1.7.150
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/migrations/postgres/v113.ex
defmodule PhoenixKit.Migrations.Postgres.V113 do
@moduledoc """
V113: System-managed media flag + source-parent link for Tessera tiles,
plus the comments ↔ files attachment junction table.
Adds two columns to `phoenix_kit_files`:
* `system_managed :: boolean, default false, not null` — when true, the
File represents internally-generated media (DZI tile pyramids and
their per-tile chunks). System-managed files are excluded from the
user-facing MediaBrowser listings and skip the variant generation
pipeline (no small / medium / large produced — they only get an
`"original"` FileInstance, since they don't need quality variants).
* `parent_file_uuid :: uuid, nullable` — FK to `phoenix_kit_files.uuid`
for system-managed children, pointing at the source file the chunk
was derived from. Lets us cascade-clean tiles when the source image
is deleted, and lets us list all tiles for a given source for
auditing / re-generation.
Index on `(parent_file_uuid)` for the cascade-delete + per-source queries.
Partial index on `system_managed = true` keeps the MediaBrowser's
"WHERE NOT system_managed" filter cheap as the tile catalog grows.
Also creates `phoenix_kit_comment_media` — a junction table that lets the
comments module attach core File rows to individual comments with a
caller-supplied ordering and optional caption:
* `comment_uuid` FK → `phoenix_kit_comments(uuid)` `ON DELETE :delete_all`
(deleting the comment removes its attachment rows).
* `file_uuid` FK → `phoenix_kit_files(uuid)` `ON DELETE :restrict`
(a file can't be hard-deleted while it's still attached to a comment;
the comments module manages the unlink lifecycle).
* `position` integer — caller-managed ordering inside a comment.
* `caption` text, nullable.
Unique index on `(comment_uuid, position)` so each slot in a comment is
occupied at most once. Per-file index on `file_uuid` for reverse lookup
("which comments reference this file?").
## Concurrent-generation safety
Two additional safeguards keep concurrent lazy-generation requests from
producing duplicate `phoenix_kit_files` rows or violating the
"user_uuid OR parent_file_uuid" invariant:
* `phoenix_kit_files_system_dedup_index` — partial unique index on
`(parent_file_uuid, file_name)` where `system_managed = true`. Lets
`Storage.store_system_file/3` use `ON CONFLICT DO NOTHING` so a
racing second writer for the same tile silently returns the
existing row.
* `phoenix_kit_files_user_or_parent_check` — DB-level CHECK
constraint enforcing `user_uuid IS NOT NULL OR parent_file_uuid
IS NOT NULL`. The schema's `validate_system_managed_invariants`
is the user-facing check; this constraint is the safety net for
raw inserts, `Repo.insert_all`, or external tools.
All column / FK / NOT-NULL changes use raw SQL with explicit
`IF NOT EXISTS` / `DO $$ … END $$` guards so re-running on a
partially-applied schema is a no-op.
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
# All column / FK / NOT-NULL changes use raw SQL with explicit IF NOT
# EXISTS / DO-block guards so re-running the migration on a partially-
# applied schema is a no-op. Ecto's `add_if_not_exists` only protects
# the column itself, not the FK constraint or the NOT NULL drop —
# those would crash on a second run.
execute(
"ALTER TABLE #{p}phoenix_kit_files ADD COLUMN IF NOT EXISTS system_managed BOOLEAN NOT NULL DEFAULT false"
)
execute("ALTER TABLE #{p}phoenix_kit_files ADD COLUMN IF NOT EXISTS parent_file_uuid UUID")
execute("""
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'phoenix_kit_files_parent_file_uuid_fkey'
) THEN
ALTER TABLE #{p}phoenix_kit_files
ADD CONSTRAINT phoenix_kit_files_parent_file_uuid_fkey
FOREIGN KEY (parent_file_uuid)
REFERENCES #{p}phoenix_kit_files(uuid)
ON DELETE CASCADE;
END IF;
END $$
""")
# System-managed rows (Tessera tile chunks) don't have a user owner —
# they belong to a parent File via `parent_file_uuid`. Drop NOT NULL
# on `user_uuid` so the constraint isn't violated; the changeset's
# `validate_system_managed_invariants` enforces "user_uuid OR
# parent_file_uuid is set" at the application level. `DROP NOT NULL`
# is itself idempotent on a column that's already nullable.
execute("ALTER TABLE #{p}phoenix_kit_files ALTER COLUMN user_uuid DROP NOT NULL")
# Per-source tile lookup + cascade-cleanup index. Only meaningful for
# tile rows; partial on NOT NULL keeps the index tight.
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_files_parent_uuid_index
ON #{p}phoenix_kit_files (parent_file_uuid)
WHERE parent_file_uuid IS NOT NULL
""")
# Cheap "show me only user files" filter for MediaBrowser.
execute("""
CREATE INDEX IF NOT EXISTS phoenix_kit_files_system_managed_index
ON #{p}phoenix_kit_files (inserted_at DESC)
WHERE system_managed = false
""")
# Idempotent dedup on (parent_file_uuid, file_name) for system-managed
# rows. Tiles + manifests are keyed by parent_file_uuid + on-bucket
# path; concurrent lazy-generation requests for the same uncached tile
# used to be able to produce duplicate File rows (the Manager.file_exists?
# check raced with the bucket write). This partial unique index turns
# that into a DB-level no-op via `ON CONFLICT DO NOTHING` in
# Storage.store_system_file/3.
execute("""
CREATE UNIQUE INDEX IF NOT EXISTS phoenix_kit_files_system_dedup_index
ON #{p}phoenix_kit_files (parent_file_uuid, file_name)
WHERE system_managed = true
""")
# DB-level enforcement of the application invariant that every File row
# has either a human owner (`user_uuid`) or a system parent
# (`parent_file_uuid`). The schema's `validate_system_managed_invariants`
# is the user-facing check; this CHECK constraint keeps raw inserts,
# `Repo.insert_all`, or external tools from silently breaking the
# invariant. NOT VALID on the ADD so existing rows don't have to be
# re-scanned (we follow with `VALIDATE CONSTRAINT` which only checks
# incoming writes after).
execute("""
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'phoenix_kit_files_user_or_parent_check'
) THEN
ALTER TABLE #{p}phoenix_kit_files
ADD CONSTRAINT phoenix_kit_files_user_or_parent_check
CHECK (user_uuid IS NOT NULL OR parent_file_uuid IS NOT NULL)
NOT VALID;
ALTER TABLE #{p}phoenix_kit_files
VALIDATE CONSTRAINT phoenix_kit_files_user_or_parent_check;
END IF;
END $$
""")
# Comments ↔ files attachment junction. Lives in core (not the comments
# module) because the files side is core. Comments owns the unlink
# lifecycle — `ON DELETE :restrict` on `file_uuid` blocks a file's hard
# delete while attachments exist; comments must detach first.
create_if_not_exists table(:phoenix_kit_comment_media,
primary_key: false,
prefix: prefix
) do
add(:uuid, :uuid,
primary_key: true,
default: fragment("uuid_generate_v7()"),
null: false
)
add(
:comment_uuid,
references(:phoenix_kit_comments,
column: :uuid,
type: :uuid,
on_delete: :delete_all,
prefix: prefix
),
null: false
)
add(
:file_uuid,
references(:phoenix_kit_files,
column: :uuid,
type: :uuid,
on_delete: :restrict,
prefix: prefix
),
null: false
)
add(:position, :integer, null: false)
add(:caption, :text)
timestamps(type: :utc_datetime)
end
create_if_not_exists(
unique_index(:phoenix_kit_comment_media, [:comment_uuid, :position],
name: :phoenix_kit_comment_media_comment_position_index,
prefix: prefix
)
)
create_if_not_exists(index(:phoenix_kit_comment_media, [:file_uuid], prefix: prefix))
execute("COMMENT ON TABLE #{p}phoenix_kit IS '113'")
end
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
drop_if_exists(table(:phoenix_kit_comment_media, prefix: prefix))
# Drop the CHECK + dedup index BEFORE dropping the columns they
# reference — order matters; PG won't let you drop a column with a
# live CHECK that references it.
execute(
"ALTER TABLE #{p}phoenix_kit_files DROP CONSTRAINT IF EXISTS phoenix_kit_files_user_or_parent_check"
)
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_files_system_dedup_index")
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_files_system_managed_index")
execute("DROP INDEX IF EXISTS #{p}phoenix_kit_files_parent_uuid_index")
# Tile rows have null user_uuid; clear them before restoring NOT NULL.
execute("DELETE FROM #{p}phoenix_kit_files WHERE system_managed = true")
# Drop FK before the column so the constraint name is freed for a
# future re-up. Both guarded so a partially-rolled-back schema doesn't
# crash here.
execute(
"ALTER TABLE #{p}phoenix_kit_files DROP CONSTRAINT IF EXISTS phoenix_kit_files_parent_file_uuid_fkey"
)
execute("ALTER TABLE #{p}phoenix_kit_files DROP COLUMN IF EXISTS parent_file_uuid")
execute("ALTER TABLE #{p}phoenix_kit_files DROP COLUMN IF EXISTS system_managed")
execute("ALTER TABLE #{p}phoenix_kit_files ALTER COLUMN user_uuid SET NOT NULL")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '112'")
end
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end