Skip to content

vorce/kino_ecto

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

build

KinoEcto - Ecto utilities for Livebook

For a better experience, see this Readme using Livebook.

KinoEcto is a collection of utilities for working with Ecto and Livebook to help developers learn more about Ecto but also as a way to augment project documentation for better onboarding, knowledge sharing, expectation setting, etc.

KinoEcto started as a hackathon project called Lively for Spawnfest 2022.

Mix.install(
  [
    {:ecto, "~> 3.8"},
    {:kino_ecto, git: "https://github.com/vorce/kino_ecto"}
  ],
  force: true
)

Existing components

  • KinoEcto.EntityRelationship: Visualize your Ecto.Schema as an ER diagram
  • KinoEcto.Explain: Visualize the query plan for an Ecto.Query.
  • KinoEcto.ChangesetValidator: Visualize an Ecto.Changeset by passing a changeset function and its attributes. The next step here would be having a SmartCell rendered for changing attributes and a better looking result of changeset evaluation.
  • KinoEcto.QueryBuilder: Build and return an Ecto.Query from the SQL query passed in the params.

Note: All the code is WIP and should be seen as such

Entity Relationship Diagram

Using the %KinoEcto.EntityRelantionship{schema: Module} you will be able to visualize a Entity Relationship Diagram and connected entities based on your Ecto.Schema.

Example:

defmodule Organization do
  use Ecto.Schema

  schema "organizations" do
    field(:name, :string)
    has_one(:team, Team)
  end
end

defmodule Team do
  use Ecto.Schema

  schema "teams" do
    field(:name, :string)
    has_many(:persons, Person)
  end
end

defmodule Person do
  use Ecto.Schema

  schema "persons" do
    field(:name, :string)
    field(:age, :integer)

    has_one(:team, Team)
    belongs_to(:organization, Organization)
  end
end

%KinoEcto.EntityRelationship{schema: Person}

Explain

To run explain on a query and output the graph in one go you can use KinoEcto.Explain.call(MyApp.Repo, :all, my_ecto_query).

If you already have a plan you can use it to draw a graph directly with: KinoEcto.Explain.Postgres.new(plan).

Note: Only Postgres is supported at the moment (myxql support issue)

Example

