├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib └── ecto_pg_extras.ex ├── mix.exs ├── mix.lock └── test ├── pg_extras_test.exs ├── test_helper.exs └── test_migration.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.1.1 4 | 5 | Fill out the docs with better descriptions and source examples. 6 | 7 | ## v0.1.0 8 | 9 | Initial release. 10 | 11 | Make a variety of Postgres functions available to the Ecto DSL so that you 12 | don't have to. 13 | 14 | - `coalesce/1` and `coalesce/2` 15 | - `nullif/1` 16 | - `greatest/1` and `greatest/2` 17 | - `least/1` and `least/2` 18 | - `lower/1` 19 | - `upper/1` 20 | - `between/3` 21 | - `not_between/3` 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Josh Branchaud 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecto_pg_extras 2 | 3 | > A collection of custom functions for PostgreSQL features in Ecto 4 | 5 | ```elixir 6 | def deps do 7 | [{:ecto_pg_extras, "~> 0.1.0"}] 8 | end 9 | ``` 10 | 11 | ## Usage 12 | 13 | Import `ecto_pg_extras` in any module where you want access to the custom 14 | functions for use with Ecto queries. 15 | 16 | ```elixir 17 | import EctoPgExtras 18 | ``` 19 | 20 | Then use any of the functions as part of a query as you would anything else 21 | defined in `Ecto.Query.API`. For example, here is the `coalesce` function in 22 | action: 23 | 24 | ```elixir 25 | from(posts in Posts, 26 | where: posts.id == 1, 27 | select: { 28 | posts.title, 29 | coalesce(posts.description, posts.short_description, "N/A") 30 | }) 31 | ``` 32 | 33 | ## About 34 | 35 | [![Hashrocket logo](https://hashrocket.com/hashrocket_logo.svg)](https://hashrocket.com) 36 | 37 | EctoPgExtras is supported by the team at [Hashrocket](https://hashrocket.com), a multidisciplinary design & development consultancy. If you'd like to [work with us](https://hashrocket.com/contact-us/hire-us) or [join our team](https://hashrocket.com/contact-us/jobs), don't hesitate to get in touch. 38 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ecto_pg_extras, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ecto_pg_extras, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/ecto_pg_extras.ex: -------------------------------------------------------------------------------- 1 | defmodule EctoPgExtras do 2 | @moduledoc """ 3 | A collection of custom functions for PostgreSQL features in Ecto 4 | """ 5 | 6 | @doc """ 7 | PostgreSQL's `coalesce` function 8 | 9 | Use `coalesce/2` to return the first argument that is not null. 10 | 11 | ``` 12 | from(posts in "posts", 13 | select: { 14 | posts.title, 15 | coalesce(posts.short_description, posts.description) 16 | }) 17 | ``` 18 | 19 | """ 20 | defmacro coalesce(left, right) do 21 | quote do 22 | fragment("coalesce(?, ?)", unquote(left), unquote(right)) 23 | end 24 | end 25 | 26 | @doc """ 27 | PostgreSQL's `coalesce` function 28 | 29 | Use `coalesce/1` to return the first value in the given list that is not 30 | null. 31 | 32 | ``` 33 | from(posts in "posts", 34 | select: { 35 | posts.title, 36 | coalesce([posts.short_description, posts.description, "N/A"]) 37 | }) 38 | ``` 39 | 40 | """ 41 | defmacro coalesce(operands) do 42 | fragment_str = "coalesce(" <> generate_question_marks(operands) <> ")" 43 | {:fragment, [], [fragment_str | operands]} 44 | end 45 | 46 | @doc """ 47 | PostgreSQL's `nullif` function 48 | 49 | Use `nullif/2` to return null if the two arguments are equal. 50 | 51 | ``` 52 | from(posts in "posts", 53 | select: nullif(posts.description, "")) 54 | ``` 55 | 56 | This is a peculiar function, but can be handy in combination with other 57 | functions. For example, use it within `coalesce/1` to weed out a blank 58 | value and replace it with some default. 59 | 60 | ``` 61 | from(posts in "posts", 62 | select: { 63 | posts.title, 64 | coalesce(nullif(posts.description, ""), "N/A") 65 | }) 66 | ``` 67 | 68 | """ 69 | defmacro nullif(left, right) do 70 | quote do 71 | fragment("nullif(?, ?)", unquote(left), unquote(right)) 72 | end 73 | end 74 | 75 | @doc """ 76 | PostgreSQL's `greatest` function 77 | 78 | Use `greatest/2` to return the larger of two arguments. This function will 79 | always preference actual values over null. 80 | 81 | ``` 82 | from(posts in "posts", 83 | select: greatest(posts.created_at, posts.published_at)) 84 | ``` 85 | """ 86 | defmacro greatest(left, right) do 87 | quote do 88 | fragment("greatest(?, ?)", unquote(left), unquote(right)) 89 | end 90 | end 91 | 92 | @doc """ 93 | PostgreSQL's `greatest` function 94 | 95 | Use `greatest/1` to return the largest of a list of arguments. This 96 | function will always preference actual values over null. 97 | 98 | ``` 99 | from(posts in "posts", 100 | select: greatest([ 101 | posts.created_at, 102 | posts.updated_at, 103 | posts.published_at 104 | ])) 105 | ``` 106 | 107 | """ 108 | defmacro greatest(operands) do 109 | fragment_str = "greatest(" <> generate_question_marks(operands) <> ")" 110 | {:fragment, [], [fragment_str | operands]} 111 | end 112 | 113 | @doc """ 114 | PostgreSQL's `least` function 115 | 116 | Use `least/2` to return the smaller of the two arguments. This function 117 | always preferences actual values over null. 118 | 119 | ``` 120 | from(posts in "posts", 121 | select: least(posts.created_at, posts.updated_at)) 122 | ``` 123 | 124 | """ 125 | defmacro least(left, right) do 126 | quote do 127 | fragment("least(?, ?)", unquote(left), unquote(right)) 128 | end 129 | end 130 | 131 | @doc """ 132 | PostgreSQL's `least` function 133 | 134 | Use `least/1` to return the smallest of the arguments. This function 135 | always preferences actual values over null. 136 | 137 | ``` 138 | from(posts in "posts", 139 | select: least([ 140 | posts.created_at, 141 | posts.updated_at, 142 | posts.published_at 143 | ])) 144 | ``` 145 | 146 | """ 147 | defmacro least(operands) do 148 | fragment_str = "least(" <> generate_question_marks(operands) <> ")" 149 | {:fragment, [], [fragment_str | operands]} 150 | end 151 | 152 | @doc """ 153 | PostgreSQL's `lower` function 154 | 155 | Use `lower/1` to lowercase a given string. This works like Elixir's 156 | `String.downcase/1` function allowing string manipulation within a query. 157 | 158 | ``` 159 | from(users in "users", 160 | select: lower(users.email)) 161 | ``` 162 | 163 | """ 164 | defmacro lower(operand) do 165 | quote do 166 | fragment("lower(?)", unquote(operand)) 167 | end 168 | end 169 | 170 | @doc """ 171 | PostgreSQL's `upper` function 172 | 173 | Use `upper/1` to uppercase a given string. This works like Elixir's 174 | `String.upcase/1` function allowing string manipulation within a query. 175 | 176 | ``` 177 | from(users in "users", 178 | select: upper(users.username)) 179 | ``` 180 | 181 | """ 182 | defmacro upper(operand) do 183 | quote do 184 | fragment("upper(?)", unquote(operand)) 185 | end 186 | end 187 | 188 | @doc """ 189 | PostgreSQL's `between` predicate 190 | 191 | Use `between/3` to perform a range test for the first argument against the 192 | second (lower bound) and third argument (upper bound). Returns true if the 193 | value falls in the given range. False otherwise. 194 | 195 | ``` 196 | from(posts in "posts", 197 | select: {posts.title, posts.description} 198 | where: between(posts.published_at, 199 | ^Ecto.DateTime.cast!({{2016,5,10},{0,0,0}}), 200 | ^Ecto.DateTime.cast!({{2016,5,20},{0,0,0}}))) 201 | ``` 202 | 203 | """ 204 | defmacro between(value, lower, upper) do 205 | quote do 206 | fragment("? between ? and ?", 207 | unquote(value), 208 | unquote(lower), 209 | unquote(upper)) 210 | end 211 | end 212 | 213 | @doc """ 214 | PostgreSQL's `not between` predicate 215 | 216 | Use `not_between/3` to perform a range test for the first argument against 217 | the second (lower bound) and third argument (upper bound). Returns true if 218 | the value does not fall in the given range. False otherwise. 219 | 220 | ``` 221 | from(posts in "posts", 222 | select: {posts.title, posts.description} 223 | where: not_between(posts.published_at, 224 | ^Ecto.DateTime.cast!({{2016,5,10},{0,0,0}}), 225 | ^Ecto.DateTime.cast!({{2016,5,20},{0,0,0}}))) 226 | ``` 227 | 228 | """ 229 | defmacro not_between(value, lower, upper) do 230 | quote do 231 | fragment("? not between ? and ?", 232 | unquote(value), 233 | unquote(lower), 234 | unquote(upper)) 235 | end 236 | end 237 | 238 | defp generate_question_marks(list) do 239 | list 240 | |> Enum.map(fn(_) -> "?" end) 241 | |> Enum.join(", ") 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoPgExtras.Mixfile do 2 | use Mix.Project 3 | 4 | @version "0.1.1" 5 | 6 | def project do 7 | [ 8 | app: :ecto_pg_extras, 9 | version: @version, 10 | elixir: "~> 1.4", 11 | build_embedded: Mix.env == :prod, 12 | start_permanent: Mix.env == :prod, 13 | description: description(), 14 | package: package(), 15 | docs: [source_ref: "v#{@version}", main: "EctoPgExtras"], 16 | deps: deps() 17 | ] 18 | end 19 | 20 | def application do 21 | [] 22 | end 23 | 24 | defp deps do 25 | [ 26 | {:postgrex, "~> 0.13.0"}, 27 | {:ecto, "~> 2.1"}, 28 | {:ex_doc, "~> 0.14", only: :dev} 29 | ] 30 | end 31 | 32 | defp description do 33 | """ 34 | A collection of custom functions for PostgreSQL features in Ecto. 35 | """ 36 | end 37 | 38 | defp package do 39 | [ 40 | files: ["lib", "mix.exs", "README.md", "LICENSE"], 41 | maintainers: ["Josh Branchaud", "Hashrocket"], 42 | licenses: ["MIT"], 43 | links: %{ 44 | "GitHub" => "https://github.com/hashrocket/ecto_pg_extras" 45 | } 46 | ] 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 2 | "db_connection": {:hex, :db_connection, "1.1.2", "2865c2a4bae0714e2213a0ce60a1b12d76a6efba0c51fbda59c9ab8d1accc7a8", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 3 | "decimal": {:hex, :decimal, "1.3.1", "157b3cedb2bfcb5359372a7766dd7a41091ad34578296e951f58a946fcab49c6", [:mix], []}, 4 | "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, 5 | "ecto": {:hex, :ecto, "2.1.4", "d1ba932813ec0e0d9db481ef2c17777f1cefb11fc90fa7c142ff354972dfba7e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, optional: true]}]}, 6 | "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, 7 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}, 8 | "postgrex": {:hex, :postgrex, "0.13.2", "2b88168fc6a5456a27bfb54ccf0ba4025d274841a7a3af5e5deb1b755d95154e", [:mix], [{:connection, "~> 1.0", [hex: :connection, optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}]}} 9 | -------------------------------------------------------------------------------- /test/pg_extras_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EctoPgExtrasTest do 2 | use EctoPgExtras.TestCase 3 | doctest EctoPgExtras 4 | 5 | import Ecto.Query 6 | import EctoPgExtras 7 | 8 | alias Ecto.Integration.TestRepo 9 | 10 | test "coalesce two values" do 11 | results = 12 | from(buckets in "buckets", 13 | select: coalesce(buckets.a, buckets.b)) 14 | |> TestRepo.all 15 | 16 | assert results == [1, 2, nil, nil, 2] 17 | end 18 | 19 | test "coalesce a list of values" do 20 | results = 21 | from(buckets in "buckets", 22 | select: coalesce([buckets.a, buckets.b, buckets.c])) 23 | |> TestRepo.all 24 | 25 | assert results == [1, 2, 3, nil, 2] 26 | end 27 | 28 | test "nullif two values" do 29 | results = 30 | from(buckets in "buckets", 31 | where: buckets.id == 1, 32 | select: { 33 | nullif(buckets.a, 2), 34 | nullif(buckets.b, 2), 35 | nullif(buckets.c, 2) 36 | }) 37 | |> TestRepo.all 38 | 39 | assert results == [{1, nil, 3}] 40 | end 41 | 42 | test "greatest of two values" do 43 | results = 44 | from(buckets in "buckets", 45 | select: { 46 | greatest(buckets.a, buckets.b), 47 | greatest(buckets.b, buckets.c) 48 | }) 49 | |> TestRepo.all 50 | 51 | assert results == [ 52 | {2, 3}, 53 | {2, 3}, 54 | {nil, 3}, 55 | {nil, nil}, 56 | {2, 2} 57 | ] 58 | end 59 | 60 | test "greatest of a list of values" do 61 | results = 62 | from(buckets in "buckets", 63 | select: greatest([buckets.a, buckets.b, buckets.c])) 64 | |> TestRepo.all 65 | 66 | assert results == [3, 3, 3, nil, 2] 67 | end 68 | 69 | test "least of two values" do 70 | results = 71 | from(buckets in "buckets", 72 | select: { 73 | least(buckets.a, buckets.b), 74 | least(buckets.b, buckets.c) 75 | }) 76 | |> TestRepo.all 77 | 78 | assert results == [ 79 | {1, 2}, 80 | {2, 2}, 81 | {nil, 3}, 82 | {nil, nil}, 83 | {2, 2} 84 | ] 85 | end 86 | 87 | test "least of a list of values" do 88 | results = 89 | from(buckets in "buckets", 90 | select: least([buckets.a, buckets.b, buckets.c])) 91 | |> TestRepo.all 92 | 93 | assert results == [1, 2, 3, nil, 2] 94 | end 95 | 96 | test "lower a string" do 97 | results = 98 | from(persons in "persons", 99 | select: { 100 | lower(persons.first_name), 101 | lower(persons.last_name) 102 | }) 103 | |> TestRepo.all 104 | 105 | assert results == [ 106 | {"liz", "lemon"}, 107 | {"tracy", "jordan"}, 108 | {nil, nil}, 109 | {"jack", "donaghy"}, 110 | {"toofer", nil}, 111 | {nil, "lutz"} 112 | ] 113 | end 114 | 115 | test "upper a string" do 116 | results = 117 | from(persons in "persons", 118 | select: { 119 | upper(persons.first_name), 120 | upper(persons.last_name) 121 | }) 122 | |> TestRepo.all 123 | 124 | assert results == [ 125 | {"LIZ", "LEMON"}, 126 | {"TRACY", "JORDAN"}, 127 | {nil, nil}, 128 | {"JACK", "DONAGHY"}, 129 | {"TOOFER", nil}, 130 | {nil, "LUTZ"} 131 | ] 132 | end 133 | 134 | test "between two timestamps" do 135 | lower_timestamp = Ecto.DateTime.cast!({{2016,5,10},{0,0,0}}) 136 | upper_timestamp = Ecto.DateTime.cast!({{2016,5,20},{0,0,0}}) 137 | 138 | results = 139 | from(posts in "posts", 140 | where: between(posts.published_at, 141 | ^lower_timestamp, 142 | ^upper_timestamp), 143 | select: posts.id 144 | ) 145 | |> TestRepo.all() 146 | 147 | assert results == [1] 148 | end 149 | 150 | test "not between two timestamps" do 151 | lower_timestamp = Ecto.DateTime.cast!({{2016,5,10},{0,0,0}}) 152 | upper_timestamp = Ecto.DateTime.cast!({{2016,5,20},{0,0,0}}) 153 | 154 | results = 155 | from(posts in "posts", 156 | where: not_between(posts.published_at, 157 | ^lower_timestamp, 158 | ^upper_timestamp), 159 | select: posts.id 160 | ) 161 | |> TestRepo.all() 162 | 163 | assert results == [2] 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | Code.require_file "test_migration.exs", __DIR__ 4 | 5 | alias Ecto.Integration.TestRepo 6 | 7 | Application.put_env(:ecto, TestRepo, 8 | adapter: Ecto.Adapters.Postgres, 9 | url: "ecto://postgres:postgres@localhost/ecto_pg_extras_test", 10 | pool: Ecto.Adapters.SQL.Sandbox) 11 | 12 | defmodule Ecto.Integration.TestRepo do 13 | use Ecto.Repo, otp_app: :ecto 14 | end 15 | 16 | defmodule EctoPgExtras.TestCase do 17 | use ExUnit.CaseTemplate 18 | 19 | setup do 20 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(TestRepo) 21 | end 22 | end 23 | 24 | # Load up the repository, start it, and run migrations 25 | _ = Ecto.Adapters.Postgres.storage_down(TestRepo.config) 26 | :ok = Ecto.Adapters.Postgres.storage_up(TestRepo.config) 27 | 28 | {:ok, _pid} = TestRepo.start_link 29 | 30 | :ok = Ecto.Migrator.up(TestRepo, 0, TestMigration, log: false) 31 | Ecto.Adapters.SQL.Sandbox.mode(TestRepo, :manual) 32 | Process.flag(:trap_exit, true) 33 | -------------------------------------------------------------------------------- /test/test_migration.exs: -------------------------------------------------------------------------------- 1 | defmodule TestMigration do 2 | use Ecto.Migration 3 | 4 | def up do 5 | execute """ 6 | create table buckets ( 7 | id serial primary key, 8 | a integer, 9 | b integer, 10 | c integer 11 | ); 12 | """ 13 | 14 | execute """ 15 | insert into buckets (a,b,c) 16 | values 17 | (1, 2, 3), 18 | (null, 2, 3), 19 | (null, null, 3), 20 | (null, null, null), 21 | (null, 2, null); 22 | """ 23 | 24 | execute """ 25 | create table persons( 26 | id serial primary key, 27 | first_name varchar, 28 | last_name varchar, 29 | email varchar not null 30 | ); 31 | """ 32 | 33 | execute """ 34 | insert into persons (first_name, last_name, email) 35 | values 36 | ('Liz', 'Lemon', 'liz.lemon@nbc.com'), 37 | ('Tracy', 'Jordan', 'tracy.jordan@nbc.com'), 38 | (null, null, 'kenneth.parcell@nbc.com'), 39 | ('Jack', 'Donaghy', 'jack.donaghy@nbc.com'), 40 | ('Toofer', null, 'toofer.spurlock@nbc.com'), 41 | (null, 'Lutz', 'johnny.aardvark@nbc.com') 42 | ; 43 | """ 44 | 45 | execute """ 46 | create table posts( 47 | id serial primary key, 48 | title varchar not null, 49 | description text, 50 | short_description text, 51 | created_at timestamp not null default now(), 52 | updated_at timestamp not null default now(), 53 | published_at timestamp 54 | ); 55 | """ 56 | 57 | execute """ 58 | insert into posts 59 | ( 60 | title, 61 | description, 62 | short_description, 63 | created_at, 64 | updated_at, 65 | published_at 66 | ) 67 | values 68 | ( 69 | 'Post 1', 70 | null, 71 | 'The first of many posts', 72 | '2016-05-05 00:00:00'::timestamp, 73 | '2016-05-05 00:00:00'::timestamp, 74 | '2016-05-15 00:00:00'::timestamp 75 | ), 76 | ( 77 | 'Post 2', 78 | 'This is a longer description of the post', 79 | 'A brief description', 80 | '2016-05-06 00:00:00'::timestamp, 81 | '2016-05-06 00:00:00'::timestamp, 82 | '2016-05-08 00:00:00'::timestamp 83 | ); 84 | """ 85 | end 86 | 87 | def down do 88 | execute """ 89 | drop table buckets, persons, posts; 90 | """ 91 | end 92 | end 93 | --------------------------------------------------------------------------------