coen.bakker

coen.bakker

I have a table for a collection of tasks. Only one of the fields :expiration_time and :expiration_all_day? may have a value (i.e. not be null/nil). Also, at least one should be provided.

Was making a start with implementing this, but can’t figure out how to use multiple fields in create constraint. I expected to be able to do this:

defmodule TodayTodo.Repo.Migrations.CreateTasks do
  use Ecto.Migration

  def change do
    create table(:tasks, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :description, :string
      add :expiration_date, :date
      add :expiration_time, :time
      add :expiration_all_day?, :boolean # Prob throws

      timestamps()
    end

    create(
      constraint(
        :tasks,
        :validate_time_or_all_day,
        check: "(expiration_time NOT NULL) OR (expiration_all_day? NOT NULL)"
      )
    )
  end
end

defmodule TodayTodo.Tasks.Task do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}

  schema "tasks" do
    field :description, :string
    field :expiration_date, :date
    field :expiration_time, :time
    field :expiration_all_day?, :boolean

    timestamps()
  end

  @doc false
  def changeset(task, attrs) do
    task
    |> cast(attrs, [:id, :description, :expiration_date, :expiration_time, :expiration_all_day?])
    |> validate_required([:description, :expiration_date])
    |> check_constraint(:expiration_time, name: :validate_time_or_all_day)
  end
end

But I get:

20:19:10.714 [info]  create table tasks

20:19:10.718 [info]  create check constraint validate_time_or_all_day on table tasks
** (Postgrex.Error) ERROR 42601 (syntax_error) syntax error at or near "NOT"

    query: ALTER TABLE "tasks" ADD CONSTRAINT "validate_time_or_all_day" CHECK ((expiration_time IS NOT NULL) OR (expiration_all_day? IS NOT NULL))
    (ecto_sql 3.8.3) lib/ecto/adapters/sql.ex:932: Ecto.Adapters.SQL.raise_sql_call_error/1
    (elixir 1.13.3) lib/enum.ex:1593: Enum."-map/2-lists^map/1-0-"/2
    (ecto_sql 3.8.3) lib/ecto/adapters/sql.ex:1024: Ecto.Adapters.SQL.execute_ddl/4
    (ecto_sql 3.8.3) lib/ecto/migration/runner.ex:352: Ecto.Migration.Runner.log_and_execute_ddl/3
    (ecto_sql 3.8.3) lib/ecto/migration/runner.ex:117: anonymous fn/6 in Ecto.Migration.Runner.flush/0
    (elixir 1.13.3) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto_sql 3.8.3) lib/ecto/migration/runner.ex:116: Ecto.Migration.Runner.flush/0
    (ecto_sql 3.8.3) lib/ecto/migration/runner.ex:289: Ecto.Migration.Runner.perform_operation/3

What am I missing?

EDIT: I changed the code blocks above, because I copied an old version of my code initially.

Showing Posts 1 to 10

al2o3cr

al2o3cr

The second argument to constraint should be a string or atom for the constraint name, but you’re passing a list of atoms.

coen.bakker

coen.bakker OP

My bad, I copied an old version of my code into this post. I have fixed that now.

So it’s not about the name argument needing to be a string or atom, but rather: I can’t quite figure out why I get the error ** (Postgrex.Error) ERROR 42601 (syntax_error) syntax error at or near "NOT" and what field I am supposed to provide in |> check_constraint(:expiration_time, name: :validate_time_or_all_day). Can check_constraint only take one field?

coen.bakker

coen.bakker OP

I tried other checks also but get similar errors. For example:

check: "num_nonnulls(expiration_time, expiration_all_day?) = 1"

al2o3cr

al2o3cr

You need to escape field names that have special characters like ? - Ecto normally takes care of this, but the SQL given to check is passed through verbatim.

dimitarvp

dimitarvp

Did you fix this? There’s probably at least two ways of doing it IIRC.

coen.bakker

coen.bakker OP

Finally got time to look at it again.

To handle the error I got when migrating, I tried to escape the ? from the column expiration_all_day?. I tried so by doing check: "num_nonnulls(expiration_time, expiration_all_day\?) = 1". That didn’t work. Then I tried using single quotes and that did the trick.

    create(
      constraint(
        :tasks,
        :validate_time_or_all_day,
        check: "num_nonnulls('expiration_time', 'expiration_all_day?') = 1"
      )
    )

In my changeset I have:

  def changeset(task, attrs) do
    task
    |> cast(attrs, [:id, :description, :expiration_date, :expiration_time, :expiration_all_day?])
    |> validate_required([:description, :expiration_date])
    |> check_constraint(:expiration_time, name: :validate_time_or_all_day, message: "select a time or 'all day', but not both")
  end

So I only mention one variable in check_constraint/3, not both. It works but also seems a bit odd.

But most importantly, what I thought would work for the exclusive or does not work: check: "num_nonnulls('col1', col2') = 1". But that probably has to do with that one of the columns is boolean and by default false. So never equal to null.

coen.bakker

coen.bakker OP

To follow up.

It indeed had to do with overlooking the default false of my boolean column. So it was an easy fix to make num_nonnulls/num_nulls work:

    create(
      constraint(
        :tasks,
        :validate_time_or_all_day,
        check: "num_nulls(time, NULLIF(all_day, false)) = 1"
      )
    )

As you can see I got rid of the question mark in my column name all together. Both cases didn’t work: NULLIF(all_day\?, false)) = 1 or NULLIF('all_day?', false)) = 1.

dimitarvp

dimitarvp

Yep, I was about to suggest you use CASE or NULLIF last night but was on the phone.

Glad you made it work!

coen.bakker

coen.bakker OP

Thanks for the support :nerd_face:

evadne

evadne

A simple solution would be to change the design completely and simplify:

  • Store expiration_date disallowing NULL
  • Store expiration_time which is a time and can be NULL, if the value is NULL then the task is all day

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews