Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
josemrb committed Apr 2, 2018
0 parents commit b2863fd
Show file tree
Hide file tree
Showing 24 changed files with 818 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
24 changes: 24 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
ecto_label_tree-*.tar

9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2018 Jose Miguel Rivero Bruno <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# EctoLtree

A library that provides the necessary modules to support the PostgreSQL’s
`ltree` data type with Ecto.

## Quickstart

### 1. Add the package to your list of dependencies in `mix.exs`

```elixir
def deps do
[
...
{:ecto_ltree, "~> 0.1.0"}
]
end
```

### 2. Define a type module with our custom extensions

```elixir
Postgrex.Types.define(
MyApp.PostgresTypes,
[EctoLtree.Postgrex.Lquery, EctoLtree.Postgrex.Ltree] ++ Ecto.Adapters.Postgres.extensions()
)
```

### 3. Configure the Repo to use the previously defined type module

```elixir
config :my_app, MyApp.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "my_app_dev",
hostname: "localhost",
poolsize: 10,
pool: Ecto.Adapters.SQL.Sandbox,
types: MyApp.PostgresTypes
```

### 4. Add a migration to enable the `ltree` extension

```elixir
defmodule MyApp.Repo.Migrations.CreateExtensionLtree do
use Ecto.Migration

def change do
execute("CREATE EXTENSION ltree",
"DROP EXTENSION ltree")
end
end
```

### 5. Add a migration to create your table

```elixir
defmodule MyApp.Repo.Migrations.CreateItems do
use Ecto.Migration

def change do
create table(:items) do
add :path, :ltree
end

create index(:items, [:path], using: :gist)
end
end
```

### 6. Define an Ecto Schema

```elixir
defmodule MyApp.Item do
use Ecto.Schema
import Ecto.Changeset
alias EctoLtree.LabelTree, as: Ltree

schema "items" do
field :path, Ltree
end

def changeset(item, params \\ %{}) do
item
|> cast(params, [:path])
end
end
```

### 7. Usage

```elixir
iex(1)> alias MyApp.Repo
MyApp.Repo
iex(2)> alias MyApp.Item
MyApp.Item
iex(3)> import Ecto.Query
Ecto.Query
iex(4)> import EctoLtree.Functions
EctoLtree.Functions
iex(5)> Item.changeset(%Item{}, %{path:1.2.3”}) |> Repo.insert!
%MyApp.Item{
__meta__: #Ecto.Schema.Metadata<:loaded, “items”>,
id: 1,
path: %EctoLtree.LabelTree{labels: [“1”, “2”, “3”]}
}
iex(6)> from(item in Item, select: nlevel(item.path)) |> Repo.one
3
```

