Custom and Enum Data Types

DANA 325 H E A R L I X C D S M P T Q B V W O G F N U K

Objectives

  • (1 Point)

  • (2 Points)

  • (2 Points)

  • (2 Points)

Please watch the videos under Resources before class.

In your Computer Science education you have learned that even for Booleans, which can be expressed in a single bit, all values in a computer (including a Postgres Database) take up at least 1 full Byte (8 bits). Recall and discuss with your partner why that is the case.

The Goal

This brings us to todays challenge. For whatever reason we will have a resource (schema) that contains 8 boolean fields. Stored directly, this would occupy 8*8 = 64 Bits in our database of which 56 bits would be wasted and unused.

We will use a custom Datatype to compress all 8 booleans into a smallint.

Small int is the smallest numeric datatype in postgres and occupies 2 instead of 4 Bytes. The range of numbers we can express by default is from -32768 to 32767. Other SQL variants might have even something like a tinyint to further reduce the field size but that is of little relevance to us.

We will also use virtual fields to capture the booleans without persisting them directly, using our custom datatype instead.

Objective: Creating Schema with Virtual Fields and Custom Datatype (1 point)

Lets generate a new live resource Item via a phoenix genernator.

mix phx.gen.live Items Item items name:string attributes:integer

Warning! If you have followed along the video you might have already created the Item resources. In this case create a new migration and add the attributes field instead. You don't need to recreate the :categories Enum type field from the video.

mix ecto.gen.migration add_attributes_to_items

Don't forget to change the datatype to smallint before you commit and migrate:

defmodule App.Repo.Migrations.CreateItems do
  use Ecto.Migration

  def change do
    create table(:items) do
      add :name, :string
      add :attributes, :smallint

      timestamps(type: :utc_datetime)
    end
  end
end

Back in our schema we want to create a bunch of virtual fields and change the attributes field to our custom type:

defmodule App.Items.Item do
  use Ecto.Schema
  import Ecto.Changeset

  schema "items" do
    field :name, :string
    field :attributes, App.Items.AttributesType

    Enum.each(1..8, &field(String.to_atom("attr#{&1}"), :boolean, default: false, virtual: true))

    timestamps(type: :utc_datetime)
  end

  @required ~w(name)a
  @optional ~w(attr1 attr2 attr3 attr4 attr5 attr6 attr7 attr8)a
  def changeset(item, attrs) do
    item
    |> cast(attrs, @required ++ @optional)
    |> validate_required(@required)
    |> prepare_changes(fn changeset ->
        # TODO: extract the values from the individual attr fields and merge them into your integer via `put_change`

        # this will likely change/removed once you have implemented TODO
        put_change(changeset, :attributes, 0)
      end)
  end
end

We have used a virtual field before! Checkout the user schema to see how we used it to enter the password, while we didn't persist the password and persisted a password_hash instead. Virtual fields behave just like regular fields except that we don't have a matching entry in the migration and thus database. They are intended for preprocessing input.

prepare_changes/2 is a very convenient function to only do something when we actually insert/update/delete a record as opposed to do it everytime a field is changed. You should definately look it up in the reference and learn more.

put_change/3 allows us to set the value of a field on the backend, bypassing all validations.

Objective: prepare_changes/3 correctly merges the virtual field values (2 points)

To get this point you will have to succefully merge all the booleans and convert it into an integer representing the binary form of the merged booleans. For example:

101 = 2^2 * 1 + 2^1 * 0 + 2^0 * 0 = 5

Use any technique you have learned in CSCI 204 to achieve this objective. You may also explore the elixir documentation for a function that allows you to directly convert a binary string into an integer. Any valid approach will count a point.

Objective: Custom Datatype (2 points)

Now its time to focus our attention back to the CustomDatatype we have yet to implement. Here is a starting template. You will need to think carefully and complete the cast, load, and dump rules. Remember you can use multiple function clauses and consult the Ecto Documentation for an example of custom datatypes.

defmodule App.Items.AttributesType do
  use Ecto.Type

  def type, do: :integer

  # Provide custom casting rules.
  def cast # TODO

  # When loading data from the database we want to return a map with the attributes and values as key value pairs, for example:
  # %{attr1: false, attr2: true, attr3: false, attr4: true, attr5: false, ...}
  # This means you will need to convert the integer back into booleans.
  def load # TODO

  # When dumping data to the database, we expect an integer
  def dump # TODO
end

Objective: Piecing it together (2 points)

The final points can be earned when you can successfully demonstrate persisting the custom datatype via checkbox fields in your form for the virtual fields that get persisted as the integer through the attributes field.

Remember to also set the value of the virtual fields back to what you extracted from the attributes field so they don't reset to false each time we want to edit an Item.

Here is a screenshot of my final items form: Screenshot

Copyright © 2025 Alexander Fuchsberger, Bucknell University. All rights reserved.