How to create a custom Ecto date field type?

The value of PostgreSQL’s date type can be between 4713 BC and 5874897 AD, but Ecto.Schema uses Elixir.Date to represent Ecto’s :date primitive type, so future dates (e.g., where the year exceeds 4 digits) cannot be entered using the defaults, even though PostgreSQL would have no issue with it:

f=# \d+ fictional_date                                                                                                                                                                                                                                   
                                           Table "public.fictional_date"                                                                                                                                                                                      
   Column    |  Type  | Collation | Nullable |                  Default                   | Storage |
-------------+--------+-----------+----------+--------------------------------------------+---------+           
 id          | bigint |           | not null | nextval('fictional_date_id_seq'::regclass) | plain   |
 future_date | date   |           |          |                                            | plain   |
indexes:
   "fictional_date_pkey" PRIMARY KEY, btree (id)
Access method: heap                                                                                                                                                                                                                                                         

f=# table fictional_date;
id |  future_date
---+---------------
 1 | 12345-12-24
 2 | 123456-12-24
 3 | 1234567-12-24                                                                                                                                                                                                                                                         
e (11 rows) 

If I wanted to implement a custom date field (e.g., PostgresDate) that allows the same values as PostgreSQL’s date type, then would the steps below be appropriate to do it?

  1. Migrations

    It would be ok to use the line

    add :future_date, :date
    

    in a migration, because, according to the Ecto.Migration documentation’s Field Types section:

    Ecto primitive types are mapped to the appropriate database type by the various database adapters

    In this case, :date will be used as is by Ecto.Adapters.Postgres. (Now, this adapter uses Postgrex which in turn uses Elixir.Date to represent :date, so do I need to define a custom Postgrex type as well?..)

  2. Schemas

    The Ecto.Schema documentation’s Custom types section is very clear that I would need to implement either Ecto.Type or Ecto.ParameterizedType behaviours.

    • The type() callback section brings a date-specific example, so I would return :date, I guess.

    • A dump() callback implementation should convert “the given term into an Ecto native type”, which is :date in my case. The example is straightforward (i.e., type/0 returns :map and dump/1 returns an Elixir.Map), but I can’t return an Elixir.Date because the issue is with it to begin with.


I stopped here because I have a feeling that I’m overthinking it. I couldn’t find an example implementation yet, so examples are welcome!

1 Like

I suspect you will need:

  • a custom date struct that conforms to the expectations of Date.t/0 but allows a wider range of years. You can test out that theory by saving such dates to the database by simply doing Date.utc_today() |> Map.put(:year, 5874897) and seeing if you can save that date to the database. I wouldn’t be surprised if that works.
  • a custom Postgrex date type because I suspect (I haven’t checked) that Postgrex will call Date.new/3 at some time when its loading data and that will fail with years outside 4 digits. You could copy the :date type and using it to call BiggerDate.new/3 instead.

EDIT: I checked and Postgrex checks the year range on both encoding (saving) and decoding (reading). So you’ll likely need to create a Postgrex type (that you’ll use in your migration) and an Ecto type (that you’ll use in your schemas). Neither of them would be too difficult to implement since you can lean heavily on the exiting implementations but remove their year constraints.

1 Like

I think a custom date struct may not be necessary, you just need a custom Calendar.

You’ll definitely need a custom ecto type. For migrations :date is probably fine.

1 Like

I thought so too, Ben, but postrex is checking for 4 digits on both encode and decode so I don’t think :date will work.

2 Likes