View Source Ecto Quickstart

Vtc ships with a number of Ecto custom types designed to work with Postgres databases connected by Postgrex.

ecto-types

Ecto types

The custom Ecto & Postgres types provided by Vtc are:

PgRational

PgFramerate

PgFramestamp

PgFramestamp.Range

configuration

Configuration

Additional configuration is done at the Repo level:

config :vtc, Vtc.Test.Support.Repo,
  adapter: Ecto.Adapters.Postgres,
  vtc: [
    rational: [
      functions_schema: :rational,
      functions_prefix: ""
    ],
    framerate: [
      functions_schema: :framerate,
      functions_prefix: ""
    ],
    framestamp: [
      functions_schema: :framestamp,
      functions_prefix: ""
    ],
    framestamp_range: [
      functions_schema: :framestamp_range,
      functions_prefix: ""
    ]
  ]

config :vtc,
  ecto_repos: [Vtc.Test.Support.Repo]

Each type can be configured to store it's support functions on a schema of your choice as well as have a custom, per-type function name prefix.

By default, all functions are added to the public schema, and are prefaced by their type name. For example, the PgFramestamp frames function is created in the public schema, and named framestamp_frames.

initial-migration

Initial Migration

To create all the Postgres types, functions, operators, casts, and operator classes that Vtc provides, run the following migration:

defmodule Vtc.Test.Support.Repo.Migrations.AddPostgresTypes do
  @moduledoc false
  use Ecto.Migration

  def change do
    Vtc.Ecto.Postgres.Migrations.run()
  end
end

This migration is safe to run multiple times, and can be run again when new functionality is added to Vtc. All migrations implement both up and down functionality, and support rollbacks out of the box.

All objects created by the migration come complete with in-database documentation with links to the relevant Vtc docs. Using DataGrip:

Database documentation

ecto-api

Ecto API

Vtc's custom Ecto types are each implemented in a dedicated module. For instance, the framestamp type is implemented by PgFramestamp.

However, each ecto type's functions are re-exported by the Vtc elixir type it is designed to work with, allowing you to mostly ignore the dedicated ecto module when creating migrations and schemas:

defmodule MyApp.MySchema do
  @moduledoc false
  use Ecto.Migration

  alias Vtc.Ecto.Postgres.PgFramestamp
  alias Vtc.Framestamp

  def change do
    create table("my_table", primary_key: false) do
      add(:id, :uuid, primary_key: true, null: false)
      add(:a, Framestamp.type())
      add(:b, Framestamp.type())
    end
  end
end

Each type ships with a set of custom constraints that can be used for data integrity validation in migrations:

Framestamp constraint docs

def change do
  create table("my_table", primary_key: false) do
    add(:b, Framestamp.type())
  end

  PgFramestamp.Migrations.create_constraints("my_table", :b)
end

Vtc modules can be used directly in schemas:

Framestamp changeset casting docs

defmodule Vtc.Test.Support.FramestampSchema01 do
  @moduledoc false
  use Ecto.Schema

  alias Ecto.Changeset
  alias Vtc.Framestamp

  @type t() :: %__MODULE__{
          id: Ecto.UUID.t(),
          a: Framestamp.t(),
          b: Framestamp.t()
        }

  @primary_key {:id, Ecto.UUID, autogenerate: true}

  schema "my_table" do
    field(:a, Framestamp)
    field(:b, Framestamp)
  end

  @spec changeset(%__MODULE__{}, %{atom() => any()}) :: Changeset.t(%__MODULE__{})
  def changeset(schema, attrs), do: Changeset.cast(schema, attrs, [:id, :a, :b])
end

Values can be used in Ecto queries using the type/2 function. Vtc registers native operators for each type, so you can write queries like you would expect to with any other numeric type:

query value

iex> one_hour = Framestamp.with_frames("01:00:00:00", Rates.f23_98())
iex> 
iex> EdlEvents
iex> |> where([event], event.start > type(^one_hour, Framestamp))
iex> |> select([event], {event, event.end - event.in_framestamp})

The above query finds all events with a start time greater than 01:00:00:00 and returns the record AND its calculated duration.

private-functions

Private functions

Many of Vtc's postgres functions are prefaced with __private__, for instance, the framestamp__private__add function is used to back the addition + operator.

These functions have no API stability guarantee, and callers should avoid calling them directly.

framestamp-postgres-functions

Framestamp Postgres functions

with_seconds-2

with_seconds/2

Elixir

iex> seconds = Ratio.new(3600)
iex> rate = Rates.f23_98()
iex> 
iex> query =
iex>   Query.from(
iex>     f in fragment(
iex>       "SELECT framestamp.with_seconds(?, ?) as r",
iex>       type(^seconds, PgRational),
iex>       type(^rate, Framerate)
iex>     ),
iex>     select: f.r
iex>   )
iex> 
iex> query |> Repo.one!() |> Framestamp.load() |> inspect()
"{:ok, <00:59:56:10 <23.98 NTSC>>}"

SQL

SELECT framestamp.with_seconds(
  (3600, 1)::rational,
  ((24000, 1001), '{non_drop}')::framerate
);

