├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bench └── ulid_bench.exs ├── ci ├── mix-ecto-2.0.exs ├── mix-ecto-2.1.exs ├── mix-ecto-2.2.exs └── mix-ecto-3.0.exs ├── config └── config.exs ├── lib └── ecto │ └── ulid.ex ├── mix.exs └── test ├── ecto └── ulid_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | mix.lock 2 | bench/snapshots/ 3 | 4 | # The directory Mix will write compiled artifacts to. 5 | /_build/ 6 | 7 | # If you run "mix test --cover", coverage assets end up here. 8 | /cover/ 9 | 10 | # The directory Mix downloads your dependencies sources to. 11 | /deps/ 12 | 13 | # Where 3rd-party dependencies like ExDoc output generated docs. 14 | /doc/ 15 | 16 | # Ignore .fetch files in case you like to edit your project deps locally. 17 | /.fetch 18 | 19 | # If the VM crashes, it generates a dump, let's ignore it too. 20 | erl_crash.dump 21 | 22 | # Also ignore archive artifacts (built via "mix archive.build"). 23 | *.ez 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | cache: 3 | directories: 4 | - deps 5 | - _build 6 | matrix: 7 | include: 8 | - elixir: 1.11.2 9 | otp_release: 23.0 10 | env: MIX_EXS=ci/mix-ecto-3.0.exs 11 | - elixir: 1.10.4 12 | otp_release: 22.3 13 | env: MIX_EXS=ci/mix-ecto-3.0.exs 14 | - elixir: 1.9.4 15 | otp_release: 22.3 16 | env: MIX_EXS=ci/mix-ecto-3.0.exs 17 | - elixir: 1.8.2 18 | otp_release: 22.3 19 | env: MIX_EXS=ci/mix-ecto-3.0.exs 20 | - elixir: 1.7.4 21 | otp_release: 22.3 22 | env: MIX_EXS=ci/mix-ecto-3.0.exs 23 | - elixir: 1.6.6 24 | otp_release: 20.3 25 | env: MIX_EXS=ci/mix-ecto-2.2.exs 26 | - elixir: 1.6.6 27 | otp_release: 20.0 28 | env: MIX_EXS=ci/mix-ecto-2.2.exs 29 | - elixir: 1.5.3 30 | otp_release: 19.3 31 | env: MIX_EXS=ci/mix-ecto-2.2.exs 32 | - elixir: 1.5.3 33 | otp_release: 19.0 34 | env: MIX_EXS=ci/mix-ecto-2.1.exs 35 | - elixir: 1.4.5 36 | otp_release: 18.3 37 | env: MIX_EXS=ci/mix-ecto-2.0.exs 38 | after_success: 39 | - mix bench 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.2.0 (2019-01-17) 4 | ### Breaking Changes 5 | * Minimum supported Elixir is now 1.4. 6 | 7 | ### Changed 8 | * ([#3](https://github.com/TheRealReal/ecto-ulid/pull/3)) 9 | Fix deprecation warnings regarding time units. 10 | 11 | ## 0.1.1 (2018-12-03) 12 | ### Added 13 | * ([#2](https://github.com/TheRealReal/ecto-ulid/pull/2)) 14 | Add support for Ecto 3.x. 15 | 16 | ## 0.1.0 (2018-06-06) 17 | ### Added 18 | * Generate ULID in Base32 or binary format. 19 | * Generate ULID for a given timestamp. 20 | * Autogenerate ULID when used as a primary key. 21 | * Supports reading and writing ULID in a database backed by its native `uuid` type (no database 22 | extensions required). 23 | * Supports Ecto 2.x. 24 | * Supports Elixir 1.2 and newer. 25 | * Tested with PostgreSQL and MySQL. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The RealReal, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ecto.ULID 2 | 3 | An `Ecto.Type` implementation of [ULID](https://github.com/ulid/spec). 4 | 5 | `Ecto.ULID` should be compatible anywhere that `Ecto.UUID` is supported. It has been confirmed to 6 | work with PostgreSQL and MySQL on Ecto 2.x and 3.x. Ecto 1.x is *not* supported. 7 | 8 | ULID is a 128-bit universally unique lexicographically sortable identifier. ULID is 9 | binary-compatible with UUID, so it can be stored in a `uuid` column in a database. 10 | 11 | ## Features 12 | 13 | * Generate ULID in Base32 or binary format. 14 | * Generate ULID for a given timestamp. 15 | * Autogenerate ULID when used as a primary key. 16 | * Supports reading and writing ULID in a database backed by its native `uuid` type (no database 17 | extensions required). 18 | * Supports Ecto 2.x and Ecto 3.x. 19 | * Supports Elixir 1.4 and newer. 20 | * Confirmed working on PostgreSQL and MySQL. 21 | * Optimized for high throughput. 22 | 23 | ## Performance 24 | 25 | Since one use case of ULID is to handle a large volume of events, `Ecto.ULID` is optimized to be as 26 | fast as possible. It borrows techniques from `Ecto.UUID` to achieve sub-microsecond times for most 27 | operations. 28 | 29 | A benchmark suite is included. Download the repository and run `mix bench` to test the performance 30 | on your system. 31 | 32 | The following are results from running the benchmark on an AMD Ryzen Threadripper 1950X: 33 | 34 | ``` 35 | benchmark name iterations average time 36 | cast/1 10000000 0.25 µs/op 37 | dump/1 10000000 0.50 µs/op 38 | load/1 10000000 0.55 µs/op 39 | bingenerate/0 10000000 0.93 µs/op 40 | generate/0 1000000 1.55 µs/op 41 | ``` 42 | 43 | ## Usage 44 | 45 | Usage is very similar to `Ecto.UUID`. The following example shows how to use `Ecto.ULID` as a 46 | primary key in a database table, but it can be used for other columns just as easily. 47 | 48 | [API documentation](https://hexdocs.pm/ecto_ulid) is available on hexdocs. 49 | 50 | ### Install 51 | 52 | Install `ecto_ulid` from Hex by adding it to the dependencies in `mix.exs`: 53 | 54 | ```elixir 55 | defp deps do 56 | [ 57 | {:ecto_ulid, "~> 0.2.0"} 58 | ] 59 | end 60 | ``` 61 | 62 | ### Migration 63 | 64 | Since ULID is binary-compatible with UUID, the migrations look the same for both types. Use 65 | `:binary_id` when defining a column in a migration: 66 | 67 | ```elixir 68 | create table(:events, primary_key: false) do 69 | add :id, :binary_id, null: false, primary_key: true 70 | # more columns ... 71 | end 72 | ``` 73 | 74 | Alternatively, if you plan to use ULID as the primary key type for all of your tables, you can set 75 | `migration_primary_key` when configuring your `Repo`: 76 | 77 | ```elixir 78 | config :my_app, MyApp.Repo, migration_primary_key: [name: :id, type: :binary_id] 79 | ``` 80 | 81 | and then you *do not* need to specify the `id` column in your migrations: 82 | 83 | ```elixir 84 | create table(:events) do 85 | # more columns ... 86 | end 87 | ``` 88 | 89 | ### Schema 90 | 91 | When defining a model's schema, use `Ecto.ULID` as the `@primary_key` or `@foreign_key_type` as 92 | appropriate for your schema. Here's an example of using both: 93 | 94 | ```elixir 95 | defmodule MyApp.Event do 96 | use Ecto.Schema 97 | 98 | @primary_key {:id, Ecto.ULID, autogenerate: false} 99 | @foreign_key_type Ecto.ULID 100 | schema "events" do 101 | # more columns ... 102 | end 103 | end 104 | ``` 105 | 106 | `Ecto.ULID` supports `autogenerate: true` as well as `autogenerate: false` when used as the primary 107 | key. 108 | 109 | ### Application Usage 110 | 111 | A ULID can be generated in string or binary format by calling `generate/0` or `bingenerate/0`. This 112 | can be useful when generating ULIDs to send to external systems: 113 | 114 | ```elixir 115 | Ecto.ULID.generate() #=> "01BZ13RV29T5S8HV45EDNC748P" 116 | Ecto.ULID.bingenerate() #=> <<1, 95, 194, 60, 108, 73, 209, 114, 136, 236, 133, 115, 106, 195, 145, 22>> 117 | ``` 118 | 119 | To backfill old data, it may be helpful to pass a timestamp to `generate/1` or `bingenerate/1`. See 120 | the [API documentation](https://hexdocs.pm/ecto_ulid) for more details. 121 | 122 | ## License 123 | 124 | Copyright © 2018 The RealReal, Inc. 125 | 126 | Distributed under the [MIT License](./LICENSE). 127 | -------------------------------------------------------------------------------- /bench/ulid_bench.exs: -------------------------------------------------------------------------------- 1 | defmodule ULIDBench do 2 | use Benchfella 3 | 4 | bench "generate/0" do 5 | Ecto.ULID.generate() 6 | nil 7 | end 8 | 9 | bench "bingenerate/0" do 10 | Ecto.ULID.bingenerate() 11 | nil 12 | end 13 | 14 | bench "cast/1" do 15 | Ecto.ULID.cast("01C0M0Y7BG2NMB15VVVJH807F3") 16 | end 17 | 18 | bench "dump/1" do 19 | Ecto.ULID.dump("01C0M0Y7BG2NMB15VVVJH807F3") 20 | end 21 | 22 | bench "load/1" do 23 | Ecto.ULID.load(<<1, 96, 40, 15, 29, 112, 21, 104, 176, 151, 123, 220, 162, 128, 29, 227>>) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /ci/mix-ecto-2.0.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ulid, 7 | version: "0.1.1", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:ecto, "~> 2.0.0"}, 23 | {:benchfella, "~> 0.3.5", only: [:dev, :test]}, 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ci/mix-ecto-2.1.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ulid, 7 | version: "0.1.1", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:ecto, "~> 2.1.0"}, 23 | {:benchfella, "~> 0.3.5", only: [:dev, :test]}, 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ci/mix-ecto-2.2.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ulid, 7 | version: "0.1.1", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:ecto, "~> 2.2.0"}, 23 | {:benchfella, "~> 0.3.5", only: [:dev, :test]}, 24 | ] 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /ci/mix-ecto-3.0.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ulid, 7 | version: "0.1.1", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [ 16 | extra_applications: [:logger] 17 | ] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:ecto, "~> 3.0.0"}, 23 | {:benchfella, "~> 0.3.5", only: [:dev, :test]}, 24 | ] 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /lib/ecto/ulid.ex: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID do 2 | @moduledoc """ 3 | An Ecto type for ULID strings. 4 | """ 5 | 6 | # replace with `use Ecto.Type` after Ecto 3.2.0 is required 7 | @behaviour Ecto.Type 8 | # and remove both of these functions 9 | def embed_as(_), do: :self 10 | def equal?(term1, term2), do: term1 == term2 11 | 12 | @typedoc """ 13 | A hex-encoded ULID string. 14 | """ 15 | @type t :: <<_::208>> 16 | 17 | @doc """ 18 | The underlying schema type. 19 | """ 20 | def type, do: :uuid 21 | 22 | @doc """ 23 | Casts a string to ULID. 24 | """ 25 | def cast(<<_::bytes-size(26)>> = value) do 26 | if valid?(value) do 27 | {:ok, value} 28 | else 29 | :error 30 | end 31 | end 32 | def cast(_), do: :error 33 | 34 | @doc """ 35 | Same as `cast/1` but raises `Ecto.CastError` on invalid arguments. 36 | """ 37 | def cast!(value) do 38 | case cast(value) do 39 | {:ok, ulid} -> ulid 40 | :error -> raise Ecto.CastError, type: __MODULE__, value: value 41 | end 42 | end 43 | 44 | @doc """ 45 | Converts a Crockford Base32 encoded ULID into a binary. 46 | """ 47 | def dump(<<_::bytes-size(26)>> = encoded), do: decode(encoded) 48 | def dump(_), do: :error 49 | 50 | @doc """ 51 | Converts a binary ULID into a Crockford Base32 encoded string. 52 | """ 53 | def load(<<_::unsigned-size(128)>> = bytes), do: encode(bytes) 54 | def load(_), do: :error 55 | 56 | @doc false 57 | def autogenerate, do: generate() 58 | 59 | @doc """ 60 | Generates a Crockford Base32 encoded ULID. 61 | 62 | If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. 63 | Otherwise, a ULID will be generated for the current time. 64 | 65 | Arguments: 66 | 67 | * `timestamp`: A Unix timestamp with millisecond precision. 68 | """ 69 | def generate(timestamp \\ System.system_time(:millisecond)) do 70 | {:ok, ulid} = encode(bingenerate(timestamp)) 71 | ulid 72 | end 73 | 74 | @doc """ 75 | Generates a binary ULID. 76 | 77 | If a value is provided for `timestamp`, the generated ULID will be for the provided timestamp. 78 | Otherwise, a ULID will be generated for the current time. 79 | 80 | Arguments: 81 | 82 | * `timestamp`: A Unix timestamp with millisecond precision. 83 | """ 84 | def bingenerate(timestamp \\ System.system_time(:millisecond)) do 85 | <> 86 | end 87 | 88 | defp encode(<< b1::3, b2::5, b3::5, b4::5, b5::5, b6::5, b7::5, b8::5, b9::5, b10::5, b11::5, b12::5, b13::5, 89 | b14::5, b15::5, b16::5, b17::5, b18::5, b19::5, b20::5, b21::5, b22::5, b23::5, b24::5, b25::5, b26::5>>) do 90 | <> 92 | catch 93 | :error -> :error 94 | else 95 | encoded -> {:ok, encoded} 96 | end 97 | defp encode(_), do: :error 98 | 99 | @compile {:inline, e: 1} 100 | 101 | defp e(0), do: ?0 102 | defp e(1), do: ?1 103 | defp e(2), do: ?2 104 | defp e(3), do: ?3 105 | defp e(4), do: ?4 106 | defp e(5), do: ?5 107 | defp e(6), do: ?6 108 | defp e(7), do: ?7 109 | defp e(8), do: ?8 110 | defp e(9), do: ?9 111 | defp e(10), do: ?A 112 | defp e(11), do: ?B 113 | defp e(12), do: ?C 114 | defp e(13), do: ?D 115 | defp e(14), do: ?E 116 | defp e(15), do: ?F 117 | defp e(16), do: ?G 118 | defp e(17), do: ?H 119 | defp e(18), do: ?J 120 | defp e(19), do: ?K 121 | defp e(20), do: ?M 122 | defp e(21), do: ?N 123 | defp e(22), do: ?P 124 | defp e(23), do: ?Q 125 | defp e(24), do: ?R 126 | defp e(25), do: ?S 127 | defp e(26), do: ?T 128 | defp e(27), do: ?V 129 | defp e(28), do: ?W 130 | defp e(29), do: ?X 131 | defp e(30), do: ?Y 132 | defp e(31), do: ?Z 133 | 134 | defp decode(<< c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, c9::8, c10::8, c11::8, c12::8, c13::8, 135 | c14::8, c15::8, c16::8, c17::8, c18::8, c19::8, c20::8, c21::8, c22::8, c23::8, c24::8, c25::8, c26::8>>) do 136 | << d(c1)::3, d(c2)::5, d(c3)::5, d(c4)::5, d(c5)::5, d(c6)::5, d(c7)::5, d(c8)::5, d(c9)::5, d(c10)::5, d(c11)::5, d(c12)::5, d(c13)::5, 137 | d(c14)::5, d(c15)::5, d(c16)::5, d(c17)::5, d(c18)::5, d(c19)::5, d(c20)::5, d(c21)::5, d(c22)::5, d(c23)::5, d(c24)::5, d(c25)::5, d(c26)::5>> 138 | catch 139 | :error -> :error 140 | else 141 | decoded -> {:ok, decoded} 142 | end 143 | defp decode(_), do: :error 144 | 145 | @compile {:inline, d: 1} 146 | 147 | defp d(?0), do: 0 148 | defp d(?1), do: 1 149 | defp d(?2), do: 2 150 | defp d(?3), do: 3 151 | defp d(?4), do: 4 152 | defp d(?5), do: 5 153 | defp d(?6), do: 6 154 | defp d(?7), do: 7 155 | defp d(?8), do: 8 156 | defp d(?9), do: 9 157 | defp d(?A), do: 10 158 | defp d(?B), do: 11 159 | defp d(?C), do: 12 160 | defp d(?D), do: 13 161 | defp d(?E), do: 14 162 | defp d(?F), do: 15 163 | defp d(?G), do: 16 164 | defp d(?H), do: 17 165 | defp d(?J), do: 18 166 | defp d(?K), do: 19 167 | defp d(?M), do: 20 168 | defp d(?N), do: 21 169 | defp d(?P), do: 22 170 | defp d(?Q), do: 23 171 | defp d(?R), do: 24 172 | defp d(?S), do: 25 173 | defp d(?T), do: 26 174 | defp d(?V), do: 27 175 | defp d(?W), do: 28 176 | defp d(?X), do: 29 177 | defp d(?Y), do: 30 178 | defp d(?Z), do: 31 179 | defp d(_), do: throw :error 180 | 181 | defp valid?(<< c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, c9::8, c10::8, c11::8, c12::8, c13::8, 182 | c14::8, c15::8, c16::8, c17::8, c18::8, c19::8, c20::8, c21::8, c22::8, c23::8, c24::8, c25::8, c26::8>>) do 183 | v(c1) && v(c2) && v(c3) && v(c4) && v(c5) && v(c6) && v(c7) && v(c8) && v(c9) && v(c10) && v(c11) && v(c12) && v(c13) && 184 | v(c14) && v(c15) && v(c16) && v(c17) && v(c18) && v(c19) && v(c20) && v(c21) && v(c22) && v(c23) && v(c24) && v(c25) && v(c26) 185 | end 186 | defp valid?(_), do: false 187 | 188 | @compile {:inline, v: 1} 189 | 190 | defp v(?0), do: true 191 | defp v(?1), do: true 192 | defp v(?2), do: true 193 | defp v(?3), do: true 194 | defp v(?4), do: true 195 | defp v(?5), do: true 196 | defp v(?6), do: true 197 | defp v(?7), do: true 198 | defp v(?8), do: true 199 | defp v(?9), do: true 200 | defp v(?A), do: true 201 | defp v(?B), do: true 202 | defp v(?C), do: true 203 | defp v(?D), do: true 204 | defp v(?E), do: true 205 | defp v(?F), do: true 206 | defp v(?G), do: true 207 | defp v(?H), do: true 208 | defp v(?J), do: true 209 | defp v(?K), do: true 210 | defp v(?M), do: true 211 | defp v(?N), do: true 212 | defp v(?P), do: true 213 | defp v(?Q), do: true 214 | defp v(?R), do: true 215 | defp v(?S), do: true 216 | defp v(?T), do: true 217 | defp v(?V), do: true 218 | defp v(?W), do: true 219 | defp v(?X), do: true 220 | defp v(?Y), do: true 221 | defp v(?Z), do: true 222 | defp v(_), do: false 223 | end 224 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULID.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ecto_ulid, 7 | version: "0.3.0", 8 | elixir: "~> 1.4", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps(), 11 | name: "Ecto.ULID", 12 | description: "An Ecto.Type implementation of ULID.", 13 | package: package(), 14 | source_url: "https://github.com/TheRealReal/ecto-ulid", 15 | homepage_url: "https://github.com/TheRealReal/ecto-ulid", 16 | docs: [main: "Ecto.ULID"], 17 | ] 18 | end 19 | 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | defp package do 27 | [ 28 | maintainers: ["David Cuddeback"], 29 | licenses: ["MIT"], 30 | links: %{"GitHub" => "https://github.com/TheRealReal/ecto-ulid"}, 31 | ] 32 | end 33 | 34 | defp deps do 35 | [ 36 | {:ecto, "~> 2.0 or ~> 3.0"}, 37 | {:benchfella, "~> 0.3.5", only: [:dev, :test]}, 38 | {:ex_doc, "~> 0.16", only: :dev, runtime: false}, 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/ecto/ulid_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.ULIDTest do 2 | use ExUnit.Case, async: true 3 | 4 | @binary <<1, 95, 194, 60, 108, 73, 209, 114, 136, 236, 133, 115, 106, 195, 145, 22>> 5 | @encoded "01BZ13RV29T5S8HV45EDNC748P" 6 | 7 | # generate/0 8 | 9 | test "generate/0 encodes milliseconds in first 10 characters" do 10 | # test case from ULID README: https://github.com/ulid/javascript#seed-time 11 | <> = Ecto.ULID.generate(1469918176385) 12 | 13 | assert encoded == "01ARYZ6S41" 14 | end 15 | 16 | test "generate/0 generates unique identifiers" do 17 | ulid1 = Ecto.ULID.generate() 18 | ulid2 = Ecto.ULID.generate() 19 | 20 | assert ulid1 != ulid2 21 | end 22 | 23 | # bingenerate/0 24 | 25 | test "bingenerate/0 encodes milliseconds in first 48 bits" do 26 | now = System.system_time(:millisecond) 27 | <> = Ecto.ULID.bingenerate() 28 | 29 | assert_in_delta now, time, 10 30 | end 31 | 32 | test "bingenerate/0 generates unique identifiers" do 33 | ulid1 = Ecto.ULID.bingenerate() 34 | ulid2 = Ecto.ULID.bingenerate() 35 | 36 | assert ulid1 != ulid2 37 | end 38 | 39 | # cast/1 40 | 41 | test "cast/1 returns valid ULID" do 42 | {:ok, ulid} = Ecto.ULID.cast(@encoded) 43 | assert ulid == @encoded 44 | end 45 | 46 | test "cast/1 returns ULID for encoding of correct length" do 47 | {:ok, ulid} = Ecto.ULID.cast("00000000000000000000000000") 48 | assert ulid == "00000000000000000000000000" 49 | end 50 | 51 | test "cast/1 returns error when encoding is too short" do 52 | assert Ecto.ULID.cast("0000000000000000000000000") == :error 53 | end 54 | 55 | test "cast/1 returns error when encoding is too long" do 56 | assert Ecto.ULID.cast("000000000000000000000000000") == :error 57 | end 58 | 59 | test "cast/1 returns error when encoding contains letter I" do 60 | assert Ecto.ULID.cast("I0000000000000000000000000") == :error 61 | end 62 | 63 | test "cast/1 returns error when encoding contains letter L" do 64 | assert Ecto.ULID.cast("L0000000000000000000000000") == :error 65 | end 66 | 67 | test "cast/1 returns error when encoding contains letter O" do 68 | assert Ecto.ULID.cast("O0000000000000000000000000") == :error 69 | end 70 | 71 | test "cast/1 returns error when encoding contains letter U" do 72 | assert Ecto.ULID.cast("U0000000000000000000000000") == :error 73 | end 74 | 75 | test "cast/1 returns error for invalid encoding" do 76 | assert Ecto.ULID.cast("$0000000000000000000000000") == :error 77 | end 78 | 79 | # dump/1 80 | 81 | test "dump/1 dumps valid ULID to binary" do 82 | {:ok, bytes} = Ecto.ULID.dump(@encoded) 83 | assert bytes == @binary 84 | end 85 | 86 | test "dump/1 dumps encoding of correct length" do 87 | {:ok, bytes} = Ecto.ULID.dump("00000000000000000000000000") 88 | assert bytes == <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> 89 | end 90 | 91 | test "dump/1 returns error when encoding is too short" do 92 | assert Ecto.ULID.dump("0000000000000000000000000") == :error 93 | end 94 | 95 | test "dump/1 returns error when encoding is too long" do 96 | assert Ecto.ULID.dump("000000000000000000000000000") == :error 97 | end 98 | 99 | test "dump/1 returns error when encoding contains letter I" do 100 | assert Ecto.ULID.dump("I0000000000000000000000000") == :error 101 | end 102 | 103 | test "dump/1 returns error when encoding contains letter L" do 104 | assert Ecto.ULID.dump("L0000000000000000000000000") == :error 105 | end 106 | 107 | test "dump/1 returns error when encoding contains letter O" do 108 | assert Ecto.ULID.dump("O0000000000000000000000000") == :error 109 | end 110 | 111 | test "dump/1 returns error when encoding contains letter U" do 112 | assert Ecto.ULID.dump("U0000000000000000000000000") == :error 113 | end 114 | 115 | test "dump/1 returns error for invalid encoding" do 116 | assert Ecto.ULID.dump("$0000000000000000000000000") == :error 117 | end 118 | 119 | # load/1 120 | 121 | test "load/1 encodes binary as ULID" do 122 | {:ok, encoded} = Ecto.ULID.load(@binary) 123 | assert encoded == @encoded 124 | end 125 | 126 | test "load/1 encodes binary of correct length" do 127 | {:ok, encoded} = Ecto.ULID.load(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) 128 | assert encoded == "00000000000000000000000000" 129 | end 130 | 131 | test "load/1 returns error when data is too short" do 132 | assert Ecto.ULID.load(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) == :error 133 | end 134 | 135 | test "load/1 returns error when data is too long" do 136 | assert Ecto.ULID.load(<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>) == :error 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------