Today I Learned

A Hashrocket project

Ready to join Hashrocket? Find Openings here and apply today.

145 posts by viniciusnegrisolo @vfnegrisolo

Disable capture in Elixir Regex

Today I came across a regex that had to use the ( parenthesis to match a sequence of chars but I wanted just to match them, so I’d love if I could ignore that piece from the “capture” part.

So I learned that we could use ?: in the beginning of the ( and that would turn the capture off for that piece. Check this out;

iex> Regex.scan(~r/aa(bb)(?:cc|CC)/, "aabbccdd aabbCCdd")
[["aabbcc", "bb"], ["aabbCC", "bb"]]

In this case I am matching and capturing the bb sequence and I am matching but not capturing the cc|CC sequence options.

When to set `inverse_of` in Rails AR

Rails ActiveRecord does not auto infer bi-directional associations if some of the associations contains a scope or any of the following through or foreign_key options.

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
irb> a = Author.first
irb> b = a.books.first
irb> a.first_name = 'David'
irb> a.id = b.writer.id
=> true
irb> a.object_id = b.writer.object_id
=> false
irb> a.first_name == b.writer.first_name
=> false

To solve this issue we should use inverse_of option, check this out:

class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end

class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
irb> a = Author.first
irb> b = a.books.first
irb> a.first_name = 'David'
irb> a.id = b.writer.id
=> true
irb> a.object_id = b.writer.object_id
=> true
irb> a.first_name == b.writer.first_name
=> true

Github Resolving Git Conflicts

Today I tried out Github to Resolve Git Conflicts of an open PR. I noticed that Github has this gray button Resolve conflicts:

image

Then it drives me to a managing conflicts page with a simple editor where I could manually choose and edit the conflicts:

image

After my editing is done I can Mark as resolved:

image

And finally I can Commit merge:

image

Wait, what? oh no, I know that merging is the easiest way to solve conflicts as you solve all conflicts once, no matter how many commits your branch has, but to be honest I did not like that. I put some small effort to keep my git history clean and I really avoid merge commits as they are not necessary in general. I tend to keep a single history line on my git repos in the main branch, most of the times at least.

I guess that it is what it is, every time we use a tool to “simplify” the work we are giving up a bit of control on how the things are executed, right?

So here’s the resolve of Github’s solving git conflicts flow:

image

Elixir Supervisor default child_spec

When using Supervisor elixir already declares a child_spec/1 for us with the right type: :supervisor. The generated code will be something like:

defmodule MyApp.MySupervisor do
  use Supervisor, opts
  
  ...

  def child_spec(init_arg) do
    default = %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [init_arg]},
      type: :supervisor
    }

    Supervisor.child_spec(default, unquote(Macro.escape(opts)))
  end
end

Keep in mind that’s a pseudo-code as I left the Macro.escape for better understanding of the “expanded” code that happens on the __using__/1 function.

I tried to get this info from the docs, which I could not find that easily so I dig into the code, and as a good surprise this was so easy to find. Check this out.

By the way, the generated code is very similar to the GenServer one for the same function, the only difference is that on the GenServer there’s no :type key, and it works fine because the default value for the :type key is :worker.

Using `@describetag` in Elixir Tests

Wow! ExUnit have some special tags for @moduletag and @describetag which are self explanatory if you know what tags are used for in ExUnit. They are so useful in case you want to, for example, only run that section of tests:

defmodule MyApp.AccountsTest do
  use MyApp.DataCase

  @moduletag :module_tag

  describe "search" do
    @describetag :describe_tag

    @tag :test_tag
    test "return all users" do
      user_1 = insert!(User)
      user_2 = insert!(User)

      assert Accounts.search(User) == [user_1, user_2]
    end
    ...
  end
  ...
end

In case you want to run only tests that match with a module (file) level @moduletag:

my_app git:(main) ✗ mix test --only my_module_tag
Excluding tags: [:test]
Including tags: [:my_module_tag]
............
Finished in 0.2 seconds
22 tests, 0 failures, 10 excluded

Or if we want to run all tests that match with a describe block tagged with @describetag:

my_app git:(main) ✗ mix test --only my_describe_tag
Excluding tags: [:test]
Including tags: [:my_describe_tag]
....
Finished in 0.2 seconds
22 tests, 0 failures, 18 excluded

