Skip to content
This repository has been archived by the owner on Oct 17, 2022. It is now read-only.

spawnfest/lively

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lively Demo

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

Lively - Ecto utilities for Livebook

For a better experience, see this Readme using Livebook

Lively 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.

Existing components:

  • Lively.EntityRelationship: Visualize your Ecto.Schema as an ER diagram
  • Lively.Explain: Visualize the query plan for an Ecto.Query.
  • Lively.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.
  • Lively.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 %Lively.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

%Lively.EntityRelationship{schema: Person}

Explain

With a given plan, you can draw a graph to better visualize the result of your query analysis using Lively.Explain.new(plan).

You can also use Lively.Explain.call(MyApp.Repo, :all, my_ecto_query) so it's easier to run the plan and output the graph in one go.

Note: Only Postgres is supported at the moment

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" => []
  }
]

Lively.Explain.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
%Lively.ChangesetValidator{fun: &User.new_user_changeset/2, attrs: ["John Doe", 13]}
%Lively.ChangesetValidator{
  fun: &User.update_address/2,
  attrs: [%User{name: "Jane Doe"}, "Street"]
}
%Lively.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

%Lively.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 Lively.

About

No description or website provided.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages