blog picture

HTTP-417

Duration: uma representação de intervalo de tempo muito útil

Duration é coberto pelo padrão ISO 8601 que trata de data e tempo e define a quantidade de tempo em um dado intervalo.

Talvez o exemplo mais simples e clássico para explicar uma duração seria a diferença entre duas datas ou dois horários

  1. Quantos anos tem uma pessoa?
  2. Quantos dias faltam para o pagamento daquele empréstimo?
  3. O tempo de espera para o início do show é de n minutos

Dessa forma um tipo de dado representando a duração de um determinado intervalo de tempo se torna útil em muitos cenários

A representação legível para humanos de uma duração é feita, de acordo com o padrão ISO 8601, por uma letra representando a unidade de tempo e um número representando a quantidade dessa unidade. Assim temos que:

P é um marco utilizado para separar a porção de um Período de uma dada duração, representado da seguinte forma:

Yrepresenta o número de anos(Years)
Mrepresenta o número de meses(Months)
Wrepresenta o número de semanas(Weeks)
Drepresenta o número de dias(Days)

T é um marco utilizado para separar a porção de Tempo de uma dada duração, representado da seguinte forma:

Hrepresenta o número de horas(Hours)
Mrepresenta o número de minutos(Minutes)
Srepresenta o número de segundos(Seconds)

Logo P3W2DT10H45M representa uma duração de 3 semanas, 2 dias, 10 horas e 45 minutos

Ecto, Postgrex e Duration

A estrutura Duration e suas funções no Elixir foram introduzidas na versão 1.17 e em versões anteriores, segundo a documentação, a estrutura utilizada para decodificar intervalos de tempo no PostgreSQL era o Postgrex.Interval que continua sendo a estrutura padrão

A seguir temos a definição do schema e migration para um conjunto fictício de dados e sua materialização no banco de dados

Schema

schema "records" do
  field :name, :string
  field :description, :string 
  field :start, :utc_datetime
  field :duration, :duration
end

Migration

def change do
   create table(:records, primary_key: false) do
     add :id, :uuid, primary_key: true
     add :name, :string
     add :description, :string
     add :start, :utc_datetime
     add :duration, :duration

     timestamps(type: :utc_datetime)
   end
 end

Estrutura da tabela

   Column    |              Type              | Collation | Nullable | Default
-------------+--------------------------------+-----------+----------+---------+
 id          | uuid                           |           | not null |         |
 name        | character varying(255)         |           |          |         |
 description | character varying(255)         |           |          |         |
 start       | timestamp(0) without time zone |           |          |         |
 duration    | interval                       |           |          |         |

Dados persistidos

                  id                  |    name    |     description      | start | duration |
--------------------------------------+------------+----------------------+-------+----------+
 664e5fd6-17ab-4a27-8799-9ebca4c603e5 | Record AA | The Record AA descr   |       | 11 days  |

O que deu errado e porque estou falando desse assunto?

Como a estrutura padrão mapeada para intervalos no PostgreSQL é o Postgrex.Interval quando o tipo :duration é definido no schema e nas migrations o valor é persistido corretamente porém na recuperação do dado e o consequente mapeamento para o struct %Duration{} ocorre o seguinte erro

(ArgumentError) cannot load %Postgrex.Interval{months: 0, days: 11, secs: 0, microsecs: 0} as type :duration for field :duration

Quando se está trabalhando num produto é interessante manter o foco no trabalho, no domínio do negócio e por vezes misturar código de infraestrutrura pode gerar débitos técnicos, dificuldade de manutenção e talvez o principal seja tirar a clareza do código.

É interessante poder manter homogeneidade do código mantendo tanto quanto possível o uso das bibliotecas padrão sempre que suportadas, e nesse caso a estrutura Duration era suportada e por um motivo que eu não sabia não estava funcionando. O conhecimento de ser Postgrex.Interval a estrutura padrão para decodificar o tipo interval do PostgreSQL veio através da pesquisa sobre como resolver o erro aí em cima.

Aprender é uma constante na indústria de tecnologia, mas nem sempre dispomos do tempo que gostaríamos para adquirir novos conhecimentos, explorar uma biblioteca ou framework novo, etc, portanto torna-se praticamente impossível não buscar caminhos mais curtos, que resolvam o problema e permitam seguir adiante, porque tempo é dinheiro e queria resolver o problema logo

Procurei bastante mas não encontrei nada no estilo stackoverflow, um simples copy & paste para commitar mais um código e encerrar o dia, então fui juntando informando aqui e acolá, parando de ser preguiçoso e lendo mais a documentação para economizar o tempo de alguém no futuro

Talvez eu não tenha encontrado muita coisa por não utilizar as keywords corretas ou por insistir em usar o DuckDuckGo

Configuração

Basicamente a configuração de uma Duration como decodificadora de interval se concentra em dois arquivos: config.exs e postgrex_types.ex

O arquivo postgrex_types.ex contém a configuração de uma opção ao tipo padrão Postgrex.Interval, já o arquivo config.exs associa os tipos customizados ao Ecto.Repo

Sem mais demora e preparando aquele pedaço de código para copiar, colar e encerrar o dia, assim ficaria a configuração que faz tudo funcionar de forma quase mágica (quase porque na verdade é design de código de caras muito bons que mantém isso tudo funcionando)

# config/config.exs
# configures postgrex types

import Config
 
config :my_app,
  ecto_repos: [MyApp.Repo],
  generators: [timestamp_type: :utc_datetime]

# configures postgrex types
config :my_app, MyApp.Repo, types: MyApp.PostgresTypes

# Configures the endpoint
(config continues...)

# lib/my_app/postgres_types.ex
# set the interval decode

Postgrex.Types.define(
  ConstruaApi.PostgresTypes,
  [] ++ Ecto.Adapters.Postgres.extensions(),
  interval_decode_type: Duration
)
# --- that's all folks ---

Enfim funcionou

A busca por um exemplo simples e direto não funcionou para mim e na correria em busca de solução esqueci de perguntar para os mecanismos de IA disponíveis como fazer isso de forma rápida e fácil (Doh!)

Agora se alguém precisar de um código na mão pra resolver este problema ou outros similares aqui está, mas não esqueça que os GPTs, Geminis e outras IAs da vida estão ai pra ajudar

uma versão em inglês está disponível aqui