We can see by the number of excluded tests that this works great as expected.

React SyntheticEvent for Focus and Blur

Some JavaScript events like focus and blur do not bubble to parent elements. Let’s say that we have:

<div
  onfocus="console.log('div focus')"
  onblur="console.log('div blur')"
>
  <input
    type="text"
    placeholder="Type something here"
    onfocus="console.log('input focus')"
    onblur="console.log('input blur')"
  >
</div>

Then if we focus we’ll see in the console input focus and if we blur we’ll see input blur. This makes a lot of sense as a div does not have the concept to focus, there’s no blinking cursor or anything like that.

But in React the scenario is different. In an attempt to standardize all browser events to behave similarly to each other and across different browsers, React wraps all events in SyntheticEvents and for the onFocus and onBlur events we’ll see that they do bubble up:

<div
  onFocus={() => console.log('div focus')}
  onBlur={() => console.log('div blur')}
>
  <input
    type="text"
    placeholder="Type something here"
    onFocus={() => console.log('input focus')}
    onBlur={() => console.log('input blur')}
  />
</div>

Here if we focus we’ll see input focus and div focus in this order and if we blur we’ll see input blur and div blur in this order again.

Use reduced motion to control a video

I used my useWatchMedia hook to play/pause a video based on the reduced motion OS config.

On Macs you can set this option in System Preferences => Accessibility => Display:

image

This way users with motion sickness can have better time browsing your website.

And here’s my video component:

const useReducedMotion = () => {
  return useWatchMedia('(prefers-reduced-motion: reduce)');
};
const MyVideo = ({url}) => {
  const ref = useRef();
  const reducedMotion = useReducedMotion();

  useLayoutEffect(() => {
    if (ref.current) {
      reducedMotion ? ref.current.pause() : ref.current.play();
    }
  }, [ref, reducedMotion]);

  return (
    <video autoPlay loop muted playsInline ref={ref} src={url} />
  );
};

As the browser exposes this config via media query we could also use regular css for that media to enable/disable features, for example css animations.

Watch Media query changes using React hooks

I end up creating a React hook to watch a media query change. I am using window.matchMedia to achieve that. Check this out:

const useWatchMedia = (media) => {
  const [matches, setMatches] = useState();

  useEffect(() => {
    const watchedMedia = window.matchMedia(media);
    const mediaListener = () => setMatches(watchedMedia.matches);

    mediaListener();
    watchedMedia.addListener(mediaListener);

    return () => watchedMedia.removeListener(mediaListener);
  }, [media]);

  return matches;
};

React hooks are an excellent way to share this setup (and cleanup)!

React `useImperativeHandle` hook

I end up using useImperativeHandle hook to extract a component to be reusable. In this case I am using react-google-recaptcha lib for captcha and I have to pass a ref so the lib can bind some functions to that. Let’s see how I got to expose a function to the parent component via ref:

import React, {useRef, useImperativeHandle, forwardRef} from 'react';
import ReCAPTCHA from 'react-google-recaptcha';

const Captcha = (_props, ref) => {
  const captchaRef = useRef();

  useImperativeHandle(ref, () => ({
    executeAsync: () => {
      captchaRef.current.executeAsync();
    },
  }));

  return (
    <ReCAPTCHA ref={captchaRef} sitekey="some key here" />
  );
};

export default forwardRef(Captcha);

I had to use both useImperativeHandle and forwardRef in order to receive the ref from the parent and “forward” to the ref internal to this component.

And here’s how I am calling that:

const MyForm = () => {
  const captchaRef = useRef();

  const onSubmit = async (values) => {
    const recaptchaToken = await captchaRef.current?.executeAsync();
    placeOrder({recaptchaToken, ...values});
  };

  ...

  return (
    <form onSubmit={onSubmit}>
      ...
      <Captcha ref={captchaRef} />
      <input type="submit" value="Save" />
    </form>
  );
};

Although this is a very unusual thing to do in React, it was necessary in order to extract this component.

Count Occurrences in Elixir

Elixir recently introduced an useful couple of functions to count how many times a value appears in an Enumerable. It comes in two formats: frequencies/1 and frequencies_by/2. Here’s an example:

