Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/ecto/adapters/postgres/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,9 @@ if Code.ensure_loaded?(Postgrex) do
table_name = quote_name(table.prefix, table.name)

query = [
"CREATE TABLE ",
"CREATE ",
modifiers_expr(table.modifiers),
"TABLE ",
if_do(command == :create_if_not_exists, "IF NOT EXISTS "),
table_name,
?\s,
Expand Down Expand Up @@ -1760,6 +1762,10 @@ if Code.ensure_loaded?(Postgrex) do
defp include_expr(literal),
do: quote_name(literal)

defp modifiers_expr(nil), do: []
defp modifiers_expr(<<_, _::binary>> = value), do: [String.upcase(String.trim(value)), ?\s]
defp modifiers_expr(value), do: error!(nil, "PostgreSQL adapter expects :modifiers to be a non-empty string or nil, got #{inspect(value)}")
Comment on lines +1766 to +1767
Copy link
Member

@greg-rychlewski greg-rychlewski Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
defp modifiers_expr(<<_, _::binary>> = value), do: [String.upcase(String.trim(value)), ?\s]
defp modifiers_expr(value), do: error!(nil, "PostgreSQL adapter expects :modifiers to be a non-empty string or nil, got #{inspect(value)}")
defp modifiers_expr(modifiers) when is_binary(modifiers), do: [modifiers, ?\s]
defp modifiers_expr(other), do: error!(nil, "PostgreSQL adapter expects :modifiers to be a string, got #{inspect(other)}")

This is for consistency with similar things like options. The existing pattern is not to normalize and not to care if they send an empty string. Raising seems a bit harsh for empty string as well since it's still valid SQL and they might populate this from a configuration value that is empty in some environments.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks for explaining


defp options_expr(nil),
do: []

Expand Down
9 changes: 7 additions & 2 deletions lib/ecto/migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -479,15 +479,16 @@ defmodule Ecto.Migration do

To define a table in a migration, see `Ecto.Migration.table/2`.
"""
defstruct name: nil, prefix: nil, comment: nil, primary_key: true, engine: nil, options: nil
defstruct name: nil, prefix: nil, comment: nil, primary_key: true, engine: nil, options: nil, modifiers: nil

@type t :: %__MODULE__{
name: String.t(),
prefix: String.t() | nil,
comment: String.t() | nil,
primary_key: boolean | keyword(),
engine: atom,
options: String.t()
options: String.t(),
modifiers: String.t() | nil
}
end

Expand Down Expand Up @@ -824,6 +825,10 @@ defmodule Ecto.Migration do
* `:options` - provide custom options that will be appended after the generated
statement. For example, "WITH", "INHERITS", or "ON COMMIT" clauses. "PARTITION BY"
can be provided for databases that support table partitioning.
* `:modifiers` - provide custom modifiers that should be inserted to the
table creation statement, between the tokens "CREATE" and "TABLE". For
example, "UNLOGGED", "GLOBAL", "TEMPORARY", or "GLOBAL TEMPORARY" in
PostgreSQL.

"""
def table(name, opts \\ [])
Expand Down
8 changes: 8 additions & 0 deletions lib/ecto/migration/runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,12 @@ defmodule Ecto.Migration.Runner do
defp command(ddl) when is_binary(ddl) or is_list(ddl),
do: "execute #{inspect(ddl)}"

defp command({:create, %Table{modifiers: <<_, _::binary>>} = table, _}),
do: "create #{render_modifiers(table.modifiers)} table #{quote_name(table.prefix, table.name)}"

defp command({:create, %Table{modifiers: value}, _}) when not is_nil(value),
do: raise(ArgumentError, "the value of :modifiers must be a non-empty string or nil, got #{inspect(value)}")

defp command({:create, %Table{} = table, _}),
do: "create table #{quote_name(table.prefix, table.name)}"

Expand Down Expand Up @@ -502,4 +508,6 @@ defmodule Ecto.Migration.Runner do
defp quote_name(prefix, name), do: quote_name(prefix) <> "." <> quote_name(name)
defp quote_name(name) when is_atom(name), do: quote_name(Atom.to_string(name))
defp quote_name(name), do: name

defp render_modifiers(value), do: String.downcase(String.trim(value))
end
16 changes: 16 additions & 0 deletions test/ecto/adapters/postgres_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,22 @@ defmodule Ecto.Adapters.PostgresTest do
]
end

test "create table with modifiers" do
create =
{:create, table(:posts, modifiers: "UNLOGGED"),
[
{:add, :id, :serial, [primary_key: true]},
{:add, :created_at, :naive_datetime, []}
]}

assert execute_ddl(create) == [
"""
CREATE UNLOGGED TABLE "posts" ("id" serial, "created_at" timestamp(0), PRIMARY KEY ("id"))
"""
|> remove_newlines
]
end

test "create table with composite key" do
create =
{:create, table(:posts),
Expand Down
19 changes: 19 additions & 0 deletions test/ecto/migrator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ defmodule Ecto.MigratorTest do
def change, do: flush()
end

defmodule CreateTableWithModifiersMigration do
use Ecto.Migration

def change do
create table(:sessions, modifiers: "UNLOGGED") do
add :id, :id
end
end
end

@moduletag migrated_versions: [{1, nil}, {2, nil}, {3, nil}]

setup context do
Expand Down Expand Up @@ -339,6 +349,15 @@ defmodule Ecto.MigratorTest do
assert output =~ "== Running 12 Ecto.MigratorTest.UpDownMigration.down/0"
assert output =~ "execute \"foo\""
assert output =~ ~r"== Migrated 12 in \d.\ds"

output =
capture_log(fn ->
:ok = up(TestRepo, 13, CreateTableWithModifiersMigration)
end)

assert output =~ "== Running 13 Ecto.MigratorTest.CreateTableWithModifiersMigration.change/0"
assert output =~ "create unlogged table sessions"
assert output =~ ~r"== Migrated 13 in \d.\ds"
end

test "logs ddl notices" do
Expand Down