Output:

                     with_seconds
------------------------------------------------------
 (43200157,12000,24000,1001,{non_drop})
(1 row)

Notice that just like Framestamp.with_seconds/3, the seconds value is rounded to the nearest whole-frame on construction.

with_frames-2

with_frames/2

Elixir

iex> frames = 24
iex> rate = Rates.f23_98()
iex> 
iex> query =
iex>   Query.from(
iex>     f in fragment(
iex>       "SELECT framestamp.with_frames(?, ?) as r",
iex>       ^frames,
iex>       type(^rate, Framerate)
iex>     ),
iex>     select: f.r
iex>   )
iex> 
iex> query |> Repo.one!() |> Framestamp.load() |> inspect()
"{:ok, <00:00:01:00 <23.98 NTSC>>}"

SQL

SELECT framestamp.with_frames(
  24,
  ((24000, 1001), '{non_drop}')::framerate
);

Output:

                   with_frames
-------------------------------------------------
 (1001,1000,24000,1001,{non_drop})
(1 row)

frame-count

frame count

Elixir

iex> stamp =
iex>   Framestamp.with_frames!(
iex>     "01:00:00:00",
iex>     Rates.f23_98()
iex>   )
iex> 
iex> query =
iex>   Query.from(
iex>     f in fragment(
iex>       "SELECT framestamp.frames(?) as r",
iex>       type(^stamp, Framestamp)
iex>     ),
iex>     select: f.r
iex>   )
iex> 
iex> Repo.one!(query)
86400

SQL

SELECT framestamp.frames(
  (18018, 5, 24000, 1001, '{non_drop}')
);

Output:

  frames
--------
  86400
(1 row)

framestamp-postgres-operators

Framestamp Postgres operators

Available operators

nametypeargsresultdescription
=operatorframestamp, framestampbooleantrue if a seconds == b seconds
<operatorframestamp, framestampbooleantrue if a seconds < b seconds
<=operatorframestamp, framestampbooleantrue if a seconds <= b seconds
>operatorframestamp, framestampbooleantrue if a seconds > b seconds
>=operatorframestamp, framestampbooleantrue if a seconds >= b seconds
ABSfunctionframestampframestampabsolute value
SIGNfunctionframestampintegerreturns -1, 0, or 1 based on comparison to 0
@operatorframestampframestampabsolute value
-functionframestampframestampnegate. flips input's sign
+operatorframestamp, framestampframestampaddition. raises if framerates do not match
@+operatorframestamp, framestampframestampaddition. inherit framerate from left
+@operatorframestamp, framestampframestampaddition. inherit framerate from right
-operatorframestamp, framestampframestampsubtraction. raises if framerates do not match
@-operatorframestamp, framestampframestampsubtraction. inherit framerate from left
-@operatorframestamp, framestampframestampsubtraction. inherit framerate from right
*operatorframestamp, rationalframestampmultiplication. rounds to nearest whole-frame
/operatorframestamp, rationalframestampdivision. rounds to nearest frame
DIVfunctionframestamp, rationalframestampfloor-division. rounds down to nearest frame
%operatorframestamp, rationalframestampremainder. returns leftover frames from DIV

.

mixed-rate-arithmetic

Mixed rate arithmetic

Elixir

iex> a = Framestamp.with_frames!("01:00:00:00", Rates.f23_98())
iex> b = Framestamp.with_frames!("01:00:00:00", Rates.f47_95())
iex> 
iex> query =
iex>   Query.from(
iex>     f in fragment(
iex>       "SELECT "? +@ ? as r",
iex>       type(^a, Framestamp),
iex>       type(^b, Framestamp)
iex>     ),
iex>     select: f.r
iex>   )
iex> 
iex> Repo.one!(query) |> Framestamp.load() |> inspect()
"{:ok, <02:00:00:00 <47.95 NTSC>>}"

SQL

SELECT
  (18018, 5, 24000, 1001, '{non_drop}')::framestamp
  +@ (18018, 5, 48000, 1001, '{non_drop}')::framestamp;

Output:

                   ?column?
-----------------------------------------------
 ("(36036,5)","(""(48000,1001)"",{non_drop})")
(1 row)

rational-postgres-operators

Rational Postgres operators

Available operators

nametypeargsresultdescription
=operatorrational, rationalbooleantrue if a == b
<operatorrational, rationalbooleantrue if a < b
<=operatorrational, rationalbooleantrue if a <= b
>operatorrational, rationalbooleantrue if a > b
>=operatorrational, rationalbooleantrue if a >= b
ABSfunctionrationalrationalabsolute value
SIGNfunctionrationalintegerreturns -1, 0, or 1 based on comparison to 0
@operatorrationalrationalabsolute value
-functionrationalrationalnegate. flips input's sign
+operatorrational, rationalrationaladdition
-operatorrational, rationalrationalsubtraction
*operatorrational, rationalrationalmultiplication
/operatorrational, rationalrationaldivision. rounds to the nearest integer
DIVfunctionrational, rationalrationalfloor-division. rounds down to nearest integer
%operatorrational, rationalrationalremainder based on DIV