iex> [
...>   %{name: "Falcon", power: "Flight"},
...>   %{name: "Titan Spirit", power: "Flight"},
...>   %{name: "Atom Claw", power: "Strength"},
...>   %{name: "Electro", power: "Electricity Control"},
...>   %{name: "Loki Brain", power: "Telekinesis"},
...> ] |> Enum.frequencies_by(& &1.power)
%{
  "Electricity Control" => 1,
  "Flight" => 2,
  "Strength" => 1,
  "Telekinesis" => 1
}

Elixir Date and Time conversion into US standards

Today I learned how to manually convert a Date and Time into US standards: mm/dd/YYYY and hh:mm am|pm. Here’s my code snippet:

defmodule Utils.Converter do
  def to_usa_date(%Date{day: day, month: month, year: year}) do
    "~2..0B/~2..0B/~4..0B"
    |> :io_lib.format([month, day, year])
    |> to_string()
  end

  def to_usa_time(%Time{} = time) do
    period = (time.hour < 12 && "am") || "pm"
    hour = time.hour |> rem(12)

    "~2..0B:~2..0B ~2..0s"
    |> :io_lib.format([hour, time.minute, period])
    |> to_string()
  end
end

This way I can convert dates like ~D[2020-05-29] into 05/29/2020" and times like ~T[11:00:07.001] into 11:00 am" and ~T[23:00:07.001] into 11:00 pm".

Here’s the erlang :io_lib documentation

Elixir Inspecting Big Structs and Lists

IO inspects limits your data by default in 50, so 50 Map keys and values, or 50 List items, etc. Sometimes you have a larger data, so you can play with this number by setting an option to the IO.inspect/2 function, or you can set it to :infinity. Use :infinity with moderation of course:

big_map = 1..100 |> Enum.map(&{ &1, "value #{&1}"}) |> Enum.into(%{})
IO.inspect(big_map)
IO.inspect(big_map, limit: 100)
IO.inspect(big_map, limit: :infinity)

Convert HTTPoison Request into curl command

Today I learned how to convert a HTTPoison Request into a curl command, useful for logs. This might be useful to someone else, so I am sharing:

defmodule MyHTTP do
  def curl(%HTTPoison.Request{} = req) do
    headers = req.headers |> Enum.map(fn {k, v} -> "-H \"#{k}: #{v}\"" end) |> Enum.join(" ")
    body = req.body && req.body != "" && "-d '#{req.body}'" || nil
    params = URI.encode_query(req.params || [])
    url = [req.url, params] |> Enum.filter(& &1 != "") |> Enum.join("?")

    [
      "curl -v -X",
      to_string(req.method),
      headers,
      body,
      url,
      ";"
    ]
    |> Enum.filter(& &1)
    |> Enum.map(&String.trim/1)
    |> Enum.filter(& &1 != "")
    |> Enum.join(" ")
  end
end

And here’s an example of usage:

MyHTTP.curl(%HTTPoison.Request{
  url: "https://til.hashrocket.com",
  params: [q: "elixir"]
})

"curl -v -X get https://til.hashrocket.com?q=elixir ;"

Ruby yield as keyword args default

Today I learned that you can use yield as a default value for a keyword arg.

def foo(bar: yield)
  "Received => #{bar}"
end

Then you can call this method using the keyword syntax:

foo(bar: "Hello world!")
#=> "Received => Hello world!"

or by using the block syntax:

foo do
  "Hello world!"
end
#=> "Received => Hello world!"

I am not sure why I’d use such flexible syntax for a single method, but we have to know what’s possibly in Ruby. Anyway, just a sanity check here, is the block evaluated if we pass the arg?:

foo(bar: "Hello") do
  puts "Block was evaluated!!!"
  "world!"
end
#=> "Received => Hello"

Cool, so ruby does not evaluate the block if this keyword is passed into, so we are cool.

Use assigned variables on its own assign statement

Javascript allows you to use an assigned variables into the same assignment statement. This is a non expected behavior for me as the first languages I’ve learned were compiled languages and that would fail compilation, at least on the ones I’ve experienced with.

Here’s an example:

Javascript ES2015:

const [hello, world] = [
  () => `Hello ${world()}`,
  () => "World",
]
hello()
//=> "Hello World"