plan = [
  %{
    "Plan" => %{
      "Actual Loops" => 1,
      "Actual Rows" => 4,
      "Actual Startup Time" => 0.139,
      "Actual Total Time" => 0.231,
      "Node Type" => "Result",
      "Plan Rows" => 7,
      "Plan Width" => 405,
      "Plans" => [
        %{
          "Actual Loops" => 1,
          "Actual Rows" => 4,
          "Actual Startup Time" => 0.131,
          "Actual Total Time" => 0.218,
          "Node Type" => "Append",
          "Parent Relationship" => "Outer",
          "Plan Rows" => 7,
          "Plan Width" => 405,
          "Plans" => [
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.009,
              "Actual Total Time" => 0.009,
              "Alias" => "paris",
              "Filter" => "(ar_num = 8)",
              "Index Cond" => "(tags ? 'tourism'::text)",
              "Index Name" => "idx_paris_tags",
              "Node Type" => "Index Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 450,
              "Relation Name" => "paris",
              "Scan Direction" => "NoMovement",
              "Startup Cost" => 0.0,
              "Total Cost" => 8.27
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.001,
              "Actual Total Time" => 0.001,
              "Alias" => "paris",
              "Filter" => "((tags ? 'tourism'::text) AND (ar_num = 8))",
              "Node Type" => "Seq Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 450,
              "Relation Name" => "paris_linestrings",
              "Startup Cost" => 0.0,
              "Total Cost" => 11.8
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.003,
              "Actual Total Time" => 0.003,
              "Alias" => "paris",
              "Filter" => "(ar_num = 8)",
              "Index Cond" => "(tags ? 'tourism'::text)",
              "Index Name" => "idx_paris_points_tags",
              "Node Type" => "Index Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 450,
              "Relation Name" => "paris_points",
              "Scan Direction" => "NoMovement",
              "Startup Cost" => 0.0,
              "Total Cost" => 8.27
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.002,
              "Actual Total Time" => 0.002,
              "Alias" => "paris",
              "Filter" => "(ar_num = 8)",
              "Index Cond" => "(tags ? 'tourism'::text)",
              "Index Name" => "idx_paris_polygons_tags",
              "Node Type" => "Index Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 450,
              "Relation Name" => "paris_polygons",
              "Scan Direction" => "NoMovement",
              "Startup Cost" => 0.0,
              "Total Cost" => 8.27
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.103,
              "Actual Total Time" => 0.103,
              "Alias" => "paris",
              "Filter" => "((tags ? 'tourism'::text) AND (ar_num = 8))",
              "Node Type" => "Seq Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 513,
              "Relation Name" => "paris_linestrings_ar_08",
              "Startup Cost" => 0.0,
              "Total Cost" => 7.27
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 4,
              "Actual Startup Time" => 0.009,
              "Actual Total Time" => 0.085,
              "Alias" => "paris",
              "Filter" => "((tags ? 'tourism'::text) AND (ar_num = 8))",
              "Node Type" => "Seq Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 72,
              "Relation Name" => "paris_points_ar_08",
              "Startup Cost" => 0.0,
              "Total Cost" => 5.16
            },
            %{
              "Actual Loops" => 1,
              "Actual Rows" => 0,
              "Actual Startup Time" => 0.007,
              "Actual Total Time" => 0.007,
              "Alias" => "paris",
              "Filter" => "((tags ? 'tourism'::text) AND (ar_num = 8))",
              "Node Type" => "Seq Scan",
              "Parent Relationship" => "Member",
              "Plan Rows" => 1,
              "Plan Width" => 450,
              "Relation Name" => "paris_polygons_ar_08",
              "Startup Cost" => 0.0,
              "Total Cost" => 1.08
            }
          ],
          "Startup Cost" => 0.0,
          "Total Cost" => 50.11
        }
      ],
      "Startup Cost" => 0.0,
      "Total Cost" => 50.13
    },
    "Execution Time" => 1.238,
    "Planning Time" => 0.92,
    "Triggers" => []
  }
]

KinoEcto.Explain.Postgres.new(plan)

Changeset Validator

A simpler way to visualize errors of applying changes to an entity using the changeset results. To use it you just need to create a struct %ChangesetValidator{fun: Module.changeset_func/arity, attrs: attrs}

Example

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field(:name, :string)
    field(:age, :integer)
    field(:address, :string)
  end

  def new_user_changeset(name, age) do
    params = %{name: name, age: age}

    %__MODULE__{}
    |> cast(params, [:name, :age])
    |> validate_required([:name])
    |> validate_inclusion(:age, 18..100)
  end

  def update_address(user, address) do
    user
    |> cast(%{address: address}, [:address])
    |> validate_address()
  end

  defp validate_address(changeset) do
    Ecto.Changeset.add_error(changeset, :address, "Bad address", validation: :address_validation)
  end
end
%KinoEcto.ChangesetValidator{fun: &User.new_user_changeset/2, attrs: ["John Doe", 13]}
%KinoEcto.ChangesetValidator{
  fun: &User.update_address/2,
  attrs: [%User{name: "Jane Doe"}, "Street"]
}
%KinoEcto.ChangesetValidator{fun: &User.new_user_changeset/2, attrs: ["John Doe", 18]}

Generate Ecto Query from SQL

If a developer is more accustomed to SQL and not Ecto.Query, this tool can guide them on the construction of an Ecto.Query that replicated the SQL they expected.

Example

%KinoEcto.QueryBuilder{sql_query: "SELECT * FROM users WHERE name = 'John'"}

Future Work

Other than a lot of bug fixing, in the future, we want to improve the code we have so we can build a more solid and faster toolset that can enable users to have more information about their systems.

For now, we're checking what we can do with Ecto since it's a common library but we already thought about future work such as:

  • JSON visualizer
  • Oban tools
  • Ecto migration tracker (what and when was a given schema changed)

This can be just the beginning of KinoEcto.

About

Ecto utilities with Kino for Livebook

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages