diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json index 64b3c9e..aef798f 100644 --- a/.obsidian/workspace.json +++ b/.obsidian/workspace.json @@ -4,16 +4,16 @@ "type": "split", "children": [ { - "id": "a470dcfe9214cd52", + "id": "d16dcbc379f22118", "type": "tabs", "children": [ { - "id": "73b3910cd9e889d3", + "id": "7212df225ebf759a", "type": "leaf", "state": { "type": "markdown", "state": { - "file": "content/tils/asdf_direnv.md", + "file": "content/tils/always_generated_ecto_boolean.md", "mode": "source", "source": false } @@ -85,7 +85,7 @@ "state": { "type": "backlink", "state": { - "file": "content/tils/asdf_direnv.md", + "file": "content/tils/always_generated_ecto_boolean.md", "collapseAll": false, "extraContext": false, "sortOrder": "alphabetical", @@ -112,7 +112,7 @@ "state": { "type": "file-properties", "state": { - "file": "content/tils/asdf_direnv.md" + "file": "content/tils/always_generated_ecto_boolean.md" } } } @@ -132,10 +132,12 @@ "markdown-importer:Open format converter": false } }, - "active": "7f987208866df706", + "active": "7212df225ebf759a", "lastOpenFiles": [ "content/tils/struct_pattern_matching.md", + "content/tils/pdf_obsidian_at_page.md", "content/tils/asdf_direnv.md", + "content/tils/always_generated_ecto_boolean.md", "_site/tils/asdf_direnv/index.html", "_site/tils/asdf_direnv", "_site/tils/Untitled/index.html", @@ -168,7 +170,6 @@ "node_modules/markdown-it/node_modules/entities/readme.md", "node_modules/liquidjs/node_modules/commander/Readme.md", "node_modules/minipass/node_modules/yallist/README.md", - "node_modules/minipass/README.md", "_site/images/ultimate-dungeon-terrain-3.jpg", "_site/images/ultimate-dungeon-terrain-2.jpg", "_site/images/ultimate-dungeon-terrain-1.jpg", diff --git a/content/tils/always_generated_ecto_boolean.md b/content/tils/always_generated_ecto_boolean.md new file mode 100644 index 0000000..dd684c9 --- /dev/null +++ b/content/tils/always_generated_ecto_boolean.md @@ -0,0 +1,45 @@ +--- +title: Always Generated Boolean tied to a date, smart deleted states in sql +date: 2023-09-25 +--- +It's a common scenario to not only know that something happened, but also _when_ it happened. Such is the common case with something like the following example of marking a user as `deleted?: true`. But, rather than relying on keeping two attributes up to date (`deleted` and `deleted_at`) we can rely on the database to have an automatically generated boolean to handle that for us. + +The following example implements a reversable database migration, field, and function used to mark a user as deleted (at the current timestamp) and undeleted (or reactivated). With all three implemented we'll be able to tell if a user is deleted `user.deleted? == true` and toggle a user's deletion `delete_user(current_user)` and `undelete_user(deleted_user)`. + +```elixir +# mix ecto.gen.migration add_deleted_to_user +defmodule App.Repo.Migrations.AddDeletedToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add(:deleted_at, :naive_datetime) + end + + execute( + "alter table users add column deleted boolean generated always as (deleted_at is null) stored", + "alter table users drop column deleted" + ) + end +end +``` + +```elixir +# App.Accounts.User +field(:deleted?, :boolean, read_after_writes: true, source: :deleted) +field(:deleted_at, :naive_datetime) +``` + +```elixir + def delete_user(%User{} = user), + do: + user + |> User.changeset(%{deleted_at: NaiveDateTime.utc_now()}) + |> Repo.update() + + def undelete_user(%User{} = user), + do: + user + |> User.changeset(%{deleted_at: nil}) + |> Repo.update() +``` \ No newline at end of file