In this example I’m assigning the second function into the world variable by using destructuring assignment and I’m using this by calling world() in the first function.

Ruby:

To my surprise ruby allows the same behavior:

hello, world = [
  lambda { "Hello #{world.call}" },
  lambda { "World" },
]
hello.call
#=> "Hello World"

Elixir:

Well, Elixir will act as I previously expected, so assigned variables are allowed to be used only after the statement that creates them.

[hello, world] = [
  fn -> "Hello #{world.()}" end,
  fn -> "World" end,
]
hello.()
#=> ERROR** (CompileError) iex:2: undefined function world/0

I won’t start using this approach, but it’s important to know that’s possible.

PostgreSQL index with NO lock on Rails

Rails allow us to create a PostgreSQL index that it would not lock the table for writing meanwhile it’s being calculated.

That’s usually handful if a table has millions of rows and this operation could take hours to release the INSERT/UPDATE/DELETE lock. Maybe the lock downtime is critical to the application.

There are caveats to this approach so please read the PostgreSQL documentation.

In order to do that you’ll need to add { algorithm: :concurrently } to the add_index on a Rails migration. Additionally PostgreSQL uses DB transactions to manage this new index creation so we need to disable the Rails migration transaction. Take this example:

class AddIndexOnEventsName < ActiveRecord::Migration[5.0]
  disable_ddl_transaction!

  def change
    add_index(:events, :name, algorithm: :concurrently)
  end
end

In this case I’m using disable_ddl_transaction! to disable the DB transaction that wraps each Rails migrations and I am using algorithm: :concurrently.

This option it will generate an index like that:

CREATE INDEX CONCURRENTLY "index_events_on_name" ON "events" ("name"));

Rails will change the `not` behavior

Yay Rails will change the behavior of the ActiveRecord#not on rails 6.1. That’s great as the current behavior sometimes leads to confusion. There will be a deprecation message on the version 6.0 so let’s watch out and change our code accordingly.

As I already wrote on this TIL this function does not act as a legit boolean algebra negation. Let’s say that we have this query:

User.where.not(active: true, admin: true).to_sql

Prior to this change this will produce this query:

SELECT users.*
  FROM users
 WHERE users.active != 't'
   AND users.admin != 't'

The problem is that negating an AND clause is naturally a NAND operator, but Rails have implemented as a NOR. Some developers might wonder why regular active users are not returning.

On Rails 6.1 this will be fixed hence the new query will be:

SELECT users.*
  FROM users
 WHERE users.active != 't'
    OR users.admin != 't'

Here’s the rollout plan:

  • Rails 5.2.3 acts as NOR
  • Rails 6.0.0 acts as NOR with a deprecation message
  • Rails 6.1.0 act as NAND

This is the PR in case you want to know more.

Javascript arguments on ES2015 Arrow functions

Javascript function arguments can be accessed by functions defined using the function keyword such as:

function logArgsES5 () {
  console.log(arguments)
}
logArgsES5('foo', 'bar')
// => Arguments(2) ["foo", "bar"]

But ES2015 Arrow functions does not bind this variable, so if you try this you will see an error:

let logArgsES2015 = () => {
  console.log(arguments)
}
logArgsES2015('foo', 'bar')
// => Uncaught ReferenceError: arguments is not defined

So if we want to have similar variable we can add an ...arguments as the function argument:

let logArgsES2015 = (...arguments) => {
  console.log(arguments)
}
logArgsES2015('foo', 'bar')
// => Array(2) ["foo", "bar"]

React Testing Library => within nested queries

Wow, React Testing Library has a within helper to get nested searches on the dom. Check this out with this example:

const DATA = {
  movies: ["The Godfather", "Pulp Fiction"],
  tv: ["Friends", "Game of Thrones"],
};

const MyContainer = () => (
  <>
    {Object.keys(DATA).map(category => (
      <MyCategory name={category} list={DATA[category]} />
    ))}
  </>
);

const MyCategory = ({name, list}) => (
  <ul data-testid={name} role="list">
    {list.map(item => <li role="listitem">{item}</li>)}
  </ul>
);

Then let’s say if we want to assert the list of movies that this MyContainer renders and in the same order as it’s been rendered we can:

