Packages
phoenix_kit
1.7.113
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/v58.ex
defmodule PhoenixKit.Migrations.Postgres.V58 do
@moduledoc """
V58: Timestamp Column Type Standardization (timestamptz)
Converts ALL timestamp columns across all 68 PhoenixKit tables from
`timestamp without time zone` (aka `timestamp(0)`) to
`timestamp with time zone` (aka `timestamptz`).
This completes the DateTime standardization:
- Steps 1-4 (prior): Elixir schemas → `:utc_datetime` / `DateTime.utc_now()`
- V58: PostgreSQL columns → `timestamptz`
## Why timestamptz?
PostgreSQL `timestamp` stores values without timezone context. While PhoenixKit
always stores UTC, `timestamptz` makes this explicit at the database level and
is the PostgreSQL-recommended type for timestamps. Ecto's `:utc_datetime` type
maps to `timestamptz` natively.
## Conversion Safety
The `up` direction needs no `USING` clause — PostgreSQL implicitly treats
existing `timestamp` values as UTC when casting to `timestamptz`.
The `down` direction requires `USING col AT TIME ZONE 'UTC'` to explicitly
convert `timestamptz` back to `timestamp(0)`.
## Idempotency
All operations check:
- `table_exists?/2` — skip if table doesn't exist (optional modules)
- `column_exists?/3` — skip if column doesn't exist
- `column_is_timestamptz?/3` — skip if already converted (up) or already reverted (down)
"""
use Ecto.Migration
# All PhoenixKit tables and their timestamp columns (68 tables, ~193 columns)
@timestamp_columns [
# V01 — Core Auth
{"phoenix_kit", ["migrated_at"]},
{"phoenix_kit_users", ["confirmed_at", "inserted_at", "updated_at"]},
{"phoenix_kit_users_tokens", ["inserted_at"]},
{"phoenix_kit_user_roles", ["inserted_at", "updated_at"]},
{"phoenix_kit_user_role_assignments", ["assigned_at", "inserted_at"]},
# V03 — Settings
{"phoenix_kit_settings", ["date_added", "date_updated"]},
# V04 — Referrals
{"phoenix_kit_referral_codes", ["date_created", "expiration_date"]},
{"phoenix_kit_referral_code_usage", ["date_used"]},
# V07+V13+V19 — Email Logs (merged)
{"phoenix_kit_email_logs",
[
"sent_at",
"delivered_at",
"bounced_at",
"complained_at",
"opened_at",
"clicked_at",
"queued_at",
"rejected_at",
"failed_at",
"delayed_at",
"inserted_at",
"updated_at"
]},
{"phoenix_kit_email_events", ["occurred_at", "inserted_at", "updated_at"]},
# V09 — Email Blocklist
{"phoenix_kit_email_blocklist", ["expires_at", "inserted_at", "updated_at"]},
# V15 — Email Templates
{"phoenix_kit_email_templates", ["last_used_at", "inserted_at", "updated_at"]},
# V16 — OAuth
{"phoenix_kit_user_oauth_providers", ["token_expires_at", "inserted_at", "updated_at"]},
# V17 — Entities
{"phoenix_kit_entities", ["date_created", "date_updated"]},
{"phoenix_kit_entity_data", ["date_created", "date_updated"]},
# V20 — Storage
{"phoenix_kit_buckets", ["inserted_at", "updated_at"]},
{"phoenix_kit_files", ["inserted_at", "updated_at"]},
{"phoenix_kit_file_instances", ["inserted_at", "updated_at"]},
{"phoenix_kit_file_locations", ["last_verified_at", "inserted_at", "updated_at"]},
{"phoenix_kit_storage_dimensions", ["inserted_at", "updated_at"]},
# V22 — Email Extensions
{"phoenix_kit_email_orphaned_events",
["received_at", "matched_at", "inserted_at", "updated_at"]},
{"phoenix_kit_email_metrics", ["inserted_at", "updated_at"]},
{"phoenix_kit_audit_logs", ["inserted_at"]},
# V29 — Posts
{"phoenix_kit_posts", ["scheduled_at", "published_at", "inserted_at", "updated_at"]},
{"phoenix_kit_post_media", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_likes", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_comments", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_mentions", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_tags", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_tag_assignments", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_groups", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_group_assignments", ["inserted_at", "updated_at"]},
{"phoenix_kit_post_views", ["viewed_at", "inserted_at", "updated_at"]},
# V31 — Billing Core
{"phoenix_kit_currencies", ["inserted_at", "updated_at"]},
{"phoenix_kit_billing_profiles", ["inserted_at", "updated_at"]},
{"phoenix_kit_orders",
[
"confirmed_at",
"paid_at",
"cancelled_at",
"checkout_expires_at",
"inserted_at",
"updated_at"
]},
{"phoenix_kit_invoices",
[
"receipt_generated_at",
"sent_at",
"paid_at",
"voided_at",
"inserted_at",
"updated_at"
]},
{"phoenix_kit_transactions", ["inserted_at", "updated_at"]},
# V32 — AI Core
{"phoenix_kit_ai_accounts", ["last_validated_at", "inserted_at", "updated_at"]},
{"phoenix_kit_ai_requests", ["inserted_at", "updated_at"]},
# V33 — Billing Extended
{"phoenix_kit_payment_methods", ["inserted_at", "updated_at"]},
{"phoenix_kit_subscription_plans", ["inserted_at", "updated_at"]},
{"phoenix_kit_subscriptions",
[
"current_period_start",
"current_period_end",
"cancelled_at",
"trial_start",
"trial_end",
"grace_period_end",
"last_renewal_attempt_at",
"inserted_at",
"updated_at"
]},
{"phoenix_kit_payment_provider_configs", ["last_verified_at", "inserted_at", "updated_at"]},
{"phoenix_kit_webhook_events", ["processed_at", "inserted_at", "updated_at"]},
# V34 — AI Endpoints
{"phoenix_kit_ai_endpoints", ["last_validated_at", "inserted_at", "updated_at"]},
# V35 — Tickets
{"phoenix_kit_tickets", ["resolved_at", "closed_at", "inserted_at", "updated_at"]},
{"phoenix_kit_ticket_comments", ["inserted_at", "updated_at"]},
{"phoenix_kit_ticket_attachments", ["inserted_at", "updated_at"]},
{"phoenix_kit_ticket_status_history", ["inserted_at"]},
# V36 — Social/Connections
{"phoenix_kit_user_follows", ["inserted_at"]},
{"phoenix_kit_user_connections",
["requested_at", "responded_at", "inserted_at", "updated_at"]},
{"phoenix_kit_user_blocks", ["inserted_at"]},
{"phoenix_kit_user_follows_history", ["inserted_at"]},
{"phoenix_kit_user_connections_history", ["inserted_at"]},
{"phoenix_kit_user_blocks_history", ["inserted_at"]},
# V37 — Sync (renamed in V44)
{"phoenix_kit_sync_connections",
[
"expires_at",
"approved_at",
"suspended_at",
"revoked_at",
"last_connected_at",
"last_transfer_at",
"inserted_at",
"updated_at"
]},
{"phoenix_kit_sync_transfers",
[
"approved_at",
"denied_at",
"approval_expires_at",
"started_at",
"completed_at",
"inserted_at"
]},
# V38 — AI Prompts
{"phoenix_kit_ai_prompts", ["last_used_at", "inserted_at", "updated_at"]},
# V39 — Admin Notes
{"phoenix_kit_admin_notes", ["inserted_at", "updated_at"]},
# V42 — Scheduled Jobs
{"phoenix_kit_scheduled_jobs", ["scheduled_at", "executed_at", "inserted_at", "updated_at"]},
# V43 — Legal/Consent
{"phoenix_kit_consent_logs", ["inserted_at", "updated_at"]},
# V45 — Shop
{"phoenix_kit_shop_categories", ["inserted_at", "updated_at"]},
{"phoenix_kit_shop_products", ["inserted_at", "updated_at"]},
{"phoenix_kit_shop_shipping_methods", ["inserted_at", "updated_at"]},
{"phoenix_kit_shop_carts", ["expires_at", "converted_at", "inserted_at", "updated_at"]},
{"phoenix_kit_shop_cart_items", ["inserted_at", "updated_at"]},
{"phoenix_kit_payment_options", ["inserted_at", "updated_at"]},
# V46 — Shop Config/Import
{"phoenix_kit_shop_config", ["inserted_at", "updated_at"]},
{"phoenix_kit_shop_import_logs", ["started_at", "completed_at", "inserted_at", "updated_at"]},
{"phoenix_kit_shop_import_configs", ["inserted_at", "updated_at"]},
# V48 — Post Reactions
{"phoenix_kit_post_dislikes", ["inserted_at", "updated_at"]},
{"phoenix_kit_comment_likes", ["inserted_at", "updated_at"]},
{"phoenix_kit_comment_dislikes", ["inserted_at", "updated_at"]},
# V53 — Role Permissions
{"phoenix_kit_role_permissions", ["inserted_at"]},
# V55 — Standalone Comments
{"phoenix_kit_comments", ["inserted_at", "updated_at"]},
{"phoenix_kit_comments_likes", ["inserted_at", "updated_at"]},
{"phoenix_kit_comments_dislikes", ["inserted_at", "updated_at"]}
]
def up(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
Enum.each(@timestamp_columns, fn {table, columns} ->
if table_exists?(table, escaped_prefix) do
convert_columns_to_timestamptz(table, columns, prefix, escaped_prefix)
end
end)
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '58'")
end
def down(%{prefix: prefix} = opts) do
escaped_prefix = Map.get(opts, :escaped_prefix, prefix)
Enum.each(@timestamp_columns, fn {table, columns} ->
if table_exists?(table, escaped_prefix) do
revert_columns_from_timestamptz(table, columns, prefix, escaped_prefix)
end
end)
execute("COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '57'")
end
defp convert_columns_to_timestamptz(table, columns, prefix, escaped_prefix) do
full_table = prefix_table_name(table, prefix)
Enum.each(columns, fn col ->
if column_exists?(table, col, escaped_prefix) and
not column_is_timestamptz?(table, col, escaped_prefix) do
execute("ALTER TABLE #{full_table} ALTER COLUMN #{col} TYPE timestamptz")
end
end)
end
defp revert_columns_from_timestamptz(table, columns, prefix, escaped_prefix) do
full_table = prefix_table_name(table, prefix)
Enum.each(columns, fn col ->
if column_exists?(table, col, escaped_prefix) and
column_is_timestamptz?(table, col, escaped_prefix) do
execute(
"ALTER TABLE #{full_table} ALTER COLUMN #{col} " <>
"TYPE timestamp(0) USING #{col} AT TIME ZONE 'UTC'"
)
end
end)
end
# Helpers
defp table_exists?(table, escaped_prefix) do
query = """
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = '#{table}'
AND table_schema = '#{escaped_prefix}'
)
"""
case repo().query(query, [], log: false) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp column_exists?(table, column, escaped_prefix) do
query = """
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_name = '#{table}'
AND column_name = '#{column}'
AND table_schema = '#{escaped_prefix}'
)
"""
case repo().query(query, [], log: false) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp column_is_timestamptz?(table, column, escaped_prefix) do
query = """
SELECT data_type = 'timestamp with time zone'
FROM information_schema.columns
WHERE table_name = '#{table}'
AND column_name = '#{column}'
AND table_schema = '#{escaped_prefix}'
"""
case repo().query(query, [], log: false) do
{:ok, %{rows: [[true]]}} -> true
_ -> false
end
end
defp prefix_table_name(table_name, nil), do: table_name
defp prefix_table_name(table_name, "public"), do: "public.#{table_name}"
defp prefix_table_name(table_name, prefix), do: "#{prefix}.#{table_name}"
end