-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit b2863fd
Showing
24 changed files
with
818 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
defmodule EctoLtree do | ||
@moduledoc false | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.