The documentation can be found at [hexdocs](https://hexdocs.pm/ecto_ltree).

## Copyright and License

Copyright (c) 2018 Jose Miguel Rivero Bruno

The source code is licensed under [The MIT License (MIT)](LICENSE.md)
17 changes: 17 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use Mix.Config

if Mix.env() == :test do
config :ecto_ltree, ecto_repos: [EctoLtree.TestRepo]

config :ecto_ltree, EctoLtree.TestRepo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "ecto_ltree_test",
hostname: "localhost",
poolsize: 10,
pool: Ecto.Adapters.SQL.Sandbox,
types: EctoLtree.TestTypes

config :logger, level: :warn
end
3 changes: 3 additions & 0 deletions lib/ecto_ltree.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule EctoLtree do
@moduledoc false
end
72 changes: 72 additions & 0 deletions lib/ecto_ltree/functions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule EctoLtree.Functions do
@moduledoc """
This module exposes the `ltree` functions.
For more information see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/static/ltree.html#LTREE-FUNC-TABLE).
"""

@doc """
subpath of `ltree` from position start to position end-1 (counting from 0).
"""
defmacro subltree(ltree, start, finish) do
quote do: fragment("SUBLTREE(?, ?, ?)", unquote(ltree), unquote(start), unquote(finish))
end

@doc """
subpath of `ltree` starting at position offset, extending to end of path.
If offset is negative, subpath starts that far from the end of the path.
"""
defmacro subpath(ltree, offset) do
quote do: fragment("SUBPATH(?, ?)", unquote(ltree), unquote(offset))
end

@doc """
subpath of `ltree` starting at position offset, length len.
If offset is negative, subpath starts that far from the end of the path.
If len is negative, leaves that many labels off the end of the path.
"""
defmacro subpath(ltree, offset, len) do
quote do: fragment("SUBPATH(?, ?, ?)", unquote(ltree), unquote(offset), unquote(len))
end

@doc """
number of labels in path.
"""
defmacro nlevel(ltree) do
quote do: fragment("NLEVEL(?)", unquote(ltree))
end

@doc """
position of first occurrence of b in a; -1 if not found.
"""
defmacro index(a, b) do
quote do: fragment("INDEX(?, ?)", unquote(a), unquote(b))
end

@doc """
position of first occurrence of b in a, searching starting at offset; negative offset means start -offset labels from the end of the path.
"""
defmacro index(a, b, offset) do
quote do: fragment("INDEX(?, ?, ?)", unquote(a), unquote(b), unquote(offset))
end

@doc """
cast `text` to `ltree`.
"""
defmacro text2ltree(text) do
quote do: fragment("TEXT2LTREE(?)", unquote(text))
end

@doc """
cast `ltree` to `text`.
"""
defmacro ltree2text(ltree) do
quote do: fragment("LTREE2TEXT(?)", unquote(ltree))
end

@doc """
lowest common ancestor.
"""
defmacro lca(a, b) do
quote do: fragment("LCA(?, ?)", unquote(a), unquote(b))
end
end
92 changes: 92 additions & 0 deletions lib/ecto_ltree/label_tree.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule EctoLtree.LabelTree do
@moduledoc """
This module defines the LabelTree struct.
Implements the Ecto.Type behaviour.
## Fields
* `labels`
"""

@behaviour Ecto.Type
@type t :: %__MODULE__{labels: [String.t()]}
defstruct labels: []
alias EctoLtree.LabelTree, as: Ltree

@labelpath_size_max 2048

@doc """
Provides custom casting rules from external data to internal representation.
"""
@spec cast(String.t()) :: {:ok, t} | :error
def cast(string) when is_binary(string) and byte_size(string) <= @labelpath_size_max do
labels_result =
string
|> String.split(".")
|> Enum.map(&cast_label/1)

if Enum.any?(labels_result, fn i -> i == :error end) do
:error
else
{:ok, %Ltree{labels: Enum.map(labels_result, fn {_k, v} -> v end)}}
end
end

def cast(%Ltree{} = struct) do
{:ok, struct}
end

def cast(_), do: :error

@label_size_max 256
@label_regex ~r/[A-Za-z0-9_]{1,256}/

@spec cast_label(String.t()) :: {:ok, String.t()} | :error
defp cast_label(string) when is_binary(string) and byte_size(string) <= @label_size_max do
string_length = String.length(string)

case Regex.run(@label_regex, string, return: :index) do
[{0, last}] when last == string_length ->
{:ok, string}

_ ->
:error
end
end

defp cast_label(_), do: :error

@doc """
From internal representation to database.
"""
@spec dump(t) :: {:ok, [String.t()]} | :error
def dump(%Ltree{} = label_tree) do
{:ok, decode(label_tree)}
end

def dump(_), do: :error

@spec decode(t) :: String.t()
def decode(%Ltree{} = label_tree) do
Enum.join(label_tree.labels, ".")
end

@doc """
From database to internal representation.
"""
@spec load(String.t()) :: {:ok, t} | :error
def load(labelpath) when is_binary(labelpath) do
{:ok, %Ltree{labels: labelpath |> String.split(".")}}
end

def load(_), do: :error

@doc """
Returns the underlying schema type.
"""
@spec type() :: :ltree
def type(), do: :ltree
end

defimpl String.Chars, for: EctoLtree.LabelTree do
def to_string(%EctoLtree.LabelTree{} = label_tree), do: EctoLtree.LabelTree.decode(label_tree)
end
37 changes: 37 additions & 0 deletions lib/ecto_ltree/postgrex/lquery.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule EctoLtree.Postgrex.Lquery do
@moduledoc """
This module provides the necessary functions to encode and decode PostgreSQL’s `lquery` data type to and from Elixir values.
Implements the Postgrex.Extension behaviour.
"""

@behaviour Postgrex.Extension

def init(opts) do
Keyword.get(opts, :decode_copy, :copy)
end

def matching(_state), do: [type: "lquery"]

def format(_state), do: :text

def encode(_state) do
quote do
bin when is_binary(bin) ->
[<<byte_size(bin)::signed-size(32)>> | bin]
end
end

def decode(:reference) do
quote do
<<len::signed-size(32), bin::binary-size(len)>> ->
bin
end
end

def decode(:copy) do
quote do
<<len::signed-size(32), bin::binary-size(len)>> ->
:binary.copy(bin)
end
end
end
Loading

0 comments on commit b2863fd

Please sign in to comment.