lib/ecto/postgres/pg_rational_migrations.ex

use Vtc.Ecto.Postgres.Utils

defpgmodule Vtc.Ecto.Postgres.PgRational.Migrations do
  @moduledoc """
  Migrations for adding rational types, casts, functions and constraints to a
  Postgres database.
  """
  use Vtc.Ecto.Postgres.PgTypeMigration

  alias Ecto.Migration
  alias Ecto.Migration.Constraint
  alias Vtc.Ecto.Postgres.Fragments
  alias Vtc.Ecto.Postgres.PgRational
  alias Vtc.Ecto.Postgres.PgTypeMigration

  require Ecto.Migration

  @doc section: :migrations_full
  @doc """
  Adds raw SQL queries to a migration for creating the database types, associated
  functions, casts, operators, and operator families.

  This migration includes all migrations under the
  [Pg Types](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-types),
  [Pg Operators](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-operators),
  [Pg Operator Classes](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-operator-classes),
  [Pg Functions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-functions),
  [Pg Private Functions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-private-functions),
  headings, and
  [Pg Casts](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-casts)

  Safe to run multiple times when new functionality is added in updates to this library.
  Existing values will be skipped.

  Individual migration functions return raw sql commands in an
  {up_command, down_command} tuple.

  ## Options

  - `include`: A list of migration functions to include. If not set, all sub-migrations
    will be included.

  - `exclude`: A list of migration functions to exclude. If not set, no sub-migrations
    will be excluded.

  ## Types Created

  Calling this macro creates the following type definitions:

  ```sql
  CREATE TYPE public.rational AS (
    numerator bigint,
    denominator bigint
  );
  ```

  ## Schemas Created

  Up to two schemas are created as detailed by the
  [Configuring Database Objects](Vtc.Ecto.Postgres.PgRational.Migrations.html#create_all/0-configuring-database-objects)
  section below.

  ## Configuring Database Objects

  To change where supporting functions are created, add the following to your
  Repo configuration:

  ```elixir
  config :vtc, Vtc.Test.Support.Repo,
    adapter: Ecto.Adapters.Postgres,
    ...
    vtc: [
      rational: [
        functions_schema: :rational,
        functions_prefix: "rational"
      ]
    ]
  ```

  Option definitions are as follows:

  - `functions_schema`: The postgres schema to store rational-related custom functions.

  - `functions_prefix`: A prefix to add before all functions. Defaults to "rational"
    for any function created in the `:public` schema, and "" otherwise.

  ## Examples

  ```elixir
  defmodule MyMigration do
    use Ecto.Migration

    alias Vtc.Ecto.Postgres.PgRational
    require PgRational.Migrations

    def change do
      PgRational.Migrations.run()
    end
  end
  ```
  """
  @spec run(include: Keyword.t(atom()), exclude: Keyword.t(atom())) :: :ok
  def run(opts \\ []), do: PgTypeMigration.run_for(__MODULE__, opts)

  @doc false
  @impl PgTypeMigration
  def ecto_type, do: PgRational

  @doc false
  @impl PgTypeMigration
  def migrations_list do
    [
      &create_type/0,
      &create_function_schemas/0,
      &create_func_simplify/0,
      &create_func_minus/0,
      &create_func_abs/0,
      &create_func_sign/0,
      &create_func_round/0,
      &create_func_floor/0,
      &create_func_add/0,
      &create_func_sub/0,
      &create_func_mult/0,
      &create_func_div/0,
      &create_func_floor_div/0,
      &create_func_modulo/0,
      &create_op_abs/0,
      &create_op_minus/0,
      &create_op_add/0,
      &create_op_sub/0,
      &create_op_mult/0,
      &create_op_div/0,
      &create_op_modulo/0,
      &create_func_cmp/0,
      &create_func_eq/0,
      &create_func_neq/0,
      &create_func_lt/0,
      &create_func_lte/0,
      &create_func_gt/0,
      &create_func_gte/0,
      &create_op_eq/0,
      &create_op_neq/0,
      &create_op_lt/0,
      &create_op_lte/0,
      &create_op_gt/0,
      &create_op_gte/0,
      &create_op_class_btree/0,
      &create_func_cast_to_double_precision/0,
      &create_func_cast_bigint_to_rational/0,
      &create_cast_double_precision/0,
      &create_cast_bigint_to_rational/0
    ]
  end

  @doc section: :migrations_types
  @doc """
  Adds:

  - `rational` composite type
  - `rationals` schema
  - `rationals_helpers` schema
  """
  @spec create_type() :: migration_info()
  def create_type do
    PgTypeMigration.create_type(:rational,
      numerator: :bigint,
      denominator: :bigint
    )
  end

  @doc section: :migrations_types
  @doc """
  Creates function schema as described by the
  [Configuring Database Objects](Vtc.Ecto.Postgres.PgRational.Migrations.html#create_all/0-configuring-database-objects)
  section above.
  """
  @spec create_function_schemas() :: migration_info()
  def create_function_schemas, do: PgTypeMigration.create_type_schema(:rational)

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__simplify(rat)` function that simplifies a rational. Used at
  the end of every rational operation to avoid overflows.
  """
  @spec create_func_simplify() :: migration_info()
  def create_func_simplify do
    PgTypeMigration.create_plpgsql_function(
      private_function(:simplify, Migration.repo()),
      args: [input: :rational],
      returns: :rational,
      declares: [
        greatest_denom: {:bigint, "GCD(input.numerator, input.denominator)"},
        denominator: {:bigint, "ABS(input.denominator / greatest_denom)"},
        numerator: {:bigint, "input.numerator / greatest_denom"}
      ],
      body: """
      numerator := numerator * SIGN((input).denominator);
      RETURN (numerator, denominator);
      """
    )
  end

  @doc section: :migrations_functions
  @doc """
  Creates `rational.__private__minus(rat)` function that flips the sign of the input
  value -- makes a positive value negative and a negative value positive.
  """
  @spec create_func_minus() :: migration_info()
  def create_func_minus do
    PgTypeMigration.create_plpgsql_function(
      private_function(:minus, Migration.repo()),
      args: [input: :rational],
      returns: :rational,
      body: """
      RETURN ((input).numerator * -1, (input).denominator);
      """
    )
  end

  @doc section: :migrations_functions
  @doc """
  Creates `ABS(rational)` function that returns the absolute value of the rational
  value.
  """
  @spec create_func_abs() :: migration_info()
  def create_func_abs do
    PgTypeMigration.create_plpgsql_function(
      "ABS",
      args: [input: :rational],
      returns: :rational,
      body: """
      RETURN (ABS((input).numerator), ABS((input).denominator));
      """
    )
  end

  @doc section: :migrations_functions
  @doc """
  Creates `ABS(rational)` function that returns the absolute value of the rational
  value.
  """
  @spec create_func_sign() :: migration_info()
  def create_func_sign do
    PgTypeMigration.create_plpgsql_function(
      "SIGN",
      args: [input: :rational],
      returns: :integer,
      body: """
      RETURN SIGN((input).numerator * (input).denominator);
      """
    )
  end

  @doc section: :migrations_functions
  @doc """
  Creates `ROUND(rational)` function that returns the rational input, rounded to the
  nearest :bigint.
  """
  @spec create_func_round() :: migration_info()
  def create_func_round do
    PgTypeMigration.create_plpgsql_function(
      "ROUND",
      args: [input: :rational],
      returns: :bigint,
      body: """
      CASE
        WHEN (input).numerator < 0 THEN
          input := -input;
          RETURN ROUND(input) * -1;
        WHEN (((input).numerator % (input).denominator) * 2) < (input).denominator THEN
          RETURN (input).numerator / (input).denominator;
        ELSE
          RETURN ((input).numerator / (input).denominator) + 1;
      END CASE;
      """
    )
  end

  @doc section: :migrations_functions
  @doc """
  Creates `FLOOR(rational)` function that returns the rational input as a `bigint`,
  rounded towards zero, to match Postgres `FLOOR(real)` behavior.
  """
  @spec create_func_floor() :: migration_info()
  def create_func_floor do
    PgTypeMigration.create_plpgsql_function(
      "FLOOR",
      args: [input: :rational],
      returns: :bigint,
      body: """
      RETURN ((input).numerator / (input).denominator);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates a native CAST from `rational` to `double precision`.
  """
  @spec create_func_cast_to_double_precision() :: migration_info()
  def create_func_cast_to_double_precision do
    PgTypeMigration.create_plpgsql_function(
      private_function(:cast_to_double, Migration.repo()),
      args: [value: :rational],
      returns: :"double precision",
      body: """
      RETURN CAST ((value).numerator AS double precision) / CAST ((value).denominator AS double precision);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates a native CAST from `bigint` to `rational`.
  """
  @spec create_func_cast_bigint_to_rational() :: migration_info()
  def create_func_cast_bigint_to_rational do
    PgTypeMigration.create_plpgsql_function(
      private_function(:cast_bigint_to_rational, Migration.repo()),
      args: [value: :bigint],
      returns: :rational,
      body: """
      RETURN (value, 1)::rational;
      """
    )
  end

  ## ARITHMETIC BACKING FUNCS

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__add(a, b)` backing function for the `+` operator
  between two rationals.
  """
  @spec create_func_add() :: migration_info()
  def create_func_add do
    PgTypeMigration.create_plpgsql_function(
      private_function(:add, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: [
        numerator: {:bigint, "((a).numerator * (b).denominator) + ((b).numerator * (a).denominator)"},
        denominator: {:bigint, "(a).denominator * (b).denominator"},
        greatest_denom: {:bigint, "GCD(numerator, denominator)"}
      ],
      returns: :rational,
      body: """
      #{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}

      RETURN (numerator, denominator);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__sub(a, b)` backing function for the `-` operator
  between two rationals.
  """
  @spec create_func_sub() :: migration_info()
  def create_func_sub do
    PgTypeMigration.create_plpgsql_function(
      private_function(:sub, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: [
        numerator: {:bigint, "((a).numerator * (b).denominator) - ((b).numerator * (a).denominator)"},
        denominator: {:bigint, "(a).denominator * (b).denominator"},
        greatest_denom: {:bigint, "GCD(numerator, denominator)"}
      ],
      returns: :rational,
      body: """
      #{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}

      RETURN (numerator, denominator);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__mult(a, b)` backing function for the `*` operator
  between two rationals.
  """
  @spec create_func_mult() :: migration_info()
  def create_func_mult do
    PgTypeMigration.create_plpgsql_function(
      private_function(:mult, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: [
        numerator: {:bigint, "(a).numerator * (b).numerator"},
        denominator: {:bigint, "(a).denominator * (b).denominator"},
        greatest_denom: {:bigint, "GCD(numerator, denominator)"}
      ],
      returns: :rational,
      body: """
      #{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}

      RETURN (numerator, denominator);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__div(a, b)` backing function for the `/` operator
  between two rationals.
  """
  @spec create_func_div() :: migration_info()
  def create_func_div do
    PgTypeMigration.create_plpgsql_function(
      private_function(:div, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: [
        numerator: {:bigint, "(a).numerator * (b).denominator"},
        denominator: {:bigint, "(a).denominator * (b).numerator"},
        greatest_denom: {:bigint, "GCD(numerator, denominator)"}
      ],
      returns: :rational,
      body: """
      #{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}

      RETURN (numerator, denominator);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `DIV(a, b)` function, which executed integer floor division on rational
  values.

  Just like `DIV(real, real)`, `DIV(rational, rational)` floors towards zero.
  """
  @spec create_func_floor_div() :: migration_info()
  def create_func_floor_div do
    PgTypeMigration.create_plpgsql_function(
      "DIV",
      args: [a: :rational, b: :rational],
      declares: [
        numerator: {:bigint, "(a).numerator * (b).denominator"},
        denominator: {:bigint, "(a).denominator * (b).numerator"}
      ],
      returns: :bigint,
      body: """
      RETURN numerator / denominator;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__modulo(a, b)` backing function for the `%` operator
  between two rationals.
  """
  @spec create_func_modulo() :: migration_info()
  def create_func_modulo do
    PgTypeMigration.create_plpgsql_function(
      private_function(:modulo, Migration.repo()),
      args: [dividend: :rational, divisor: :rational],
      declares: [
        numerator:
          {:bigint,
           """
           ((dividend).numerator * (divisor).denominator)
           % ((divisor).numerator * (dividend).denominator)
           """},
        denominator: {:bigint, "(dividend).denominator * (divisor).denominator"},
        greatest_denom: {:bigint, "GCD(numerator, denominator)"}
      ],
      returns: :rational,
      body: """
      #{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}

      RETURN (numerator, denominator);
      """
    )
  end

  ## COMPARISON BACKING FUNCS

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__cmp(a, b)` that returns:

    - `1` if a > b
    - `0` if a == b
    - `-1` if a < b

  Used to back equality operators.
  """
  @spec create_func_cmp() :: migration_info()
  def create_func_cmp do
    PgTypeMigration.create_plpgsql_function(
      private_function(:cmp, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: [
        a_cmp: {:bigint, "((a).numerator * (b).denominator)"},
        b_cmp: {:bigint, "((b).numerator * (a).denominator)"}
      ],
      returns: :integer,
      body: """
      RETURN SIGN(a_cmp - b_cmp);
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__eq(a, b)` that backs the `=` operator.
  """
  @spec create_func_eq() :: migration_info()
  def create_func_eq do
    PgTypeMigration.create_plpgsql_function(
      private_function(:eq, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign = 0;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__neq(a, b)` that backs the `<>` operator.
  """
  @spec create_func_neq() :: migration_info()
  def create_func_neq do
    PgTypeMigration.create_plpgsql_function(
      private_function(:neq, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign != 0;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__lt(a, b)` that backs the `<` operator.
  """
  @spec create_func_lt() :: migration_info()
  def create_func_lt do
    PgTypeMigration.create_plpgsql_function(
      private_function(:lt, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign = -1;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__lte(a, b)` that backs the `<=` operator.
  """
  @spec create_func_lte() :: migration_info()
  def create_func_lte do
    PgTypeMigration.create_plpgsql_function(
      private_function(:lte, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign = -1 or cmp_sign = 0;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__gt(a, b)` that backs the `>` operator.
  """
  @spec create_func_gt() :: migration_info()
  def create_func_gt do
    PgTypeMigration.create_plpgsql_function(
      private_function(:gt, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign = 1;
      """
    )
  end

  @doc section: :migrations_private_functions
  @doc """
  Creates `rational.__private__gte(a, b)` that backs the `>=` operator.
  """
  @spec create_func_gte() :: migration_info()
  def create_func_gte do
    PgTypeMigration.create_plpgsql_function(
      private_function(:gte, Migration.repo()),
      args: [a: :rational, b: :rational],
      declares: compare_declarations(),
      returns: :boolean,
      body: """
      RETURN cmp_sign = 1 or cmp_sign = 0;
      """
    )
  end

  ## ARITHMETIC OPS

  @doc section: :migrations_operators
  @doc """
  Creates a custom unary :rational `@` unary operator.

  Returns the absolute value of the input.
  """
  @spec create_op_abs() :: migration_info()
  def create_op_abs do
    PgTypeMigration.create_operator(
      :@,
      nil,
      :rational,
      "ABS"
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom unary :rational `-` operator.

  Flips the sign of `value`. Equivalent to `value * -1`.
  """
  @spec create_op_minus() :: migration_info()
  def create_op_minus do
    PgTypeMigration.create_operator(
      :-,
      nil,
      :rational,
      private_function(:minus, Migration.repo())
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `+` operator.
  """
  @spec create_op_add() :: migration_info()
  def create_op_add do
    PgTypeMigration.create_operator(
      :+,
      :rational,
      :rational,
      private_function(:add, Migration.repo()),
      commutator: :+
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `-` operator.
  """
  @spec create_op_sub() :: migration_info()
  def create_op_sub do
    PgTypeMigration.create_operator(
      :-,
      :rational,
      :rational,
      private_function(:sub, Migration.repo())
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `*` operator.
  """
  @spec create_op_mult() :: migration_info()
  def create_op_mult do
    PgTypeMigration.create_operator(
      :*,
      :rational,
      :rational,
      private_function(:mult, Migration.repo()),
      commutator: :*
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `/` operator.
  """
  @spec create_op_div() :: migration_info()
  def create_op_div do
    PgTypeMigration.create_operator(
      :/,
      :rational,
      :rational,
      private_function(:div, Migration.repo())
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `%` operator.
  """
  @spec create_op_modulo() :: migration_info()
  def create_op_modulo do
    PgTypeMigration.create_operator(
      :%,
      :rational,
      :rational,
      private_function(:modulo, Migration.repo())
    )
  end

  ## COMPARISON OPS

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `=` operator.
  """
  @spec create_op_eq() :: migration_info()
  def create_op_eq do
    PgTypeMigration.create_operator(
      :=,
      :rational,
      :rational,
      private_function(:eq, Migration.repo()),
      commutator: :=,
      negator: :<>
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `<>` operator.
  """
  @spec create_op_neq() :: migration_info()
  def create_op_neq do
    PgTypeMigration.create_operator(
      :<>,
      :rational,
      :rational,
      private_function(:neq, Migration.repo()),
      commutator: :<>,
      negator: :=
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `<` operator.
  """
  @spec create_op_lt() :: migration_info()
  def create_op_lt do
    PgTypeMigration.create_operator(
      :<,
      :rational,
      :rational,
      private_function(:lt, Migration.repo()),
      commutator: :>,
      negator: :>=
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `<` operator.
  """
  @spec create_op_lte() :: migration_info()
  def create_op_lte do
    PgTypeMigration.create_operator(
      :<=,
      :rational,
      :rational,
      private_function(:lte, Migration.repo()),
      commutator: :>=,
      negator: :>
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `<` operator.
  """
  @spec create_op_gt() :: migration_info()
  def create_op_gt do
    PgTypeMigration.create_operator(
      :>,
      :rational,
      :rational,
      private_function(:gt, Migration.repo()),
      commutator: :<,
      negator: :<=
    )
  end

  @doc section: :migrations_operators
  @doc """
  Creates a custom :rational, :rational `<` operator.
  """
  @spec create_op_gte() :: migration_info()
  def create_op_gte do
    PgTypeMigration.create_operator(
      :>=,
      :rational,
      :rational,
      private_function(:gte, Migration.repo()),
      commutator: :<=,
      negator: :<
    )
  end

  ## OPERATOR CLASSES

  @doc section: :migrations_operator_classes
  @doc """
  Creates a B-tree operator class to support indexing on comparison operations.
  """
  @spec create_op_class_btree() :: migration_info()
  def create_op_class_btree do
    PgTypeMigration.create_operator_class(
      :rational_ops_btree,
      :rational,
      :btree,
      [
        <: 1,
        <=: 2,
        =: 3,
        >=: 4,
        >: 5
      ],
      [
        {private_function(:cmp, Migration.repo()), 1}
      ]
    )
  end

  ## CASTS

  @doc section: :migrations_casts
  @doc """
  Creates a native cast for:

  ```sql
  rational AS double precision
  ```
  """
  @spec create_cast_double_precision() :: migration_info()
  def create_cast_double_precision do
    PgTypeMigration.create_cast(
      :rational,
      :"double precision",
      private_function(:cast_to_double, Migration.repo())
    )
  end

  @doc section: :migrations_casts
  @doc """
  Creates a native cast for:

  ```sql
  bigint AS rational
  ```
  """
  @spec create_cast_bigint_to_rational() :: migration_info()
  def create_cast_bigint_to_rational do
    PgTypeMigration.create_cast(
      :bigint,
      :rational,
      private_function(:cast_bigint_to_rational, Migration.repo()),
      implicit: true
    )
  end

  @doc section: :migrations_constraints
  @doc """
  Creates basic constraints for a `PgRational` database field.

  ## Constraints created:

  - `{field_name}_denominator_positive`: Checks that the denominator of the field is
    positive.

  ## Examples

  ```elixir
  create table("rationals", primary_key: false) do
    add(:id, :uuid, primary_key: true, null: false)
    add(:a, PgRational.type())
    add(:b, PgRational.type())
  end

  PgRational.migration_add_field_constraints(:rationals, :a)
  PgRational.migration_add_field_constraints(:rationals, :b)
  ```
  """
  @spec create_constraints(atom(), atom()) :: :ok
  def create_constraints(table, field_name) do
    sql_field = "#{table}.#{field_name}"

    constraint =
      Migration.constraint(
        table,
        "#{field_name}_denominator_positive",
        check: """
        (#{sql_field}).denominator > 0
        """
      )

    %Constraint{} = Migration.create(constraint)

    :ok
  end

  # Returns declaration list for comparison operators.
  @spec compare_declarations() :: PgTypeMigration.function_declarations()
  defp compare_declarations do
    [
      a_cmp: {:bigint, "((a).numerator * (b).denominator)"},
      b_cmp: {:bigint, "((b).numerator * (a).denominator)"},
      cmp_sign: {:bigint, "SIGN(a_cmp - b_cmp)"}
    ]
  end
end