...
import { render, within } from "@testing-library/react";
import MyContainer from "../MyContainer";

describe("MyContainer", () => {
  it("tests movies", () => {
    const { getByTestId, getAllByRole } = render(<MyContainer />);

    const moviesCategory = getByTestId("movies");
    const movies = within(moviesCategory).getAllByRole("listitem");

    expect(items.length).toBe(2);
    expect(items[0]).toHaveTextContent("The Godfather");
    expect(items[1]).toHaveTextContent("Pulp Fiction");
  });
});

Real code is way more complex than this example, so this within helper function turns to be very convenient.

Proposing new time on Google Calendar invitations

Today I learned that we can respond to an invitation using google calendar proposing a new time. Check this out:

First when we open an invitation on gmail I can hit the link more options:

image

This will open the event on google calendar. Then we can expand more options with the caret down and hit Propose a new time.

image

Finally choose a time and send it back to the event owner. And that’s how they will receive our request to change the event time:

image

Change PostgreSQL psql prompt colors

Today I learned how to change psql prompt to add some color and manipulate which info to show:

$ psql postgres
postgres=# \set PROMPT1 '%[%033[1;32m%]@%/ => %[%033[0m%]%'
@postgres => \l
                                           List of databases
            Name            |   Owner    | Encoding |   Collate   |    Ctype    |   Access privileges
----------------------------+------------+----------+-------------+-------------+-----------------------
 postgres                   | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0                  | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
                            |            |          |             |             | postgres=CTc/postgres
 template1                  | postgres   | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
                            |            |          |             |             | postgres=CTc/postgres
(3 rows)

image

Check this documentation if you want to know more.

Make dry run

make has an great option to validate the commands you want to run without executing them. For that just use --dry-run.

#!make
PHONY: test
test:
    echo "running something"

Then:

$ make test --dry-run
echo "running something"
$ make test
echo "running something"
running something

Thanks @DillonHafer for that tip!

How to clear a Mac terminal and its scroll-back?

Just type this: clear && printf '\e[3J'

Or even better create an alias for that, here’s mine:

alias clear='clear && printf "\e[3J"';

Here’s what I’ve learned today:

On Mac a regular clear is pretty much the same as typing Control + L on iTerm2. This clears the screen what’s good but sometimes I want to clear all the scroll-back to clean the noise and find things faster.

In order to clean the scroll-back I was performing a Command + K. This cleans the screen and the scroll-back. That’s great, except that it messes up with tmux rendering and tmux holds all the scroll-back per pane, so it won’t work at all.

So my new alias solves that as after clear the screen it also sends a terminal command to reset the scroll back through printf '\e[3J' and this keeps tmux working just fine!

Rails 6 new ActiveRecord method pick

Rails 6 will be released with a new convenient method on ActiveRecord pick. This method picks the first result value(s) from a ActiveRecord relation.

Person.all.pick(:name)
# SELECT people.name FROM people LIMIT 1
# => 'John'

Person.all.pick(:name, :email)
# SELECT people.name, people.email FROM people LIMIT 1
# => ['John', 'john@mail.com']

So pick(:name) is basically equivalent to limit(1).pluck(:name).first.

Watch out for rails inconsistencies as pick(:name) returns a single string value and pick(:name, :email) returns an array of values.

Jest toEqual on js objects

jest toEqual compares recursively all properties of object with Object.is. And in javascript an object return undefined for missing keys:

const obj = {foo: "FOO"}

obj.foo
// "FOO"

obj.bar
// undefined

So it does not make sense to compare an object against another one that has a undefined as value, so remove them all from the right side of the expectation to avoid confusion. In other words, this test will fail:

test("test fails => objects are similar", () => {
  expect({
    planet: "Mars"
  }).not.toEqual({
    planet: "Mars",
    humans: undefined
  });
});

// Expected value to not equal:
//   {"humans": undefined, "planet": "Mars"}
// Received:
//   {"planet": "Mars"}

Watch out!

Elixir sigil for DateTime with timezone

Elixir have a new sigil_U to be relased in the upcomming 1.9.0 version.

~U[2019-05-18 21:25:06.098765Z]

This new sigil creates a UTC DateTime.

Now Elixir will have all these sigils for dates & times:

