Packages
phoenix_kit
1.7.154
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/v88.ex
defmodule PhoenixKit.Migrations.Postgres.V88 do
@moduledoc """
V88: Publishing schema V2 — restructure posts/versions/contents.
Posts become a minimal routing shell. Versions become the source of truth
for published state and metadata. Contents hold per-language title + body.
Changes:
- Add `active_version_uuid` to posts (FK → versions, nullable)
- Add `trashed_at` to posts (replaces status-based soft delete)
- Add `published_at` to versions (moved from posts)
- Add `title_i18n` and `description_i18n` JSONB to groups (for future i18n)
- Data migration: populate new columns from existing data
- Drop legacy post columns: `scheduled_at`, `status`, `published_at`, `primary_language`, `data`
- Drop obsolete indexes (scheduled, group_status, group_published_at)
- Add new indexes for active_version_uuid, trashed_at, and version published_at
"""
use Ecto.Migration
def up(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
schema = if prefix == "public", do: "public", else: prefix
# Guard: only run if publishing tables exist
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_publishing_posts'
) THEN
RAISE NOTICE 'Publishing tables not found, skipping V88';
RETURN;
END IF;
-- 1. Add active_version_uuid to posts
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'active_version_uuid'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN active_version_uuid UUID;
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD CONSTRAINT fk_publishing_posts_active_version
FOREIGN KEY (active_version_uuid)
REFERENCES #{p}phoenix_kit_publishing_versions(uuid)
ON DELETE SET NULL;
END IF;
-- 2. Add trashed_at to posts
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'trashed_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN trashed_at TIMESTAMPTZ;
END IF;
-- 3. Add published_at to versions
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_versions'
AND column_name = 'published_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_versions
ADD COLUMN published_at TIMESTAMPTZ;
END IF;
-- 4. Add translatable title/description to groups (for future i18n)
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_groups'
AND column_name = 'title_i18n'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_groups
ADD COLUMN title_i18n JSONB NOT NULL DEFAULT '{}';
END IF;
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_groups'
AND column_name = 'description_i18n'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_groups
ADD COLUMN description_i18n JSONB NOT NULL DEFAULT '{}';
END IF;
END $$;
""")
# ── New indexes ────────────────────────────────────────────────
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_active_version
ON #{p}phoenix_kit_publishing_posts (active_version_uuid)
WHERE active_version_uuid IS NOT NULL
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_trashed_at
ON #{p}phoenix_kit_publishing_posts (trashed_at)
WHERE trashed_at IS NULL
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_versions_published_at
ON #{p}phoenix_kit_publishing_versions (published_at DESC)
WHERE published_at IS NOT NULL
""")
# ── Data migration (BEFORE dropping legacy columns) ─────────────
# Populate trashed_at for trashed posts
migrate_trashed_posts(p, schema)
# Copy published_at from post to its published version
migrate_published_at(p, schema)
# Set active_version_uuid for published posts
migrate_active_version(p, schema)
# Copy metadata from content.data and post.data → version.data
migrate_version_data(p, schema)
# ── Drop legacy columns (AFTER data migration) ────────────────
drop_legacy_post_columns(p, schema)
# ── Drop obsolete indexes ──────────────────────────────────────
execute("DROP INDEX IF EXISTS idx_publishing_posts_scheduled")
execute("DROP INDEX IF EXISTS idx_publishing_posts_group_status")
execute("DROP INDEX IF EXISTS idx_publishing_posts_group_published_at")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '88'")
end
# WARNING: Rollback restores schema structure but data migrations are irreversible.
# - Legacy post columns are re-added as empty (original values lost)
# - published_at copied to versions is lost when the version column is dropped
# - active_version_uuid linkages are lost
# - Merged JSONB data (tags, seo, featured_image_uuid) cannot be un-merged
def down(opts) do
prefix = Map.get(opts, :prefix, "public")
p = prefix_str(prefix)
schema = if prefix == "public", do: "public", else: prefix
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_publishing_posts'
) THEN
RETURN;
END IF;
-- Restore legacy post columns (empty — original values lost)
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'scheduled_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN scheduled_at TIMESTAMPTZ;
END IF;
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'status'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'draft';
END IF;
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'published_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN published_at TIMESTAMPTZ;
END IF;
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'primary_language'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN primary_language VARCHAR(10) NOT NULL DEFAULT 'en';
END IF;
IF NOT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'data'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
ADD COLUMN data JSONB NOT NULL DEFAULT '{}';
END IF;
-- Drop new columns
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'active_version_uuid'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
DROP COLUMN active_version_uuid;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'trashed_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts
DROP COLUMN trashed_at;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_versions'
AND column_name = 'published_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_versions
DROP COLUMN published_at;
END IF;
-- Drop new group columns
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_groups'
AND column_name = 'title_i18n'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_groups
DROP COLUMN title_i18n;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_groups'
AND column_name = 'description_i18n'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_groups
DROP COLUMN description_i18n;
END IF;
END $$;
""")
# Drop new indexes
execute("DROP INDEX IF EXISTS idx_publishing_posts_active_version")
execute("DROP INDEX IF EXISTS idx_publishing_posts_trashed_at")
execute("DROP INDEX IF EXISTS idx_publishing_versions_published_at")
# Restore old indexes
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_scheduled
ON #{p}phoenix_kit_publishing_posts (scheduled_at)
WHERE status = 'scheduled'
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_status
ON #{p}phoenix_kit_publishing_posts (group_uuid, status)
""")
execute("""
CREATE INDEX IF NOT EXISTS idx_publishing_posts_group_published_at
ON #{p}phoenix_kit_publishing_posts (group_uuid, published_at DESC)
""")
execute("COMMENT ON TABLE #{p}phoenix_kit IS '87'")
end
# ── Column cleanup (runs AFTER data migration) ──────────────────
defp drop_legacy_post_columns(p, schema) do
execute("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '#{schema}' AND table_name = 'phoenix_kit_publishing_posts'
) THEN
RETURN;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'scheduled_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts DROP COLUMN scheduled_at;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'status'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts DROP COLUMN status;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'published_at'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts DROP COLUMN published_at;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'primary_language'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts DROP COLUMN primary_language;
END IF;
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'data'
) THEN
ALTER TABLE #{p}phoenix_kit_publishing_posts DROP COLUMN data;
END IF;
END $$;
""")
end
# ── Data migration helpers ───────────────────────────────────────
defp migrate_trashed_posts(p, schema) do
execute("""
DO $$
BEGIN
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'status'
) THEN
UPDATE #{p}phoenix_kit_publishing_posts
SET trashed_at = updated_at
WHERE status = 'trashed' AND trashed_at IS NULL;
END IF;
END $$;
""")
end
defp migrate_published_at(p, schema) do
execute("""
DO $$
BEGIN
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'published_at'
) THEN
UPDATE #{p}phoenix_kit_publishing_versions v
SET published_at = p.published_at
FROM #{p}phoenix_kit_publishing_posts p
WHERE v.post_uuid = p.uuid
AND v.status = 'published'
AND p.published_at IS NOT NULL
AND v.published_at IS NULL;
END IF;
END $$;
""")
end
defp migrate_active_version(p, schema) do
execute("""
DO $$
BEGIN
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'status'
) THEN
UPDATE #{p}phoenix_kit_publishing_posts p
SET active_version_uuid = v.uuid
FROM (
SELECT DISTINCT ON (post_uuid) post_uuid, uuid
FROM #{p}phoenix_kit_publishing_versions
WHERE status = 'published'
ORDER BY post_uuid, version_number DESC
) v
WHERE p.uuid = v.post_uuid
AND p.status = 'published'
AND p.active_version_uuid IS NULL;
END IF;
END $$;
""")
end
defp migrate_version_data(p, schema) do
# For each version, find the site-default language content (falling back to
# first by language ASC), and merge its data fields into version.data.
# Also merge post.data fields (allow_version_access, tags, seo).
execute("""
DO $$
BEGIN
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'data'
) THEN
-- Merge post.data fields into version.data
UPDATE #{p}phoenix_kit_publishing_versions v
SET data = v.data || jsonb_build_object(
'allow_version_access', COALESCE(p.data->'allow_version_access', 'false'::jsonb),
'tags', COALESCE(p.data->'tags', '[]'::jsonb),
'seo', COALESCE(p.data->'seo', '{}'::jsonb)
)
FROM #{p}phoenix_kit_publishing_posts p
WHERE v.post_uuid = p.uuid
AND p.data != '{}'::jsonb
AND NOT v.data ? 'tags';
-- Merge content.data fields into version.data (from best content row)
-- Uses primary_language to pick the best content per version if the column
-- still exists; falls back to 'en' on partial re-run after column was dropped
IF EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = '#{schema}'
AND table_name = 'phoenix_kit_publishing_posts'
AND column_name = 'primary_language'
) THEN
EXECUTE '
WITH best_content AS (
SELECT DISTINCT ON (c.version_uuid)
c.version_uuid,
c.data
FROM #{p}phoenix_kit_publishing_contents c
JOIN #{p}phoenix_kit_publishing_versions v ON v.uuid = c.version_uuid
JOIN #{p}phoenix_kit_publishing_posts p ON p.uuid = v.post_uuid
WHERE c.data != ''{}''::jsonb
ORDER BY c.version_uuid,
CASE WHEN c.language = COALESCE(p.primary_language, ''en'') THEN 0 ELSE 1 END,
c.language ASC
)
UPDATE #{p}phoenix_kit_publishing_versions v
SET data = v.data || jsonb_build_object(
''featured_image_uuid'', bc.data->''featured_image_uuid'',
''description'', bc.data->''description'',
''seo_title'', bc.data->''seo_title'',
''excerpt'', bc.data->''excerpt''
)
FROM best_content bc
WHERE v.uuid = bc.version_uuid
AND NOT v.data ? ''featured_image_uuid''
';
ELSE
-- primary_language already dropped (partial re-run), fall back to ''en''
EXECUTE '
WITH best_content AS (
SELECT DISTINCT ON (c.version_uuid)
c.version_uuid,
c.data
FROM #{p}phoenix_kit_publishing_contents c
JOIN #{p}phoenix_kit_publishing_versions v ON v.uuid = c.version_uuid
WHERE c.data != ''{}''::jsonb
ORDER BY c.version_uuid,
CASE WHEN c.language = ''en'' THEN 0 ELSE 1 END,
c.language ASC
)
UPDATE #{p}phoenix_kit_publishing_versions v
SET data = v.data || jsonb_build_object(
''featured_image_uuid'', bc.data->''featured_image_uuid'',
''description'', bc.data->''description'',
''seo_title'', bc.data->''seo_title'',
''excerpt'', bc.data->''excerpt''
)
FROM best_content bc
WHERE v.uuid = bc.version_uuid
AND NOT v.data ? ''featured_image_uuid''
';
END IF;
END IF;
END $$;
""")
end
defp prefix_str("public"), do: "public."
defp prefix_str(prefix), do: "#{prefix}."
end