Date.new(2019, 5, 18)
=> {:ok, ~D[2019-05-18]}

Time.new(23, 55, 6, 98_765)
=> {:ok, ~T[23:55:06.098765]}

NaiveDateTime.new(2019, 5, 18, 23, 55, 6, 98_765)
=> {:ok, ~N[2019-05-18 23:55:06.098765]}

DateTime.from_iso8601("2019-05-18T23:55:06.098765+02:30")
=> {:ok, ~U[2019-05-18 21:25:06.098765Z], 9000}

Install the last Elixir version with asdf

If you are using asdf for managing Elixir version through asdf-elixir then you can install the master version. This way we can use new features yet to be release as 1.9. Let’s see how:

asdf install elixir master
asdf global elixir master

Then:

elixir --version
Erlang/OTP 21 [erts-10.3.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]

Elixir 1.9.0-dev (6ac1b99) (compiled with Erlang/OTP 20)

ActiveRecord not is not boolean algebra negation

Today I learned that rails ActiveRecord not query is not to be considered a boolean algebra negative. Let’s see by an example:

User.where(name: "Jon", role: "admin")

This will produce a simple sql query:

SELECT "admins".*
FROM "admins"
WHERE "admins"."name" = $1
  AND "admins"."role" = $2
[["name", "Jon"], ["role", "admin"]]

If we get the same where clause and negate it:

User.where.not(name: "Jon", role: "admin")

Then we get:

SELECT "admins".*
FROM "admins"
WHERE "admins"."name" != $1
  AND "admins"."role" != $2
[["name", "Jon"], ["role", "admin"]]

But I was expecting a query like:

SELECT "admins".*
FROM "admins"
WHERE ("admins"."name" != $1
   OR "admins"."role" != $2)
 [["name", "Jon"], ["role", "admin"]]

So if you want to produce a query like that you’ll have to build on your own:

User.where.not(name: "Jon").or(User.where.not(role: "admin"))

Just beaware of that when using not with multiple where clauses.

Elixir ExDoc has version dropdown

ExDoc released a new version that allow developers to show a version dropdown on their documentation.

Here’s how I added to my library:

Open the mix.exs file and add javascript_config_path to the docs option on your project function.

def project do
  [
    ...
    docs: [
      main: "readme",
      extras: ~w(README.md),
      javascript_config_path: "../.doc-versions.js"
    ],
    ...
end

And on my Makefile I have this:

docs: ## Generate documentation.
docs: setup
    echo "var versionNodes = [" > .doc-versions.js
    app=`mix run -e 'IO.puts(Mix.Project.config()[:app])'`; \
    for v in $$(git tag | tail -r); do echo "{version: \"$$v\", url: \"https://hexdocs.pm/$$app/$$v/\"}," >> .doc-versions.js; done
    echo "]" >> .doc-versions.js
    mix docs

So if I run make docs this will generate or update a file .doc-versions.js from what I have on my git tag

And here is how it looks like:

image

Here’s the ExDoc changelog.

How to assert Elixir doctest raises an error

Today I learned how to assert an Elixir doctest raises an error. Check this out:

defmodule MyModule do
  @doc """
  This function raises ArgumentError.

  ## Examples

      iex> MyModule.my_func()
      ** (ArgumentError) something is wrong
  """
  def my_func() do
    raise(ArgumentError, "something is really wrong")
  end
end

The previous doctest will fail with this message:

  1) doctest MyModule.my_func/0 (1) (MyModuleTest)
     test/my_module_test.exs:3
     Doctest failed: wrong message for ArgumentError
     expected:
       "something is wrong"
     actual:
       "something is really wrong"
     code: MyModule.my_func()
     stacktrace:
       lib/my_module.ex:10: MyModule (module)

Rails protects production database

Rails has a mechanism to protect production databases to be dropped (and other destructive commands). In order to do that rails database tasks use a private database rake task check_protected_environments. Here’s a db task code sample from rails code databases.rake:

task drop: [:load_config, :check_protected_environments] do
  db_namespace["drop:_unsafe"].invoke
end

Under the hood it checks the database environment from a metadata table ar_internal_metadata created by rails on the first load schema attempt

SELECT * FROM ar_internal_metadata;
     key     |  value 
-------------+---------
 environment | staging