diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml new file mode 100644 index 0000000..a92f239 --- /dev/null +++ b/.github/workflows/elixir.yml @@ -0,0 +1,72 @@ +name: Elixir CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + name: Build and test + runs-on: ubuntu-latest + + env: + MIX_ENV: test + PGUSER: postgres + PGPASSWORD: postgres + + services: + db: + image: postgres + ports: ['5432:5432'] + env: + POSTGRES_DB: moebius_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v3 + + - name: Setup Elixir + uses: erlef/setup-beam@v1 + with: + version-type: strict + version-file: .tool-versions + + - name: Restore dependencies cache + uses: actions/cache@v3 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + + - name: Install dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + + - name: Set up test data + run: | + mix moebius.migrate + mix moebius.seed + + - name: Check warnings + run: | + mix compile --warnings-as-errors + + - name: Run tests + run: | + mix test + + - name: Check code quality, security and format + run: | + mix quality --strict diff --git a/.gitignore b/.gitignore index 0d5b634..3103d39 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ db.sql *.DS_Store /*.beam + +.elixir_ls/ + +# VSCode +.vscode/ + +# .DS_Store files from macOS +.DS_Store +**/.DS_Store \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..989b51f --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 26.1.2 +elixir 1.15.7-otp-26 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f7d6fb7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: elixir -elixir: -- 1.4.1 -otp_release: -- 18.0 - -sudo: false - -addons: - postgresql: "9.4" - -before_script: - - psql -c 'create database meebuss;' -U postgres - -install: -- mix local.hex --force -- mix deps.get -- mix local.rebar --force - -script: mix test - -notifications: - recipients: - - rob@datachomp.com - - rob@conery.io diff --git a/README.md b/README.md index c433996..501a2c8 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,13 @@ -# This Project is Archived - -I'm hoping to get back to this project at some point, but it will require significant changes in things I can't control. The biggest issue is Postgrex, the Postgres driver, which, after 10 years, is still < 1.0.0. This has put me in the unfortunate position of having to rev every time Postgrex breaks things, which it had been doing for a while, which is why I gave up. - -I know I can use the Erlang drivers but there are enough improvements in Postgrex that I would like to stay within the Elixir ecosystem, it just makes things difficult when I have to keep revving this project when Postgrex does something because Ecto needs it. - - ## A functional query tool for Elixir and PostgreSQL. -*Note: this is version 3.0 and there are significant changes from version 1.0. If you need version 1.x, you can [find the last release here](https://github.com/robconery/moebius/releases/tag/v1)* - Our goal with creating Moebius is to try and keep as close as possible to the functional nature of Elixir and, at the same time, the goodness that is PostgreSQL. We think working with a database should feel like a natural extension of the language, with as little abstraction wonkery as possible. Moebius is *not* an ORM. There are no mappings, no schemas, no migrations; only queries and data. We embrace PostgreSQL as much as possible, surfacing the goodness so you be a hero. -## Difference from version 2.0 - -- Fixed a number of issues surrounding dates etc -- Moved to a more Elixiry way of returning results, using `{:ok, result}` and `{:error, error}`. We were always doing the latter, but decided to move to the former to keep in step with other libraries. -- Moved to Elixir 1.4, along with Dates etc. -- Removed multiple dependencies, including Timex and Poolboy (which is built into the driver, Postgrex) - - ## Documentation API documentation is available at http://hexdocs.pm/moebius -## Building docs from source - -```bash -$ MIX_ENV=dev mix docs -``` - ## Installation Installing Moebius involves a few small steps: @@ -43,7 +20,7 @@ Installing Moebius involves a few small steps: end ``` - 2. Add the db child process to your `Application` module: + 2. Add the db child process to your `Application` module's supervision tree: ```elixir children = [ @@ -187,12 +164,13 @@ The API is built around the concept of transforming raw data from your database This returns a user with the id of 1. ```elixir -{:ok, result} = db(:users) - |> filter(name: "Steve") - |> sort(:city, :desc) - |> limit(10) - |> offset(2) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(name: "Steve") + |> sort(:city, :desc) + |> limit(10) + |> offset(2) + |> Moebius.Db.run ``` Hopefully it's fairly straightforward what this query returns. All users named Steve sorted by city... skipping the first two, returning the next 10. @@ -203,31 +181,35 @@ Hopefully it's fairly straightforward what this query returns. All users named S An "=" (Equal) query happens when you pass a column name and a value: ```elixir -{:ok, result} = db(:users) - |> filter(name: "mark") - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(name: "mark") + |> Moebius.Db.run # or, if you want to be more precise, specify the `eq` key: -{:ok, result} = db(:users) - |> filter(:name, eq: "mark"]) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:name, eq: "mark"]) + |> Moebius.Db.run ``` A "!=" (Not Equal) query happens when you specify the `neq` key: ```elixir -{:ok, result} = db(:users) - |> filter(:name, neq: "mark") - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:name, neq: "mark") + |> Moebius.Db.run ``` A ">" (Greater Than) query happens when you specify the `gt` key: ```elixir -{:ok, result} = db(:users) - |> filter(:order_count, gt: 5) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:order_count, gt: 5) + |> Moebius.Db.run ``` Additionally, the following comparison operators are available: @@ -239,23 +221,26 @@ Additionally, the following comparison operators are available: An "IN" query happens when you pass an array: ```elixir -{:ok, result} = db(:users) - |> filter(:name, ["mark", "biff", "skip"]) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:name, ["mark", "biff", "skip"]) + |> Moebius.Db.run # or, if you want to be more precise, specify the `in` key: -{:ok, result} = db(:users) - |> filter(:name, in: ["mark", "biff", "skip"]) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:name, in: ["mark", "biff", "skip"]) + |> Moebius.Db.run ``` A "NOT IN" query happens when you specify the `not_in` or `nin` key: ```elixir -{:ok, result} = db(:users) - |> filter(:name, not_in: ["mark", "biff", "skip"]) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(:name, not_in: ["mark", "biff", "skip"]) + |> Moebius.Db.run ``` If you prefer a more SQL-like syntax, you can use the following aliases: @@ -265,10 +250,11 @@ If you prefer a more SQL-like syntax, you can use the following aliases: - sort: `order_by` ```elixir -{:ok, result} = from(:users) - |> where(name: "Steve") - |> where(:order_count, gt: 5) - |> order_by(id: :asc, name: :desc) +{:ok, result} = + from(:users) + |> where(name: "Steve") + |> where(:order_count, gt: 5) + |> order_by(id: :asc, name: :desc) ``` If you don't want to deal with my abstractions, just use SQL: @@ -282,9 +268,10 @@ If you don't want to deal with my abstractions, just use SQL: One of the great features of PostgreSQL is the ability to do intelligent full text searches. We support this functionality directly: ```elixir -{:ok, result} = db(:users) - |> search(for: "Mike", in: [:first, :last, :email]) - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> search(for: "Mike", in: [:first, :last, :email]) + |> Moebius.Db.run ``` The `search` function builds a `tsvector` search on the fly for you and executes it over the columns you send in. The results are ordered in descending order using `ts_rank`. @@ -298,7 +285,8 @@ Start by importing `Moebius.DocumentQuery` and saving a document: ```elixir import Moebius.DocumentQuery -{:ok, new_user} = db(:friends) +{:ok, new_user} = + db(:friends) |> Moebius.Db.save(email: "test@test.com", name: "Moe Test") ``` @@ -323,7 +311,8 @@ The entire `DocumentQuery` module works off the premise that this is how you wil ```elixir import Moebius.DocumentQuery -{:ok, new_user} = db(:friends) +{:ok, new_user} = + db(:friends) |> searchable([:name]) |> Moebius.Db.save(email: "test@test.com", name: "Moe Test") ``` @@ -333,7 +322,8 @@ By specifying the searchable fields, the `search` field will be updated with the Now, we can query our document using full text indexing which is optimized to use the GIN index created above: ```elixir -{:ok, user} = db(:friends) +{:ok, user} = + db(:friends) |> search("test.com") |> Moebius.Db.run ``` @@ -341,7 +331,8 @@ Now, we can query our document using full text indexing which is optimized to us Or we can do a simple filter: ```elixir -{:ok, user} = db(:friends) +{:ok, user} = + db(:friends) |> contains(email: "test@test.com") |> Moebius.Db.run ``` @@ -349,7 +340,8 @@ Or we can do a simple filter: This query is optimized to use the `@` (or "contains" operator), using the *other* GIN index specified above. There's more we can do... ```elixir -{:ok, users} = db(:friends) +{:ok, users} = + db(:friends) |> filter(:money_spent, ">", 100) |> Moebius.Db.run ``` @@ -357,7 +349,8 @@ This query is optimized to use the `@` (or "contains" operator), using the *othe This runs a full table scan so is not terribly optimal, but it does work if you need it once in a while. You can also use the existence (`?`) operator, which is very handy for querying arrays. In the library, it is implemented as `exists`: ```elixir -{:ok, buddies} = db(:friends) +{:ok, buddies} = + db(:friends) |> exists(:tags, "best") |> Moebius.Db.run ``` @@ -401,42 +394,46 @@ I highly recommend this approach if you have some difficult SQL you want to writ Inserting is pretty straightforward: ```elixir -{:ok, result} = db(:users) - |> insert(email: "test@test.com", first: "Test", last: "User") - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> insert(email: "test@test.com", first: "Test", last: "User") + |> Moebius.Db.run ``` Updating can work over multiple rows, or just one, depending on the filter you use: ```elixir -{:ok, result} = db(:users) - |> filter(id: 1) - |> update(email: "maggot@test.com") - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter(id: 1) + |> update(email: "maggot@test.com") + |> Moebius.Db.run ``` The filter can be a single record, or affect multiple records: ```elixir -{:ok, result} = db(:users) - |> filter("id > 100") - |> update(email: "test@test.com") - |> Moebius.Db.run - -{:ok, result} = db(:users) - |> filter("email LIKE $2", "%test") - |> update(email: "ox@test.com") - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter("id > 100") + |> update(email: "test@test.com") + |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter("email LIKE $2", "%test") + |> update(email: "ox@test.com") + |> Moebius.Db.run ``` Deleting works exactly the same way as `update`, but returns the count of deleted items in the result: ```elixir -{:ok, result} = db(:users) - |> filter("email LIKE $2", "%test") - |> delete - |> Moebius.Db.run +{:ok, result} = + db(:users) + |> filter("email LIKE $2", "%test") + |> delete + |> Moebius.Db.run #result.deleted = 10, for instance ``` @@ -451,7 +448,8 @@ A bulk insert works by invoking one directly: ```elixir data = [#let's say 10,000 records or so] -{:ok, result} = db(:people) +{:ok, result} = + db(:people) |> bulk_insert(data) |> Moebius.Db.transact_batch ``` @@ -473,20 +471,22 @@ Table joins can be applied for a single join or piped to create multiple joins. The simplest example is a basic join: ```elixir -{:ok, result} = db(:customers) - |> join(:orders) - |> select - |> Moebius.Db.run +{:ok, result} = + db(:customer) + |> join(:order) + |> select + |> Moebius.Db.run ``` For multiple table joins you can specify the table that you want to join on: ```elixir -{:ok, result} = db(:customers) - |> join(:orders, on: :customers) - |> join(:items, on: :orders) - |> select - |> Moebius.Db.run +{:ok, result} = + db(:customer) + |> join(:order, on: :customer) + |> join(:item, on: :order) + |> select + |> Moebius.Db.run ``` ## Transactions @@ -495,7 +495,8 @@ Transactions are facilitated by using a callback that has a `pid` on it, which y ```elixir {:ok, result} = transaction fn(pid) -> - new_user = db(:users) + new_user = + db(:users) |> insert(pid, email: "frodo@test.com") |> Moebius.Db.run(pid) @@ -515,7 +516,8 @@ Aggregates are built with a functional approach in mind. This might seem a bit o So, to that end, we have: ```elixir -{:ok, sum} = db(:products) +{:ok, sum} = + db(:products) |> map("id > 1") |> group(:sku) |> reduce(:sum, :id) @@ -540,6 +542,15 @@ You get the idea. If your function only returns one thing, you can specify you d {:ok, no_party} = function(:bad_time, :single [me]) |> Moebius.Db.run ``` +## Test + +You'll need a local postgres instance running. + +```bash +MIX_ENV=test mix moebius.setup +MIX_ENV=test mix test +``` + ## Help? I would love to have your help! I do ask that if you do find a bug, please add a test to your PR that shows the bug and how it was fixed. diff --git a/config/config.exs b/config/config.exs index a3916b0..871a3d1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,15 +1,3 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. import Config -config :moebius, connection: [ - #database: "meebuss" - url: "postgres://localhost/meebuss" -], -test_db: [ - database: "meebuss" -], -chinook: [ - database: "chinook" -], -scripts: "test/db" +import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..e69de29 diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..af144e9 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,7 @@ +import Config + +config :moebius, + connection: [ + url: "postgres://postgres:postgres@localhost:5432/moebius_test" + ], + scripts: "test/db" diff --git a/lib/mix/tasks/helpers.ex b/lib/mix/tasks/helpers.ex new file mode 100644 index 0000000..496a604 --- /dev/null +++ b/lib/mix/tasks/helpers.ex @@ -0,0 +1,24 @@ +defmodule Mix.Tasks.Helpers do + @moduledoc false + def create_database(database_name) do + "-c \"CREATE DATABASE #{database_name};\"" + |> database_cmd() + end + + def drop_database(database_name) do + "-c \"DROP DATABASE #{database_name};\"" + |> database_cmd() + end + + defp database_cmd(cmd) do + db_cmd = "psql -U postgres " <> cmd + + case System.shell(db_cmd) do + {output, 0} -> + output + + {output, _status} -> + output + end + end +end diff --git a/lib/mix/tasks/moebius.create.ex b/lib/mix/tasks/moebius.create.ex new file mode 100644 index 0000000..8a26a49 --- /dev/null +++ b/lib/mix/tasks/moebius.create.ex @@ -0,0 +1,16 @@ +defmodule Mix.Tasks.Moebius.Create do + @moduledoc """ + Creates the database + """ + use Mix.Task + + alias Mix.Tasks.Helpers + + def run(_args) do + Mix.Task.run("app.start") + + Moebius.get_connection() + |> Keyword.get(:database) + |> Helpers.create_database() + end +end diff --git a/lib/mix/tasks/moebius.drop.ex b/lib/mix/tasks/moebius.drop.ex new file mode 100644 index 0000000..3f8c86d --- /dev/null +++ b/lib/mix/tasks/moebius.drop.ex @@ -0,0 +1,16 @@ +defmodule Mix.Tasks.Moebius.Drop do + @moduledoc """ + Drops the database + """ + use Mix.Task + + alias Mix.Tasks.Helpers + + def run(_args) do + Mix.Task.run("app.start") + + Moebius.get_connection() + |> Keyword.get(:database) + |> Helpers.drop_database() + end +end diff --git a/lib/mix/tasks/moebius.migrate.ex b/lib/mix/tasks/moebius.migrate.ex new file mode 100644 index 0000000..bf94870 --- /dev/null +++ b/lib/mix/tasks/moebius.migrate.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Moebius.Migrate do + @moduledoc """ + Migrates the database + """ + use Mix.Task + + def run(_args) do + Mix.Task.run("app.start") + + Moebius.get_connection() + |> migrate_database() + end + + defp migrate_database(opts) do + case Mix.env() do + :test -> + "test/db/tables.sql" + |> File.read!() + |> Moebius.run_with_psql(opts) + + _ -> + raise "You can only run migrations in the test environment" + end + end +end diff --git a/lib/mix/tasks/moebius.seed.ex b/lib/mix/tasks/moebius.seed.ex new file mode 100644 index 0000000..f078f36 --- /dev/null +++ b/lib/mix/tasks/moebius.seed.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Moebius.Seed do + @moduledoc """ + Seeds the database + """ + use Mix.Task + + def run(_args) do + Mix.Task.run("app.start") + + Moebius.get_connection() + |> seed_database() + end + + defp seed_database(opts) do + case Mix.env() do + :test -> + "test/db/seeds.sql" + |> File.read!() + |> Moebius.run_with_psql(opts) + + _ -> + raise "You can only run seeds in the test environment" + end + end +end diff --git a/lib/moebius.ex b/lib/moebius.ex index 50d4626..f70be35 100644 --- a/lib/moebius.ex +++ b/lib/moebius.ex @@ -1,13 +1,13 @@ -#this is the default database that is entirely optional +# this is the default database that is entirely optional defmodule Moebius.Db do use Moebius.Database end defmodule Moebius do - use Application + def start(_type, _args) do - Moebius.get_connection |> Moebius.Db.start_link + Moebius.get_connection() |> Moebius.Db.start_link() end @doc """ @@ -16,40 +16,64 @@ defmodule Moebius do one command per query. """ def run_with_psql(sql, opts) do - db = opts[:database] || opts[:db] - host = opts[:host] || "localhost" - port = opts[:port] || "5432" + port = if is_binary(opts[:port]), do: opts[:port], else: to_string(opts[:port]) - args = ["-h", host, "-d", db, "-p", port, "-c", sql,"--quiet", "--set", "ON_ERROR_STOP=1", "--no-psqlrc"] + args = + [ + "-h", + opts[:hostname], + "-d", + opts[:database], + "-p", + port, + "-c", + sql, + "--quiet", + "--set", + "ON_ERROR_STOP=1", + "--no-psqlrc" + ] - env = [] - env = cond do - Keyword.has_key?(opts, :user) -> [{"PGUSER", opts[:user]} | env] - true -> env - end + env = set_env(opts) - env = cond do - Keyword.has_key?(opts, :password) -> [{"PGPASSWORD", opts[:password]} | env] - true -> env - end + System.cmd("psql", args, env: env) + end + + def set_env(opts) do + cond do + Keyword.has_key?(opts, :username) and Keyword.has_key?(opts, :password) -> + [{"PGUSER", opts[:username]}, {"PGPASSWORD", opts[:password]}] + + Keyword.has_key?(opts, :username) -> + [{"PGUSER", opts[:username]}] + + Keyword.has_key?(opts, :password) -> + [{"PGPASSWORD", opts[:password]}] - System.cmd "psql", args, env: env + true -> + [] + end end def get_connection(), do: get_connection(:connection) + def pool_opts do [pool: DBConnection.ConnectionPool] end + def get_connection(key) when is_atom(key) do opts = Application.get_env(:moebius, key) - opts = cond do - Keyword.has_key?(opts, :url) -> Keyword.merge(opts, parse_connection(opts[:url])) - true -> opts - end + + opts = + cond do + Keyword.has_key?(opts, :url) -> Keyword.merge(opts, parse_connection(opts[:url])) + true -> opts + end + opts ++ pool_opts() end - #thanks to the Ecto team for this code! + # thanks to the Ecto team for this code! def parse_connection(url) when is_binary(url) do info = url |> URI.decode() |> URI.parse() @@ -64,16 +88,17 @@ defmodule Moebius do destructure [username, password], info.userinfo && String.split(info.userinfo, ":") "/" <> database = info.path - opts = [username: username, - password: password, - database: database, - hostname: info.host, - port: info.port] + opts = [ + username: username, + password: password, + database: database, + hostname: info.host, + port: info.port + ] - #strip off any nils + # strip off any nils Enum.reject(opts, fn {_k, v} -> is_nil(v) end) - #send the values to a char list because that's what :epgsql likes + # send the values to a char list because that's what :epgsql likes # opts = for {k, v} <- opts, into: %{}, do: {k, String.to_char_list(v)} end - end diff --git a/lib/moebius/bulk_command.ex b/lib/moebius/bulk_command.ex index c2b01a7..1d917af 100644 --- a/lib/moebius/bulk_command.ex +++ b/lib/moebius/bulk_command.ex @@ -1,7 +1,5 @@ defmodule Moebius.BulkCommand do - defstruct [ - table_name: nil, - columns: [], - values: [] - ] + defstruct table_name: nil, + columns: [], + values: [] end diff --git a/lib/moebius/command_batch.ex b/lib/moebius/command_batch.ex index da7a3ee..9cd8eb6 100644 --- a/lib/moebius/command_batch.ex +++ b/lib/moebius/command_batch.ex @@ -1,5 +1,3 @@ defmodule Moebius.CommandBatch do - defstruct [ - commands: [], - ] + defstruct commands: [] end diff --git a/lib/moebius/database.ex b/lib/moebius/database.ex index 74d51b3..586c9ff 100644 --- a/lib/moebius/database.ex +++ b/lib/moebius/database.ex @@ -1,19 +1,17 @@ defmodule Moebius.Database do - defmacro __using__(_opts) do quote location: :keep do @name __MODULE__ alias __MODULE__ - def start_link(opts) do + def start_link(opts) do opts - |> prepare_extensions - |> Moebius.Database.start_link - + |> prepare_extensions + |> Moebius.Database.start_link() end - def child_spec([]), do: child_spec(Moebius.get_connection) + def child_spec([]), do: child_spec(Moebius.get_connection()) def child_spec(arg) do %{ @@ -22,108 +20,132 @@ defmodule Moebius.Database do } end - def prepare_extensions(opts) do - - #make sure we convert a tuple list, which will happen if our db is a worker - opts = cond do - Keyword.keyword?(opts) -> opts - true -> Keyword.new([opts]) - end + # make sure we convert a tuple list, which will happen if our db is a worker + opts = + cond do + Keyword.keyword?(opts) -> opts + true -> Keyword.new([opts]) + end opts - |> Keyword.put_new(:name, @name) - |> Keyword.put_new(:types, PostgresTypes) + |> Keyword.put_new(:name, @name) + |> Keyword.put_new(:types, PostgresTypes) end def run(sql) when is_binary(sql), do: run(sql, []) - def run(sql, params) when is_binary(sql) and is_list(params), do: %Moebius.QueryCommand{sql: sql, params: params} |> run - def run(sql, %DBConnection{} = conn) when is_binary(sql), do: %Moebius.QueryCommand{sql: sql, params: []} |> run(conn) - def run(sql, %DBConnection{} = conn, params) when is_binary(sql), do: %Moebius.QueryCommand{sql: sql, params: params} |> run(conn) + def run(sql, params) when is_binary(sql) and is_list(params), + do: %Moebius.QueryCommand{sql: sql, params: params} |> run + + def run(sql, %DBConnection{} = conn) when is_binary(sql), + do: %Moebius.QueryCommand{sql: sql, params: []} |> run(conn) + + def run(sql, %DBConnection{} = conn, params) when is_binary(sql), + do: %Moebius.QueryCommand{sql: sql, params: params} |> run(conn) + + def run(%Moebius.QueryCommand{type: :insert} = cmd), + do: execute(cmd) |> Moebius.Transformer.to_single() + + def run(%Moebius.QueryCommand{type: :update} = cmd), + do: execute(cmd) |> Moebius.Transformer.to_single() + + def run(%Moebius.QueryCommand{type: :delete} = cmd), + do: execute(cmd) |> Moebius.Transformer.to_single() + + def run(%Moebius.QueryCommand{type: :count} = cmd), + do: execute(cmd) |> Moebius.Transformer.to_single() + + def run(%Moebius.QueryCommand{} = cmd), do: execute(cmd) |> Moebius.Transformer.to_list() + + def run(%Moebius.QueryCommand{type: :insert} = cmd, %DBConnection{} = conn), + do: execute(cmd, conn) |> Moebius.Transformer.to_single() + + def run(%Moebius.QueryCommand{type: :update} = cmd, %DBConnection{} = conn), + do: execute(cmd, conn) |> Moebius.Transformer.to_single() - def run(%Moebius.QueryCommand{type: :insert} = cmd), do: execute(cmd) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{type: :update} = cmd), do: execute(cmd) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{type: :delete} = cmd), do: execute(cmd) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{type: :count} = cmd), do: execute(cmd) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{} = cmd), do: execute(cmd) |> Moebius.Transformer.to_list + def run(%Moebius.QueryCommand{type: :delete} = cmd, %DBConnection{} = conn), + do: execute(cmd, conn) |> Moebius.Transformer.to_single() - def run(%Moebius.QueryCommand{type: :insert} = cmd, %DBConnection{} = conn), do: execute(cmd, conn) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{type: :update} = cmd, %DBConnection{} = conn), do: execute(cmd, conn) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{type: :delete} = cmd, %DBConnection{} = conn), do: execute(cmd, conn) |> Moebius.Transformer.to_single - def run(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn), do: execute(cmd, conn) |> Moebius.Transformer.to_list + def run(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn), + do: execute(cmd, conn) |> Moebius.Transformer.to_list() defdelegate all(table), to: __MODULE__, as: :run def run_batch(%Moebius.CommandBatch{} = batch) do batch.commands - |> Enum.map(fn(cmd) -> execute(cmd) end) + |> Enum.map(fn cmd -> execute(cmd) end) end def transact_batch(%Moebius.CommandBatch{} = batch) do - transaction fn(tx) -> + transaction(fn tx -> batch.commands - |> Enum.map(fn(cmd) -> execute(cmd, tx) end) - end + |> Enum.map(fn cmd -> execute(cmd, tx) end) + end) end def run(%Moebius.DocumentCommand{sql: nil} = cmd) do - res = %{cmd | conn: @name} - |> Moebius.DocumentQuery.select - |> Moebius.Database.execute - |> Moebius.Transformer.from_json + res = + %{cmd | conn: @name} + |> Moebius.DocumentQuery.select() + |> Moebius.Database.execute() + |> Moebius.Transformer.from_json() end def run(%Moebius.DocumentCommand{} = cmd) do - execute(cmd) - |> Moebius.Transformer.from_json + execute(cmd) + |> Moebius.Transformer.from_json() end def first(%Moebius.DocumentCommand{} = cmd) do Moebius.DocumentQuery.select(cmd) - |> execute - |> Moebius.Transformer.from_json(:single) + |> execute + |> Moebius.Transformer.from_json(:single) end def first(%Moebius.QueryCommand{sql: nil} = cmd) do Moebius.Query.select(cmd) - |> execute - |> Moebius.Transformer.to_single + |> execute + |> Moebius.Transformer.to_single() end def first(%Moebius.QueryCommand{} = cmd) do cmd - |> execute - |> Moebius.Transformer.to_single + |> execute + |> Moebius.Transformer.to_single() end defdelegate one(table), to: __MODULE__, as: :first def find(%Moebius.QueryCommand{} = cmd, id) do sql = "select * from #{cmd.table_name} where id=#{id}" + %{cmd | sql: sql} - |> execute - |> Moebius.Transformer.to_single + |> execute + |> Moebius.Transformer.to_single() end def find(%Moebius.DocumentCommand{} = cmd, id) do - sql = "select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where id=$1" + sql = + "select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where id=$1" + %{cmd | sql: sql, params: [id]} - |> execute - |> Moebius.Transformer.from_json(:single) + |> execute + |> Moebius.Transformer.from_json(:single) end def transaction(fun) do try do - {:ok, conn} = Postgrex.transaction(@name, fun, Moebius.get_connection) + {:ok, conn} = Postgrex.transaction(@name, fun, Moebius.get_connection()) conn catch e, %{message: message} -> {:error, message} - e, {:error, message} -> {:error, message} + e, {:error, message} -> {:error, message} end end - def save(%Moebius.DocumentCommand{} = cmd, doc) when is_list(doc), do: save(cmd, Enum.into(doc, %{})) + def save(%Moebius.DocumentCommand{} = cmd, doc) when is_list(doc), + do: save(cmd, Enum.into(doc, %{})) def save(%Moebius.DocumentCommand{} = cmd, doc) when is_struct(doc) do case save(%Moebius.DocumentCommand{} = cmd, Map.from_struct(doc)) do @@ -133,24 +155,22 @@ defmodule Moebius.Database do end def save(%Moebius.DocumentCommand{} = cmd, doc) when is_map(doc) do - res = %{cmd | conn: @name} + res = + %{cmd | conn: @name} |> Moebius.DocumentQuery.decide_command(doc) - |> Moebius.Database.execute + |> Moebius.Database.execute() |> Moebius.Transformer.from_json(:single) |> handle_save_result(cmd, doc) |> check_struct(doc) - end def save(%Moebius.DocumentCommand{} = cmd, doc, %DBConnection{} = conn) when is_map(doc) do - %{cmd | conn: @name} - |> Moebius.DocumentQuery.decide_command(doc) - |> Moebius.Database.execute(conn) - |> Moebius.Transformer.from_json(:single) - |> handle_save_result(cmd, doc) - |> check_struct(doc) - + |> Moebius.DocumentQuery.decide_command(doc) + |> Moebius.Database.execute(conn) + |> Moebius.Transformer.from_json(:single) + |> handle_save_result(cmd, doc) + |> check_struct(doc) end def create_document_table(name) when is_atom(name) do @@ -159,8 +179,8 @@ defmodule Moebius.Database do %Moebius.DocumentCommand{} = cmd -> {:ok, "Table created"} end end - def create_document_table(%Moebius.DocumentCommand{} = cmd, _) do + def create_document_table(%Moebius.DocumentCommand{} = cmd, _) do sql = """ create table #{cmd.table_name}( id serial primary key not null, @@ -172,39 +192,65 @@ defmodule Moebius.Database do """ %Moebius.QueryCommand{conn: @name, sql: sql} |> execute - %Moebius.QueryCommand{conn: @name, sql: "create index idx_#{cmd.table_name}_search on #{cmd.table_name} using GIN(search);"} |> execute - %Moebius.QueryCommand{conn: @name, sql: "create index idx_#{cmd.table_name} on #{cmd.table_name} using GIN(body jsonb_path_ops);"} |> execute + + %Moebius.QueryCommand{ + conn: @name, + sql: "create index idx_#{cmd.table_name}_search on #{cmd.table_name} using GIN(search);" + } + |> execute + + %Moebius.QueryCommand{ + conn: @name, + sql: + "create index idx_#{cmd.table_name} on #{cmd.table_name} using GIN(body jsonb_path_ops);" + } + |> execute + cmd end - defp check_struct({:ok, query_result} = res, original) do - res = cond do - Map.has_key?(original, :__struct__) -> Map.put_new(query_result, :__struct__, original.__struct__) - true -> query_result - end + res = + cond do + Map.has_key?(original, :__struct__) -> + Map.put_new(query_result, :__struct__, original.__struct__) + + true -> + query_result + end + {:ok, res} end - defp handle_save_result({:ok, save_result}=res, cmd, doc) when is_map(save_result), do: update_search(res, cmd) && res + defp handle_save_result({:ok, save_result} = res, cmd, doc) when is_map(save_result), + do: update_search(res, cmd) && res + defp handle_save_result({:error, err}, cmd, doc) do table = cmd.table_name + cond do - String.contains? err, "column" -> raise err - String.contains? err, "does not exist" -> create_document_table(cmd, doc) |> save(Map.delete(doc, :id)) - true -> {:error, err} + String.contains?(err, "column") -> + raise err + + String.contains?(err, "does not exist") -> + create_document_table(cmd, doc) |> save(Map.delete(doc, :id)) + + true -> + {:error, err} end end defp execute(%Moebius.DocumentCommand{sql: nil} = cmd) do %{cmd | conn: @name} - |> Moebius.DocumentQuery.select - |> Moebius.Database.execute + |> Moebius.DocumentQuery.select() + |> Moebius.Database.execute() end defp execute(%Moebius.DocumentCommand{} = cmd) do - res = %{cmd | conn: @name} - |> Moebius.Database.execute + res = + %{cmd | conn: @name} + |> Moebius.Database.execute() + case res do {:error, err} -> create_document_table(cmd, nil) && execute(cmd) res -> res @@ -213,36 +259,34 @@ defmodule Moebius.Database do defp execute(%Moebius.QueryCommand{sql: nil} = cmd) do %{cmd | conn: @name} - |> Moebius.Query.select - |> Moebius.Database.execute - + |> Moebius.Query.select() + |> Moebius.Database.execute() end defp execute(%Moebius.QueryCommand{} = cmd) do %{cmd | conn: @name} - |> Moebius.Database.execute + |> Moebius.Database.execute() end - defp execute(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn), do: Moebius.Database.execute(cmd, conn) - - + defp execute(%Moebius.QueryCommand{} = cmd, %DBConnection{} = conn), + do: Moebius.Database.execute(cmd, conn) defp update_search({:error, err}, cmd), do: {:error, err} - defp update_search([], _), do: [] - defp update_search({:ok, query_result} = res, cmd) do + defp update_search([], _), do: [] + defp update_search({:ok, query_result} = res, cmd) do if length(cmd.search_fields) > 0 do terms = Enum.map_join(cmd.search_fields, ", ' ', ", &"body -> '#{Atom.to_string(&1)}'") - sql = "update #{cmd.table_name} set search = to_tsvector(concat(#{terms})) where id=#{query_result.id}" - %Moebius.QueryCommand{sql: sql} - |> execute + sql = + "update #{cmd.table_name} set search = to_tsvector(concat(#{terms})) where id=#{query_result.id}" + %Moebius.QueryCommand{sql: sql} + |> execute end res end - end end @@ -250,13 +294,14 @@ defmodule Moebius.Database do Postgrex.start_link(opts) end - def execute(cmd) do - case Postgrex.query(cmd.conn, cmd.sql, cmd.params, Moebius.pool_opts) do - {:ok, result} -> {:ok, result} - {:error, err} -> {:error, err.postgres.message} - end + case Postgrex.query(cmd.conn, cmd.sql, cmd.params, Moebius.pool_opts()) do + {:ok, result} -> + {:ok, result} + {:error, err} -> + {:error, err.postgres.message} + end end @doc """ @@ -264,11 +309,12 @@ defmodule Moebius.Database do it will be caught in `Query.transaction/1` and reported back using `{:error, err}`. """ def execute(cmd, %DBConnection{} = conn) do - case Postgrex.query(conn, cmd.sql, cmd.params, Moebius.pool_opts) do - {:ok, result} -> {:ok, result} - {:error, err} -> Postgrex.query(conn, "ROLLBACK", []) && raise err.postgres.message + case Postgrex.query(conn, cmd.sql, cmd.params, Moebius.pool_opts()) do + {:ok, result} -> + {:ok, result} + + {:error, err} -> + Postgrex.query(conn, "ROLLBACK", []) && raise err.postgres.message end end - - end diff --git a/lib/moebius/document_command.ex b/lib/moebius/document_command.ex index a4e7b31..8b39589 100644 --- a/lib/moebius/document_command.ex +++ b/lib/moebius/document_command.ex @@ -1,17 +1,15 @@ defmodule Moebius.DocumentCommand do - defstruct [ - pid: nil, - table_name: nil, - type: :select, - sql: nil, - params: [], - json_field: "body", - where: "", - order: "", - limit: "", - offset: "", - search_fields: [], - group_by: nil, - conn: nil - ] + defstruct pid: nil, + table_name: nil, + type: :select, + sql: nil, + params: [], + json_field: "body", + where: "", + order: "", + limit: "", + offset: "", + search_fields: [], + group_by: nil, + conn: nil end diff --git a/lib/moebius/document_query.ex b/lib/moebius/document_query.ex index 8e5a320..19657cb 100644 --- a/lib/moebius/document_query.ex +++ b/lib/moebius/document_query.ex @@ -1,5 +1,4 @@ defmodule Moebius.DocumentQuery do - @moduledoc """ If you like your Postgres doing document goodness, then you'll want to use this interface. Just include it in your module and you can work directly with JSONB in PostgreSQL. We've tried to keep reasonable parity with @@ -70,7 +69,7 @@ defmodule Moebius.DocumentQuery do map = Enum.into(criteria, %{}) encoded = Jason.encode!(map) - #TODO: Do we need to parameterize this? I don't think so + # TODO: Do we need to parameterize this? I don't think so where = " where #{cmd.json_field} @> '#{encoded}'" %{cmd | where: where, params: []} end @@ -88,11 +87,11 @@ defmodule Moebius.DocumentQuery do ``` """ def filter(%DocumentCommand{} = cmd, criteria, params \\ []) when is_bitstring(criteria) do - - param_list = cond do - is_list(params) -> params - true -> [params] - end + param_list = + cond do + is_list(params) -> params + true -> [params] + end where = " where #{criteria}" %{cmd | where: where, params: param_list} @@ -111,10 +110,12 @@ defmodule Moebius.DocumentQuery do ``` """ def filter(%DocumentCommand{} = cmd, field, operator, params) do - param_list = cond do - is_list(params) -> params - true -> [params] - end + param_list = + cond do + is_list(params) -> params + true -> [params] + end + where = " where body -> '#{field}' #{operator} $1" %{cmd | where: where, params: param_list} end @@ -132,10 +133,12 @@ defmodule Moebius.DocumentQuery do ``` """ def exists(%DocumentCommand{} = cmd, field, params) do - param_list = cond do - is_list(params) -> params - true -> [params] - end + param_list = + cond do + is_list(params) -> params + true -> [params] + end + where = " where body -> '#{field}' ? $1" %{cmd | where: where, params: param_list} end @@ -152,6 +155,7 @@ defmodule Moebius.DocumentQuery do #{cmd.limit} #{cmd.offset}; """ + %{cmd | sql: sql} end @@ -159,6 +163,7 @@ defmodule Moebius.DocumentQuery do Alias for Query limit """ def limit(%DocumentCommand{} = cmd, len), do: Moebius.Query.limit(cmd, len) + @doc """ Alias for Query offset """ @@ -177,22 +182,23 @@ defmodule Moebius.DocumentQuery do ``` """ def sort(%DocumentCommand{} = cmd, cols, direction \\ :asc) do - order_column = cond do - is_atom(cols) -> Atom.to_string cols - true -> cols - end - sort_dir = Atom.to_string direction + order_column = + cond do + is_atom(cols) -> Atom.to_string(cols) + true -> cols + end + + sort_dir = Atom.to_string(direction) %{cmd | order: " order by body -> '#{order_column}' #{sort_dir}"} end def decide_command(%DocumentCommand{} = cmd, doc) do cond do - Map.has_key?(doc, :id) && doc.id !=nil -> update(cmd, Map.delete(doc, :id), doc.id) - true -> insert(cmd, Map.delete(doc, :id)) + Map.has_key?(doc, :id) && doc.id != nil -> update(cmd, Map.delete(doc, :id), doc.id) + true -> insert(cmd, Map.delete(doc, :id)) end end - @doc """ Marks a set of fields for indexing during save. @@ -205,7 +211,8 @@ defmodule Moebius.DocumentQuery do |> save(product) ``` """ - def searchable({:error, err}), do: {:error ,err} + def searchable({:error, err}), do: {:error, err} + def searchable(%DocumentCommand{} = cmd, search_params) when is_list(search_params) do %{cmd | search_fields: search_params} end @@ -223,18 +230,20 @@ defmodule Moebius.DocumentQuery do - Deletes a document based on the filter (if any) - Deletes a document with the given id """ - def delete(%DocumentCommand{} = cmd), do: cmd |> delete_command - def delete(%DocumentCommand{} = cmd, pid) when is_pid(pid), do: cmd |> delete_command + def delete(%DocumentCommand{} = cmd), do: cmd |> delete_command + def delete(%DocumentCommand{} = cmd, pid) when is_pid(pid), do: cmd |> delete_command def delete(%DocumentCommand{} = cmd, id), do: cmd |> delete_command(id) def delete(%DocumentCommand{} = cmd, pid, id) when is_pid(pid), do: cmd |> delete_command(id) def insert(%DocumentCommand{} = cmd, doc) do doc = Map.delete(doc, :created_at) |> Map.delete(:updated_at) + sql = """ insert into #{cmd.table_name}(#{cmd.json_field}) VALUES($1) RETURNING id, #{cmd.json_field}::text, created_at, updated_at; """ + %{cmd | sql: sql, params: [doc], type: :insert} end @@ -244,8 +253,8 @@ defmodule Moebius.DocumentQuery do # end def update(%DocumentCommand{} = cmd, change, id) when is_map(change) and is_integer(id) do - #{:ok, encoded} = Poison.encode(change) - #remove created/updated + # {:ok, encoded} = Poison.encode(change) + # remove created/updated change = Map.delete(change, :created_at) |> Map.delete(:updated_at) sql = """ @@ -254,10 +263,10 @@ defmodule Moebius.DocumentQuery do updated_at = now() where id = #{id} returning id, #{cmd.json_field}::text, created_at, updated_at; """ + %{cmd | sql: sql, type: :update, params: [change]} end - @doc """ Finds a document based on ID using the Primary Key index. An optimized query for finding a single document. @@ -268,13 +277,14 @@ defmodule Moebius.DocumentQuery do |> find(12) ``` """ - def find(%DocumentCommand{} = cmd, id) when is_integer id do - #no need to param this, it's an integer - sql = "select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where id=#{id}" + def find(%DocumentCommand{} = cmd, id) when is_integer(id) do + # no need to param this, it's an integer + sql = + "select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where id=#{id}" + %{cmd | sql: sql} end - @doc """ Performs a highly-tuned Full Text query on the indexed `search` column. This is set on `save/3`. @@ -295,8 +305,7 @@ defmodule Moebius.DocumentQuery do |> search(for: "test.com", in: [:email]) ``` """ - def search(%DocumentCommand{} = cmd, term) when is_bitstring(term) do - + def search(%DocumentCommand{} = cmd, term) when is_bitstring(term) do sql = """ select id, #{cmd.json_field}::text, created_at, updated_at from #{cmd.table_name} where search @@ to_tsquery($1) @@ -316,21 +325,19 @@ defmodule Moebius.DocumentQuery do """ %{cmd | sql: sql, params: [term]} - end - defp delete_command(%DocumentCommand{} = cmd, id) when is_integer(id) do - sql = "delete from #{cmd.table_name} where id=#{id} returning id, body::text, created_at, updated_at" + sql = + "delete from #{cmd.table_name} where id=#{id} returning id, body::text, created_at, updated_at" + %{cmd | sql: sql, type: :delete} end defp delete_command(%DocumentCommand{} = cmd) do - sql = "delete from #{cmd.table_name} #{cmd.where} returning id, body::text, created_at, updated_at;" + sql = + "delete from #{cmd.table_name} #{cmd.where} returning id, body::text, created_at, updated_at;" + %{cmd | sql: sql, type: :delete} end - - - - end diff --git a/lib/moebius/query.ex b/lib/moebius/query.ex index 6d62fd0..c233d49 100644 --- a/lib/moebius/query.ex +++ b/lib/moebius/query.ex @@ -1,9 +1,8 @@ defmodule Moebius.Query do + use Moebius.QueryFilter - import Inflex, only: [singularize: 1] alias Moebius.QueryCommand alias Moebius.CommandBatch - use Moebius.QueryFilter @moduledoc """ The main query interface for Moebius. Import this module into your code and query like a champ @@ -18,17 +17,20 @@ defmodule Moebius.Query do Example ``` - result = db(:users) + result = + db(:users) |> to_list - result = db("membership.users") + result = + db("membership.users") |> to_list ``` Or if you prefer more SQL-like syntax, you can use _from_, which is an alias for _db_: ``` - result = from(:users) + result = + from(:users) |> to_list ``` """ @@ -46,7 +48,8 @@ defmodule Moebius.Query do For example `now() as current_time, name, description`. Defaults to "*" Example: ``` - cheap_skate = db(:users) + cheap_skate = + db(:users) |> sort(:money_spent, :desc) |> last("first, last, email") ``` @@ -55,7 +58,6 @@ defmodule Moebius.Query do cmd |> sort(sort_by, :desc) |> select - end @doc """ @@ -66,24 +68,27 @@ defmodule Moebius.Query do Example of single order by: ``` - result = db(:users) - |> sort(:name, :desc) - |> to_list + result = + db(:users) + |> sort(:name, :desc) + |> to_list ``` Example of multiple order by: ``` - result = db(:users) - |> sort(id: :asc, name: :desc) - |> to_list + result = + db(:users) + |> sort(id: :asc, name: :desc) + |> to_list ``` Or if you prefer more SQL-like syntax, you can use "order_by", which is an alias for "sort": ``` - result = db(:users) - |> order_by(id: :asc, name: :desc) - |> to_list + result = + db(:users) + |> order_by(id: :asc, name: :desc) + |> to_list ``` """ def sort(%QueryCommand{} = cmd, col, dir) when is_atom(col) do @@ -116,9 +121,10 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) - |> limit(20) - |> to_list + result = + db(:users) + |> limit(20) + |> to_list ``` """ def limit(cmd, bound) when is_integer(bound), @@ -130,10 +136,11 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) - |> limit(20) - |> offset(2) - |> to_list + result = + db(:users) + |> limit(20) + |> offset(2) + |> to_list ``` """ def offset(cmd, n), @@ -145,10 +152,11 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) - |> limit(20) - |> skip(2) - |> to_list + result = + db(:users) + |> limit(20) + |> skip(2) + |> to_list ``` """ def skip(%QueryCommand{} = cmd, n), @@ -162,25 +170,29 @@ defmodule Moebius.Query do Example of String: ``` - command = db(:users) - |> limit(20) - |> offset(2) - |> select("now() as current_time, name, description") + command = + db(:users) + |> limit(20) + |> offset(2) + |> select("now() as current_time, name, description") #command is a QueryCommand object with all of the pipelined settings applied ``` Example of List: ``` - command = db(:users) - |> limit(20) - |> offset(2) - |> select([:name, :description]) + command = + db(:users) + |> limit(20) + |> offset(2) + |> select([:name, :description]) #command is a QueryCommand object with all of the pipelined settings applied ``` """ - def select(%QueryCommand{} = cmd, cols \\ "*") when is_bitstring(cols) do + def select(cmd, cols \\ "*") + + def select(%QueryCommand{} = cmd, cols) when is_bitstring(cols) do select_sql(cmd, cols) end @@ -189,7 +201,11 @@ defmodule Moebius.Query do end defp select_sql(cmd, cols) do - %{cmd | sql: "select #{cols} from #{cmd.table_name}#{cmd.join}#{cmd.where}#{cmd.order}#{cmd.limit}#{cmd.offset};"} + %{ + cmd + | sql: + "select #{cols} from #{cmd.table_name}#{cmd.join}#{cmd.where}#{cmd.order}#{cmd.limit}#{cmd.offset};" + } end @doc """ @@ -197,28 +213,33 @@ defmodule Moebius.Query do Example: - count = db(:users) - |> limit(20) - |> count - |> single + count = + db(:users) + |> limit(20) + |> count + |> single #count == 20 """ def count(%QueryCommand{} = cmd) do - %{cmd | type: :count, sql: "select count(1) from #{cmd.table_name}#{cmd.join}#{cmd.where}#{cmd.order}#{cmd.limit}#{cmd.offset};"} + %{ + cmd + | type: :count, + sql: + "select count(1) from #{cmd.table_name}#{cmd.join}#{cmd.where}#{cmd.order}#{cmd.limit}#{cmd.offset};" + } end - @doc """ Specifies a GROUP BY for a `map/reduce` (aggregate) query. cols - An atom indicating the column to GROUP BY. Will also be part of the SELECT list. - Example: ``` - result = db(:users) + result = + db(:users) |> map("money_spent > 100") |> group(:company) |> reduce(:sum, :money_spent) @@ -231,7 +252,8 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) + result = + db(:users) |> map("money_spent > 100") |> group("company, state") |> reduce(:sum, :money_spent) @@ -251,7 +273,8 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) + result = + db(:users) |> map("money_spent > 100") |> reduce(:sum, :money_spent) ``` @@ -268,7 +291,8 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) + result = + db(:users) |> map("money_spent > 100") |> reduce(:sum, :money_spent) ``` @@ -277,15 +301,16 @@ defmodule Moebius.Query do do: reduce(cmd, op, Atom.to_string(column)) def reduce(%QueryCommand{} = cmd, op, column) when is_bitstring(column) do - sql = cond do - cmd.group_by -> - "select #{op}(#{column}), #{cmd.group_by} from #{cmd.table_name}#{cmd.join}#{cmd.where} GROUP BY #{cmd.group_by}" - true -> - "select #{op}(#{column}) from #{cmd.table_name}#{cmd.join}#{cmd.where}" - end + sql = + cond do + cmd.group_by -> + "select #{op}(#{column}), #{cmd.group_by} from #{cmd.table_name}#{cmd.join}#{cmd.where} GROUP BY #{cmd.group_by}" - %{cmd | sql: sql} + true -> + "select #{op}(#{column}) from #{cmd.table_name}#{cmd.join}#{cmd.where}" + end + %{cmd | sql: sql} end @doc """ @@ -301,13 +326,15 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) - |> search(for: "Mike", in: [:first, :last, :email]) - |> run + result = + db(:users) + |> search(for: "Mike", in: [:first, :last, :email]) + |> run ``` """ - def search(%QueryCommand{} = cmd, for: term, in: columns) when is_list columns do + def search(%QueryCommand{} = cmd, for: term, in: columns) when is_list(columns) do concat_list = Enum.map_join(columns, ", ' ', ", &"#{&1}") + sql = """ select *, ts_rank_cd(to_tsvector(concat(#{concat_list})),to_tsquery($1)) as rank from #{cmd.table_name} where to_tsvector(concat(#{concat_list})) @@ to_tsquery($1) @@ -317,7 +344,6 @@ defmodule Moebius.Query do %{cmd | sql: sql, params: [term]} end - @doc """ Insert multiple rows at once, within a single transaction, returning the inserted records. Pass in a composite list, containing the rows to be inserted. Note, the columns to be inserted are defined based on the first record in the list. All records to be inserted must adhere to the same schema. @@ -338,7 +364,8 @@ defmodule Moebius.Query do def bulk_insert(%QueryCommand{} = cmd, list) when is_list(list) do # do this once and get a canonnical map for the records - - column_map = list |> hd |> Keyword.keys + column_map = list |> hd |> Keyword.keys() + cmd |> bulk_insert_batch(list, [], column_map) end @@ -352,8 +379,9 @@ defmodule Moebius.Query do column_count = length(column_map) max_records_per_command = div(max_params, column_count) - { current, next_batch } = Enum.split(list, max_records_per_command) + {current, next_batch} = Enum.split(list, max_records_per_command) new_cmd = bulk_insert_command(cmd, current, column_map) + case next_batch do [] -> %CommandBatch{commands: Enum.reverse([new_cmd | acc])} _ -> db(cmd.table_name) |> bulk_insert_batch(next_batch, [new_cmd | acc], column_map) @@ -364,31 +392,35 @@ defmodule Moebius.Query do column_count = length(column_map) row_count = length(list) - param_list = for row <- 0..row_count-1 do - list = (row * column_count + 1 .. (row * column_count) + column_count) - |> Enum.to_list - |> Enum.map_join(",", &"$#{&1}") - "(#{list})" - end + param_list = + for row <- 0..(row_count - 1) do + list = + (row * column_count + 1)..(row * column_count + column_count) + |> Enum.to_list() + |> Enum.map_join(",", &"$#{&1}") + + "(#{list})" + end params = for row <- list, {_k, v} <- row, do: v - column_names = Enum.map_join(column_map,", ", &"#{&1}") - value_sql = Enum.join param_list, "," + column_names = Enum.map_join(column_map, ", ", &"#{&1}") + value_sql = Enum.join(param_list, ",") sql = "insert into #{cmd.table_name}(#{column_names}) values #{value_sql};" %{cmd | sql: sql, params: params, type: :insert} end - @doc """ Creates an insert command based on the assembled pipeline """ def insert(%QueryCommand{} = cmd, criteria) do cols = Keyword.keys(criteria) vals = Keyword.values(criteria) - column_names = Enum.map_join(cols,", ", &"#{&1}") + column_names = Enum.map_join(cols, ", ", &"#{&1}") parameter_placeholders = Enum.map_join(1..length(cols), ", ", &"$#{&1}") - sql = "insert into #{cmd.table_name}(#{column_names}) values(#{parameter_placeholders}) returning *;" + + sql = + "insert into #{cmd.table_name}(#{column_names}) values(#{parameter_placeholders}) returning *;" %{cmd | sql: sql, params: vals, type: :insert} end @@ -397,27 +429,30 @@ defmodule Moebius.Query do Creates an update command based on the assembled pipeline. """ def update(%QueryCommand{} = cmd, criteria) do - cols = Keyword.keys(criteria) vals = Keyword.values(criteria) - {cols, col_count} = Enum.map_reduce cols, 1, fn col, acc -> - {"#{col} = $#{acc}", acc + 1} - end + {cols, col_count} = + Enum.map_reduce(cols, 1, fn col, acc -> + {"#{col} = $#{acc}", acc + 1} + end) - #here's something for John to clean up :):) - where = cond do + # here's something for John to clean up :):) + where = + cond do + length(cmd.where_columns) > 0 -> + {filters, _count} = + Enum.map_reduce(cmd.where_columns, col_count, fn col, acc -> + {"#{col} = $#{acc}", acc + 1} + end) - length(cmd.where_columns) > 0 -> - {filters, _count} = Enum.map_reduce cmd.where_columns, col_count, fn col, acc -> - {"#{col} = $#{acc}", acc + 1} - end - " where " <> Enum.join(filters, " and ") + " where " <> Enum.join(filters, " and ") - cmd.where -> cmd.where - end + cmd.where -> + cmd.where + end - #add the filter criteria to the update list + # add the filter criteria to the update list params = vals ++ cmd.params columns = Enum.join(cols, ", ") @@ -425,8 +460,6 @@ defmodule Moebius.Query do %{cmd | sql: sql, type: :update, params: params} end - - @doc """ Creates a DELETE command """ @@ -435,8 +468,6 @@ defmodule Moebius.Query do %{cmd | sql: sql, type: :delete} end - - @doc """ Build a table join for your query. There are a number of options to handle various joins. Joins can also be piped for multiple joins. @@ -447,36 +478,65 @@ defmodule Moebius.Query do :primary_key - specify the joining tables primary key column :using - used to specify a USING queries list of columns to join on - Example of simple join: + Example of simple join (assumes primary key is "id" and foreign key is "customer_id"): ``` - cmd = db(:customers) - |> join(:orders) - |> select + cmd = + db(:customer) + |> join(:order) + |> select + ``` + + Example specifying the primary key (customer.customer_id): + ``` + cmd = + db(:customer) + |> join(:order, primary_key: :customer_id) + |> select + ``` + + Example specifying the foreign key (order.customer_number): + ``` + cmd = + db(:customer) + |> join(:order, foreign_key: :customer_number) + |> select ``` Example of multiple table joins: ``` - cmd = db(:customers) - |> join(:orders, on: :customers) - |> join(:items, on: :orders) + cmd = + db(:customer) + |> join(:order, on: :customer) + |> join(:item, on: :order) + |> select + ``` + + Example of outer joins: + ``` + cmd = + db(:customer) + |> join(:order, join: :left) |> select ``` """ def join(%QueryCommand{} = cmd, table, opts \\ []) do - join_type = Keyword.get(opts, :join, "inner") - join_table = Keyword.get(opts, :on, cmd.table_name) - foreign_key = Keyword.get(opts, :foreign_key, "#{singularize(join_table)}_id") + join_type = Keyword.get(opts, :join, "inner") + join_table = Keyword.get(opts, :on, cmd.table_name) + foreign_key = Keyword.get(opts, :foreign_key, "#{join_table}_id") primary_key = Keyword.get(opts, :primary_key, "id") - using = Keyword.get(opts, :using, nil) + using = Keyword.get(opts, :using, nil) - join_condition = case using do - nil -> - " #{join_type} join #{table} on #{join_table}.#{primary_key} = #{table}.#{foreign_key}" - cols -> - " #{join_type} join #{table} using (#{Enum.join(cols, ", ")})" - end + condition = join_condition(table, join_type, join_table, foreign_key, primary_key, using) + + %{cmd | join: [cmd.join | condition]} + end + + defp join_condition(table, join_type, join_table, foreign_key, primary_key, nil) do + " #{join_type} join #{table} on #{join_table}.#{primary_key} = #{table}.#{foreign_key}" + end - %{cmd | join: [cmd.join|join_condition]} + defp join_condition(table, join_type, _join_table, _foreign_key, _primary_key, cols) do + " #{join_type} join #{table} using (#{Enum.join(cols, ", ")})" end @doc """ @@ -491,7 +551,6 @@ defmodule Moebius.Query do |> sql_file_command([]) end - @doc """ Executes the SQL in a given SQL file with the specified parameters. Specify the scripts directory by setting the `scripts` directive in the config. Pass the file name as an atom, @@ -511,27 +570,28 @@ defmodule Moebius.Query do """ def sql_file_command(file, params \\ []) - def sql_file_command(file, params) when not is_list(params), - do: sql_file_command(file, [params]) + def sql_file_command(file, params) when not is_list(params) do + sql_file_command(file, [params]) + end def sql_file_command(file, params) do - #find the DB dir - scripts_dir = Application.get_env(:moebius, :scripts) - file_path = Path.join(scripts_dir, "#{Atom.to_string(file)}.sql") - sql=File.read!(file_path) + sql = + Application.get_env(:moebius, :scripts) + |> Path.join("#{Atom.to_string(file)}.sql") + |> File.read!() + |> String.trim() - %Moebius.QueryCommand{sql: String.trim(sql), params: params} + %Moebius.QueryCommand{sql: sql, params: params} end @doc """ Executes a function with the given name, passed as an atom. Example: - ``` - result = db(:users) + result = + db(:users) |> function(:all_users) - ``` """ def function(function_name) do @@ -539,7 +599,6 @@ defmodule Moebius.Query do |> function_command([]) end - @doc """ Executes a function with the given name, passed as an atom. @@ -548,9 +607,9 @@ defmodule Moebius.Query do Example: ``` - result = db(:users) - |> function(:friends, ["mike","jane"]) - + result = + db(:users) + |> function(:friends, ["mike", "jane"]) ``` """ def function(function_name, params) do @@ -558,7 +617,6 @@ defmodule Moebius.Query do |> function_command(params) end - @doc """ Creates a function command """ @@ -568,14 +626,13 @@ defmodule Moebius.Query do do: function_command(function_name, [params]) def function_command(function_name, params) do - arg_list = cond do - length(params) > 0 -> Enum.map_join(1..length(params), ", ", &"$#{&1}") - true -> "" - end + arg_list = + cond do + length(params) > 0 -> Enum.map_join(1..length(params), ", ", &"$#{&1}") + true -> "" + end sql = "select * from #{function_name}(#{arg_list});" %Moebius.QueryCommand{sql: sql, params: params} end - - end diff --git a/lib/moebius/query_command.ex b/lib/moebius/query_command.ex index fdf1048..f85bff1 100644 --- a/lib/moebius/query_command.ex +++ b/lib/moebius/query_command.ex @@ -2,22 +2,20 @@ defmodule Moebius.QueryCommand do @moduledoc """ Struct for the query command which is piped through all the transforms """ - defstruct [ - pid: nil, - sql: nil, - params: [], - table_name: nil, - columns: nil, - vals: nil, - type: :select, - where: "", - order: "", - limit: "", - offset: "", - where_columns: [], - join: [""], - group_by: nil, - error: nil, - conn: nil - ] + defstruct pid: nil, + sql: nil, + params: [], + table_name: nil, + columns: nil, + vals: nil, + type: :select, + where: "", + order: "", + limit: "", + offset: "", + where_columns: [], + join: [""], + group_by: nil, + error: nil, + conn: nil end diff --git a/lib/moebius/query_filter.ex b/lib/moebius/query_filter.ex index 63637ab..0d733d5 100644 --- a/lib/moebius/query_filter.ex +++ b/lib/moebius/query_filter.ex @@ -1,5 +1,4 @@ defmodule Moebius.QueryFilter do - @moduledoc """ The QueryFilter module is used to build WHERE clauses to be used in queries. You can @@ -108,9 +107,10 @@ defmodule Moebius.QueryFilter do cols = Keyword.keys(criteria) vals = Keyword.values(criteria) - {filters, _count} = Enum.map_reduce cols, 1, fn col, acc -> - {"#{col} = $#{acc}", acc + 1} - end + {filters, _count} = + Enum.map_reduce(cols, 1, fn col, acc -> + {"#{col} = $#{acc}", acc + 1} + end) %{cmd | params: vals, where: " where #{Enum.join(filters, " and ")}", where_columns: cols} end @@ -120,13 +120,14 @@ defmodule Moebius.QueryFilter do vals = Keyword.values(criteria) param_seed = length(cmd.params) + 1 - {filters, _count} = Enum.map_reduce cols, param_seed, fn col, acc -> - {"#{col} = $#{acc}", acc + 1} - end + {filters, _count} = + Enum.map_reduce(cols, param_seed, fn col, acc -> + {"#{col} = $#{acc}", acc + 1} + end) - #we have an existing filter, which means we need to append the params and "and" the where + # we have an existing filter, which means we need to append the params and "and" the where new_params = cmd.params ++ vals - new_where = Enum.join([cmd.where,"and #{Enum.join(filters, " and ")}"]," ") + new_where = Enum.join([cmd.where, "and #{Enum.join(filters, " and ")}"], " ") new_cols = cmd.where_columns ++ cols %{cmd | params: new_params, where: new_where, where_columns: new_cols} diff --git a/lib/moebius/transformer.ex b/lib/moebius/transformer.ex index f446803..9c0986a 100644 --- a/lib/moebius/transformer.ex +++ b/lib/moebius/transformer.ex @@ -8,17 +8,21 @@ defmodule Moebius.Transformer do def to_single({:ok, %{command: :delete, num_rows: count}}), do: {:ok, %{deleted: count}} def to_single({:ok, %{num_rows: count}}) when count == 0, do: {:ok, nil} def to_single({:error, message}) when is_binary(message), do: {:error, message} - def to_single({:error, %{postgres: %{message: message}}}), do: {:error, message} + def to_single({:error, %{postgres: %{message: message}}}), do: {:error, message} + def to_single({:ok, %{rows: _rows, columns: _cols}} = res) do {:ok, result_list} = to_list(res) - result_list |> List.first |> format_ok_result + result_list |> List.first() |> format_ok_result end def to_list({:ok, %{rows: nil}}), do: [] def to_list({:error, message}) when is_binary(message), do: {:error, message} - def to_list({:error, %{postgres: %{message: message}}}), do: {:error, message} + def to_list({:error, %{postgres: %{message: message}}}), do: {:error, message} + def to_list({:ok, %{rows: rows, columns: cols}}) do - map_list = for row <- rows, cols = atomize_columns(cols), do: match_columns_to_row(row,cols) |> to_map + map_list = + for row <- rows, cols = atomize_columns(cols), do: match_columns_to_row(row, cols) |> to_map + format_ok_result(map_list) end @@ -27,17 +31,20 @@ defmodule Moebius.Transformer do end def match_columns_to_row(row, cols), do: List.zip([cols, row]) + def to_map(list) do - Enum.into(list,%{}) + Enum.into(list, %{}) end def from_json({:error, err}), do: {:error, err} + def from_json({:ok, res}) do - res = Enum.map(res.rows, &handle_row/1) - {:ok, res} + res = Enum.map(res.rows, &handle_row/1) + {:ok, res} end def from_json({:error, err}, _), do: {:error, err} + def from_json({:ok, %{rows: rows}}, :single) do res = List.first(rows) |> handle_row {:ok, res} @@ -47,10 +54,10 @@ defmodule Moebius.Transformer do defp handle_row([id, json, created_at, updated_at]) do json - |> decode_json - |> Map.put_new(:id, id) - |> Map.put_new(:created_at, created_at) - |> Map.put_new(:updated_at, updated_at) + |> decode_json + |> Map.put_new(:id, id) + |> Map.put_new(:created_at, created_at) + |> Map.put_new(:updated_at, updated_at) end defp decode_json(json), do: Jason.decode!(json, keys: :atoms) diff --git a/mix.exs b/mix.exs index c852ec2..66e687e 100644 --- a/mix.exs +++ b/mix.exs @@ -6,19 +6,24 @@ defmodule Moebius.Mixfile do def project do [ app: :moebius, - description: "A functional approach to data access with Elixir", - version: @version, - elixir: "~> 1.15", - package: package(), - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - # ExDoc - name: "Moebius", - docs: [source_ref: "v#{@version}", - main: Moebius.Query, - source_url: "https://github.com/robconery/moebius", - extras: ["README.md"]], - deps: deps()] + description: "A functional approach to data access with Elixir", + version: @version, + elixir: "~> 1.15", + package: package(), + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + # ExDoc + name: "Moebius", + docs: [ + source_ref: "v#{@version}", + main: Moebius.Query, + source_url: "https://github.com/robconery/moebius", + extras: ["README.md"] + ], + deps: deps(), + aliases: aliases(), + elixirc_paths: elixirc_paths(Mix.env()) + ] end # Configuration for the OTP application @@ -31,17 +36,40 @@ defmodule Moebius.Mixfile do end defp deps do - [{:postgrex, "~> 0.17"}, - {:inflex, "~> 2.1.0"}, - {:jason, "~> 1.4"}, - {:ex_doc, "~> 0.30.4", only: [:dev, :docs]}, - {:earmark, "~> 1.4.39", only: [:dev, :docs]}, - {:credo, "~> 1.7.0", only: [:dev, :test]}] + [ + {:postgrex, "~> 0.17.4"}, + # {:exflect, "~> 0.4.0"}, + # {:inflex, "~> 2.1.0"}, + {:jason, "~> 1.4"}, + + # Dev & Test + {:ex_doc, "~> 0.31.1", only: [:dev, :docs]}, + {:earmark, "~> 1.4.46", only: [:dev, :docs]}, + {:credo, "~> 1.7.4", only: [:dev, :test]}, + {:sobelow, "~> 0.12", only: [:dev, :test], runtime: false}, + {:rambo, "~> 0.3.4", only: [:dev, :test], runtime: false} + ] end def package do - [maintainers: ["Rob Conery", "Chase Pursley"], - licenses: ["New BSD"], - links: %{"GitHub" => "https://github.com/robconery/moebius"}] + [ + maintainers: ["Rob Conery", "Chase Pursley"], + licenses: ["New BSD"], + links: %{"GitHub" => "https://github.com/robconery/moebius"} + ] end + + defp aliases do + [ + "moebius.setup": ["moebius.create", "moebius.migrate", "moebius.seed"], + "moebius.reset": ["moebius.drop", "moebius.setup"], + quality: [ + "format --check-formatted", + "sobelow --config", + "credo --only warning" + ] + ] + end + + defp elixirc_paths(_), do: ["lib"] end diff --git a/mix.lock b/mix.lock index 3affd53..d54d0e3 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,21 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.4", "68ca5cf89071511c12fd9919eb84e388d231121988f6932756596195ccf7fd35", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9cf776d062c78bbe0f0de1ecaee183f18f2c3ec591326107989b054b7dddefc2"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, - "earmark": {:hex, :earmark, "1.4.39", "acdb2f02c536471029dbcc509fbd6b94b89f40ad7729fb3f68f4b6944843f01d", [:mix], [{:earmark_parser, "~> 1.4.33", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "156c9d8ec3cbeccdbf26216d8247bdeeacc8c76b4d9eee7554be2f1b623ea440"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, - "ex_doc": {:hex, :ex_doc, "0.30.4", "e8395c8e3c007321abb30a334f9f7c0858d80949af298302daf77553468c0c39", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9a19f0c50ffaa02435668f5242f2b2a61d46b541ebf326884505dfd3dd7af5e4"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "exflect": {:hex, :exflect, "0.4.0", "a418093986c91e2ce23c69c3f4234d8d0c6afb4632e54767af1883b6becd41cc", [:mix], [], "hexpm", "15bede95aba34fd972e00447aa74ab19f4f26f44028d7a28deb4136fd6a01360"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, - "postgrex": {:hex, :postgrex, "0.17.2", "a3ec9e3239d9b33f1e5841565c4eb200055c52cc0757a22b63ca2d529bbe764c", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "80a918a9e9531d39f7bd70621422f3ebc93c01618c645f2d91306f50041ed90c"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, + "rambo": {:hex, :rambo, "0.3.4", "8962ac3bd1a633ee9d0e8b44373c7913e3ce3d875b4151dcd060886092d2dce7", [:mix], [], "hexpm", "0cc54ed089fbbc84b65f4b8a774224ebfe60e5c80186fafc7910b3e379ad58f1"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/db/seeds.sql b/test/db/seeds.sql new file mode 100644 index 0000000..6c1fe6b --- /dev/null +++ b/test/db/seeds.sql @@ -0,0 +1,9 @@ +insert into users(email, first, last) values('rob@test.com','Rob','Blah'); +insert into users(email, first, last) values('jill@test.com','Jill','Gloop'); +insert into users(email, first, last) values('mary@test.com','Mary','Muggtler'); +insert into users(email, first, last) values('mike@test.com','Mike','Ghruoisl'); + +insert into date_night(date) values(now()); +insert into date_night(date) values(now() - '1 day' :: interval); +insert into date_night(date) values(now() + '2 days' :: interval); +insert into date_night(date) values(now() + '1 year' :: interval); \ No newline at end of file diff --git a/test/db/test_schema.sql b/test/db/tables.sql similarity index 64% rename from test/db/test_schema.sql rename to test/db/tables.sql index d7a701c..0e85ce9 100644 --- a/test/db/test_schema.sql +++ b/test/db/tables.sql @@ -1,9 +1,10 @@ -drop index if exists idx_docs; +-- drop index if exists idx_docs; drop table if exists user_docs; drop table if exists logs; drop table if exists users; drop table if exists products; drop table if exists date_night; +drop table if exists sessions; create table users( id serial primary key, @@ -11,7 +12,8 @@ create table users( first varchar(50), last varchar(50), order_count integer not null default 10, - profile jsonb + profile jsonb, + roles varchar[] ); create table products( @@ -39,19 +41,8 @@ create table user_docs( create index idx_docs on user_docs using GIN(body jsonb_path_ops); -insert into users(email, first, last) values('rob@test.com','Rob','Blah'); -insert into users(email, first, last) values('jill@test.com','Jill','Gloop'); -insert into users(email, first, last) values('mary@test.com','Mary','Muggtler'); -insert into users(email, first, last) values('mike@test.com','Mike','Ghruoisl'); - - create table date_night(id serial primary key, date timestamptz); -insert into date_night(date) values(now()); -insert into date_night(date) values(now() - '1 day' :: interval); -insert into date_night(date) values(now() + '2 days' :: interval); -insert into date_night(date) values(now() + '1 year' :: interval); -drop table if exists sessions; create table sessions( id varchar(36) primary key not null, body jsonb not null, @@ -60,5 +51,4 @@ create table sessions( updated_at timestamptz ); -create index idx_sessions_search on sessions using GIN(search); create index idx_sessions on sessions using GIN(body jsonb_path_ops); \ No newline at end of file diff --git a/test/moebius/aggregate_test.exs b/test/moebius/aggregate_test.exs index db745af..87fc4fa 100644 --- a/test/moebius/aggregate_test.exs +++ b/test/moebius/aggregate_test.exs @@ -2,52 +2,51 @@ defmodule Moebius.AggregateTest do use ExUnit.Case import Moebius.Query - setup_all do "insert into users(email, first, last) values('aggs@test.com','Rob','Blah');" - |> TestDb.run + |> TestDb.run() + {:ok, []} end test "count returns integer" do - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> count - |> TestDb.run + |> TestDb.run() - assert is_integer res.count + assert is_integer(res.count) end test "sum returns integer for user ids" do - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> map("id > 1") |> reduce(:sum, :id) - |> TestDb.first + |> TestDb.first() assert res.sum > 1 end test "sum returns integer for user ids grouped by email" do - - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> map("id > 1") |> group(:email) |> reduce(:sum, :id) - |> TestDb.run + |> TestDb.run() assert length(res) > 0 - end test "reduce allows an expression" do - - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> map("id > 1") |> group(:email) |> reduce(:sum, "id + order_count") - |> TestDb.run + |> TestDb.run() assert length(res) > 0 - end - end diff --git a/test/moebius/basic_select_test.exs b/test/moebius/basic_select_test.exs index d5aa7d8..aa9ccc2 100644 --- a/test/moebius/basic_select_test.exs +++ b/test/moebius/basic_select_test.exs @@ -1,5 +1,4 @@ defmodule Moebius.BasicSelectTest do - use ExUnit.Case import Moebius.Query import TestDb @@ -13,77 +12,81 @@ defmodule Moebius.BasicSelectTest do end test "a basic select *" do - - cmd = db(:users) - |> select + cmd = + db(:users) + |> select assert cmd.sql == "select * from users;" end test "a basic select * using binary for tablename" do - - cmd = db("users") - |> select + cmd = + db("users") + |> select assert cmd.sql == "select * from users;" end test "a basic select with columns" do - - cmd = db(:users) - |> select("first, last") + cmd = + db(:users) + |> select("first, last") assert cmd.sql == "select first, last from users;" end test "a basic select with order" do - cmd = db(:users) - |> sort(:name, :desc) - |> select + cmd = + db(:users) + |> sort(:name, :desc) + |> select assert cmd.sql == "select * from users order by name desc;" end test "a basic select with order and limit without skip" do - cmd = db(:users) - |> sort(:name, :desc) - |> limit(10) - |> select + cmd = + db(:users) + |> sort(:name, :desc) + |> limit(10) + |> select assert cmd.sql == "select * from users order by name desc limit 10;" end test "a basic select with order and limit with offset" do - - cmd = db(:users) - |> sort(:name, :desc) - |> limit(10) - |> offset(2) - |> select + cmd = + db(:users) + |> sort(:name, :desc) + |> limit(10) + |> offset(2) + |> select assert cmd.sql == "select * from users order by name desc limit 10 offset 2;" end test "first returns first" do - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> first assert res.email == "friend@test.com" end test "find returns a single record", %{res: user} do - {:ok, found} = db(:users) - |> find(user.id) + {:ok, found} = + db(:users) + |> find(user.id) assert found.id == user.id end test "filter returns a few records", %{res: user} do - {:ok, found} = db(:users) - |> filter(id: user.id) - |> run + {:ok, found} = + db(:users) + |> filter(id: user.id) + |> run assert length(found) > 0 end - end diff --git a/test/moebius/bulk_insert_test.exs b/test/moebius/bulk_insert_test.exs index b0a65ce..30f655e 100644 --- a/test/moebius/bulk_insert_test.exs +++ b/test/moebius/bulk_insert_test.exs @@ -5,7 +5,7 @@ defmodule MoebiusBulkInsertTest do import Moebius.Query setup do - "drop table if exists people" |> TestDb.run + "drop table if exists people" |> TestDb.run() "create table people ( id serial primary key, @@ -15,52 +15,66 @@ defmodule MoebiusBulkInsertTest do city text null, state text null, zip text null - );" |> TestDb.run + );" |> TestDb.run() {:ok, res: true} end test "inserts a list of records outside a transaction" do data = 5000 |> people - res = db(:people) + + res = + db(:people) |> bulk_insert(data) - |> TestDb.run_batch + |> TestDb.run_batch() + assert [{:ok, _result} | _other_results] = res end test "inserts a list of records within a transaction" do data = 5000 |> people - res = db(:people) + + res = + db(:people) |> bulk_insert(data) - |> TestDb.transact_batch + |> TestDb.transact_batch() + assert [{:ok, _result} | _other_results] = res end - test "bulk insert fails as a transaction" do data = flawed_people(4) - res = db(:people) + + res = + db(:people) |> bulk_insert(data) - |> TestDb.transact_batch - assert {:error, "null value in column \"first_name\" of relation \"people\" violates not-null constraint"} == res + |> TestDb.transact_batch() + + assert {:error, + "null value in column \"first_name\" of relation \"people\" violates not-null constraint"} == + res + # no records were written to the db either... end defp people(qty) do - Enum.map(1..qty, &( - [ + Enum.map( + 1..qty, + &[ first_name: "FirstName #{&1}", last_name: "LastName #{&1}", address: "666 SW Pine St.", city: "Portland", state: "OR", zip: "97209" - ])) + ] + ) end # tests for trans failures dur to constraint violations: defp flawed_people(qty) do p = Enum.reverse(people(qty - 1)) + flawed = [ first_name: nil, last_name: nil, @@ -69,6 +83,7 @@ defmodule MoebiusBulkInsertTest do state: "BumFuck", zip: "10011" ] + Enum.reverse([flawed | p]) end @@ -84,5 +99,4 @@ defmodule MoebiusBulkInsertTest do # ] # Enum.reverse([flawed | p]) # end - end diff --git a/test/moebius/date_test.exs b/test/moebius/date_test.exs index 1ab7679..cdbde60 100644 --- a/test/moebius/date_test.exs +++ b/test/moebius/date_test.exs @@ -2,11 +2,11 @@ defmodule Moebius.DateTests do use ExUnit.Case setup_all do - {:ok, res} = "select * from date_night" |> TestDb.run + {:ok, res} = "select * from date_night" |> TestDb.run() {:ok, data: res} end - test "Dates are returned as Elixir structs", %{data: [ %{id: _id, date: date} | _rest]} do - assert (date.month > 0 && date.month < 13) + test "Dates are returned as Elixir structs", %{data: [%{id: _id, date: date} | _rest]} do + assert date.month > 0 && date.month < 13 end end diff --git a/test/moebius/delete_test.exs b/test/moebius/delete_test.exs index a070a06..2e62788 100644 --- a/test/moebius/delete_test.exs +++ b/test/moebius/delete_test.exs @@ -4,39 +4,42 @@ defmodule Moebius.DeleteTest do import Moebius.Query test "a simple delete" do - cmd = db(:users) + cmd = + db(:users) |> filter(id: 1) |> delete - assert cmd.sql == "delete from users where id = $1;"; + assert cmd.sql == "delete from users where id = $1;" assert length(cmd.params) == 1 end test "a bulk delete with no params" do - cmd = db(:users) + cmd = + db(:users) |> filter("id > 100") |> delete - assert cmd.sql == "delete from users where id > 100;"; - assert length(cmd.params) == 0 + assert cmd.sql == "delete from users where id > 100;" + assert Enum.empty?(cmd.params) end test "a bulk delete with a single param" do - cmd = db(:users) + cmd = + db(:users) |> filter("id > $1", 1) |> delete - assert cmd.sql == "delete from users where id > $1;"; + assert cmd.sql == "delete from users where id > $1;" assert length(cmd.params) == 1 end test "it actually works" do - {:ok, res} = db(:logs) + {:ok, res} = + db(:logs) |> filter("id > $1", 1) |> delete - |> TestDb.run + |> TestDb.run() assert res.deleted end - end diff --git a/test/moebius/document_test.exs b/test/moebius/document_test.exs index 50f72f0..d0157be 100644 --- a/test/moebius/document_test.exs +++ b/test/moebius/document_test.exs @@ -1,110 +1,116 @@ defmodule Candy do - defstruct [ - id: nil, - sticky: true, - chocolate: :gooey - ] + defstruct id: nil, + sticky: true, + chocolate: :gooey end -defmodule Moebius.DocTest do +defmodule Moebius.DocTest do use ExUnit.Case import Moebius.DocumentQuery setup do - "delete from user_docs;" |> TestDb.run - "drop table if exists monkies;" |> TestDb.run + "delete from user_docs;" |> TestDb.run() + "drop table if exists monkies;" |> TestDb.run() doc = [email: "steve@test.com", first: "Steve", money_spent: 500, pets: ["poopy", "skippy"]] monkey = %{sku: "stuff", name: "Chicken Wings", description: "duck dog lamb"} db(:monkies) - |> searchable([:name, :description]) - |> TestDb.save(monkey) + |> searchable([:name, :description]) + |> TestDb.save(monkey) - {:ok, res} = db(:user_docs) + {:ok, res} = + db(:user_docs) |> TestDb.save(doc) {:ok, res: res} end test "A document table will created by calling create_document_table" do - res = TestDb.create_document_table :poop + res = TestDb.create_document_table(:poop) assert res == {:ok, "Table created"} end test "a document can be saved if one of the values has a single quote" do - "drop table if exists artists;" |> TestDb.run - thing = %{collections: ["equipment"], cost: 67743, - description: "Why walk **when you can fly**! Weak Martian gravity means you too can fly wherever you want, whenever you want with some rockets on your back. Light, portable and really loud - you'll be the talk of the Martian skies! ", + "drop table if exists artists;" |> TestDb.run() + + thing = %{ + collections: ["equipment"], + cost: 67743, + description: + "Why walk **when you can fly**! Weak Martian gravity means you too can fly wherever you want, whenever you want with some rockets on your back. Light, portable and really loud - you'll be the talk of the Martian skies! ", domain: "localhost", image: "johnny-liftoff.jpg", inventory: 43, name: "Johnny Liftoff Rocket Suit", - price: 8933300, + price: 8_933_300, published_at: "2016-02-12T01:21:29.147Z", sku: "johnny-liftoff", status: "published", summary: "Keep your feet off the ground with our space-age rocket suit", - vendor: %{name: "Martian Armaments, Ltd", slug: "martian-armaments"}} + vendor: %{name: "Martian Armaments, Ltd", slug: "martian-armaments"} + } + {:ok, res} = db(:artists) |> TestDb.save(thing) assert res.sku == "johnny-liftoff" end test "save creates table if it doesn't exist" do - "drop table if exists artists;" |> TestDb.run + "drop table if exists artists;" |> TestDb.run() {:ok, res} = db(:artists) |> TestDb.save(%{name: "Spiff"}) assert res.name == "Spiff" end test "nil is returned when id is not found in docs" do - {:ok, res} = db(:monkies) |> TestDb.find(155555) + {:ok, res} = db(:monkies) |> TestDb.find(155_555) assert res == nil end test "the document is returned with find" do - {:ok, res} = db(:monkies) |> TestDb.find(1) + {:ok, res} = db(:monkies) |> TestDb.find(1) assert res.name == "Chicken Wings" end test "the document is returned with created and updated" do - {:ok, res} = db(:monkies) |> TestDb.find(1) + {:ok, res} = db(:monkies) |> TestDb.find(1) assert res.created_at end test "updated_at is overridden" do - {:ok, res} = db(:monkies) |> TestDb.save(%{name: "bip", updated_at: "poop"}) + {:ok, res} = db(:monkies) |> TestDb.save(%{name: "bip", updated_at: "poop"}) assert res.updated_at == res.created_at end test "saving a struct" do thing = %Candy{} - {:ok, res} = db(:monkies) |> TestDb.save(thing) + {:ok, res} = db(:monkies) |> TestDb.save(thing) assert res.id end test "returns a struct if a struct was passed in" do thing = %Candy{} assert thing.__struct__ == Candy - {:ok, res} = db(:monkies) |> TestDb.save(thing) + {:ok, res} = db(:monkies) |> TestDb.save(thing) assert res.__struct__ == Candy end test "can pull out a single record by id with find" do - {:ok, res} = db(:monkies) |> TestDb.find(1) + {:ok, res} = db(:monkies) |> TestDb.find(1) assert res.id == 1 end test "first creates table if it doesn't exist" do - "drop table if exists artists;" |> TestDb.run - {:ok, res} = db(:artists) |> TestDb.first + "drop table if exists artists;" |> TestDb.run() + {:ok, res} = db(:artists) |> TestDb.first() + case res do - {:error, _err} -> flunk "Nope" + {:error, _err} -> flunk("Nope") res -> res end end test "save creates table if it doesn't exist even when an id is included" do - "drop table if exists artists;" |> TestDb.run + "drop table if exists artists;" |> TestDb.run() assert {:ok, %{name: "jeff", id: 1}} = db(:artists) |> TestDb.save(%{name: "jeff", id: 100}) end @@ -118,158 +124,160 @@ defmodule Moebius.DocTest do test "a simple insert as a map" do doc = %{email: "steve@test.com", first: "Steve"} - {:ok, res} = db(:user_docs) + + {:ok, res} = + db(:user_docs) |> TestDb.save(doc) + assert res.id > 0 end test "a simple document query with the DocumentQuery lib" do assert {:ok, [%{email: "steve@test.com", id: _id}]} = - db(:user_docs) - |> TestDb.run + db(:user_docs) + |> TestDb.run() end test "a simple single document query with the DocumentQuery lib" do assert {:ok, %{email: "steve@test.com", id: _id}} = - db(:user_docs) - |> TestDb.first + db(:user_docs) + |> TestDb.first() end test "updating a document", %{res: res} do change = %{email: "blurgh@test.com", id: res.id} + assert {:ok, %{email: "blurgh@test.com", id: _id}} = - db(:user_docs) - |> TestDb.save(change) + db(:user_docs) + |> TestDb.save(change) end test "the save shortcut inserts a document without an id" do new_doc = %{email: "new_person@test.com"} + assert {:ok, %{email: "new_person@test.com", id: _id}} = - db(:user_docs) - |> TestDb.save(new_doc) + db(:user_docs) + |> TestDb.save(new_doc) end test "the save shortcut works updating a document", %{res: _res} do change = %{email: "blurgh@test.com"} + assert {:ok, %{email: "blurgh@test.com", id: _id}} = - db(:user_docs) - |> TestDb.save(change) + db(:user_docs) + |> TestDb.save(change) end test "delete works with just an id", %{res: res} do - {:ok, res} = db(:user_docs) + {:ok, res} = + db(:user_docs) |> delete(res.id) - |> TestDb.first + |> TestDb.first() assert res.id end test "delete works with criteria", %{res: res} do - - {:ok, res} = db(:user_docs) + {:ok, res} = + db(:user_docs) |> contains(email: res.email) |> delete - |> TestDb.run + |> TestDb.run() assert length(res) > 0 end test "select works with filter", %{res: res} do - {:ok, return} = db(:user_docs) + {:ok, return} = + db(:user_docs) |> contains(email: res.email) - |> TestDb.first + |> TestDb.first() assert return.email == res.email - end test "select works with string criteria", %{res: res} do - {:ok, return} = db(:user_docs) + {:ok, return} = + db(:user_docs) |> filter("body -> 'email' = $1", res.email) - |> TestDb.first + |> TestDb.first() assert return.email == res.email - end test "select works with basic criteria", %{res: _res} do - - {:ok, return} = db(:user_docs) + {:ok, return} = + db(:user_docs) |> filter(:money_spent, ">", 100) - |> TestDb.run + |> TestDb.run() assert length(return) > 0 - end test "select works with existence operator", %{res: res} do - - {:ok, return} = db(:user_docs) + {:ok, return} = + db(:user_docs) |> exists(:pets, "poopy") - |> TestDb.first + |> TestDb.first() assert return.id == res.id - end test "setting search fields works" do new_doc = %{sku: "stuff", name: "Chicken Wings", description: "duck dog lamb"} + db(:monkies) - |> searchable([:name, :description]) - |> TestDb.save(new_doc) + |> searchable([:name, :description]) + |> TestDb.save(new_doc) end test "select works with sort limit offset" do - - {:ok, return} = db(:user_docs) + {:ok, return} = + db(:user_docs) |> exists(:pets, "poopy") |> sort(:money_spent) |> limit(1) |> offset(0) - |> TestDb.first + |> TestDb.first() assert return end test "full text search works" do - - {:ok, res} = db(:monkies) + {:ok, res} = + db(:monkies) |> search("duck") - |> TestDb.run + |> TestDb.run() assert length(res) > 0 end test "full text search on the fly works" do - - {:ok, res} = db(:monkies) + {:ok, res} = + db(:monkies) |> search(for: "duck", in: [:name, :description]) - |> TestDb.run + |> TestDb.run() assert length(res) > 0 end - test "single returns nil when no match" do - {:ok, res} = db(:monkies) + {:ok, res} = + db(:monkies) |> contains(email: "dog@dog.comdog") - |> TestDb.first + |> TestDb.first() assert res == nil end - test "finds by id", %{res: res} do - - monkey = db(:user_docs) - |> TestDb.find(res.id) + monkey = + db(:user_docs) + |> TestDb.find(res.id) case monkey do {:error, err} -> raise err {:ok, steve} -> assert steve.id == res.id end - end - - end diff --git a/test/moebius/full_text_search_test.exs b/test/moebius/full_text_search_test.exs index 7c88156..18aca0c 100644 --- a/test/moebius/full_text_search_test.exs +++ b/test/moebius/full_text_search_test.exs @@ -3,19 +3,20 @@ defmodule Moebius.FullTextSearch do import Moebius.Query setup_all do - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> insert(first: "Mike", last: "Booger", email: "boogerbob@test.com") - |> TestDb.run + |> TestDb.run() + {:ok, user: res} end test "a simple full text query", %{user: user} do - - {:ok,result} = db(:users) - |> search(for: user.first, in: [:first, :last, :email]) - |> TestDb.run + {:ok, result} = + db(:users) + |> search(for: user.first, in: [:first, :last, :email]) + |> TestDb.run() assert length(result) > 0 end - end diff --git a/test/moebius/function_test.exs b/test/moebius/function_test.exs index ef31b1a..c7a5294 100644 --- a/test/moebius/function_test.exs +++ b/test/moebius/function_test.exs @@ -1,21 +1,16 @@ defmodule Moebius.FunctionTest do - use ExUnit.Case import Moebius.Query test "a simple function call is constructed" do - cmd = function_command(:all_users) - assert cmd.sql == "select * from all_users();" end test "a simple function call is constructed with args" do - cmd = function_command(:all_users, name: "steve") - assert cmd.sql == "select * from all_users($1);" assert length(cmd.params) == 1 end diff --git a/test/moebius/insert_test.exs b/test/moebius/insert_test.exs index 51562c0..84bffd2 100644 --- a/test/moebius/insert_test.exs +++ b/test/moebius/insert_test.exs @@ -4,8 +4,10 @@ defmodule MoebiusInsertTest do import Moebius.Query setup_all do - cmd = db(:users) - |> insert(email: "test@test.com", first: "Test", last: "User") + cmd = + db(:users) + |> insert(email: "test@test.com", first: "Test", last: "User") + {:ok, cmd: cmd} end @@ -18,11 +20,9 @@ defmodule MoebiusInsertTest do end test "it actually works" do - assert {:ok,%{email: "test@test.com", first: "Test", id: _id, last: "User", profile: nil}} = - db(:users) - |> insert(email: "test@test.com", first: "Test", last: "User") - |> TestDb.run - + assert {:ok, %{email: "test@test.com", first: "Test", id: _id, last: "User", profile: nil}} = + db(:users) + |> insert(email: "test@test.com", first: "Test", last: "User") + |> TestDb.run() end - end diff --git a/test/moebius/join_test.exs b/test/moebius/join_test.exs index 1a64a20..583e3c2 100644 --- a/test/moebius/join_test.exs +++ b/test/moebius/join_test.exs @@ -1,98 +1,106 @@ defmodule Moebius.JoinTest do - use ExUnit.Case import Moebius.Query test "a basic join" do - cmd = db(:customers) - |> join(:orders) - |> select + cmd = + db(:customer) + |> join(:order) + |> select assert cmd.sql == - "select * from customers inner join orders on customers.id = orders.customer_id;" + "select * from customer inner join order on customer.id = order.customer_id;" end test "using singular table names" do - cmd = db("customer") - |> join("order") - |> select + cmd = + db("customer") + |> join("order") + |> select assert cmd.sql == - "select * from customer inner join order on customer.id = order.customer_id;" + "select * from customer inner join order on customer.id = order.customer_id;" end test "custom primary key" do - cmd = db("customer") - |> join("order", primary_key: :customer_id) - |> select + cmd = + db("customer") + |> join("order", primary_key: :customer_id) + |> select assert cmd.sql == - "select * from customer inner join order on customer.customer_id = order.customer_id;" + "select * from customer inner join order on customer.customer_id = order.customer_id;" end test "custom foreign key" do - cmd = db("customer") - |> join("order", foreign_key: :customer_number) - |> select + cmd = + db("customer") + |> join("order", foreign_key: :customer_number) + |> select assert cmd.sql == - "select * from customer inner join order on customer.id = order.customer_number;" + "select * from customer inner join order on customer.id = order.customer_number;" end test "multiple joins" do - cmd = db(:customers) - |> join(:orders, on: :customers) - |> join(:items, on: :orders) - |> select + cmd = + db(:customer) + |> join(:order, on: :customer) + |> join(:item, on: :order) + |> select assert cmd.sql == - "select * from customers" <> - " inner join orders on customers.id = orders.customer_id" <> - " inner join items on orders.id = items.order_id;" + "select * from customer" <> + " inner join order on customer.id = order.customer_id" <> + " inner join item on order.id = item.order_id;" end test "outer joins" do - cmd = db(:customers) - |> join(:orders, join: :left) - |> select + cmd = + db(:customer) + |> join(:order, join: :left) + |> select assert cmd.sql == - "select * from customers" <> - " left join orders on customers.id = orders.customer_id;" + "select * from customer" <> + " left join order on customer.id = order.customer_id;" - cmd = db(:customers) - |> join(:orders, join: :right) - |> select + cmd = + db(:customer) + |> join(:order, join: :right) + |> select assert cmd.sql == - "select * from customers" <> - " right join orders on customers.id = orders.customer_id;" + "select * from customer" <> + " right join order on customer.id = order.customer_id;" - cmd = db(:customers) - |> join(:orders, join: :full) - |> select + cmd = + db(:customer) + |> join(:order, join: :full) + |> select assert cmd.sql == - "select * from customers" <> - " full join orders on customers.id = orders.customer_id;" + "select * from customer" <> + " full join order on customer.id = order.customer_id;" - cmd = db(:customers) - |> join(:orders, join: :cross) - |> select + cmd = + db(:customer) + |> join(:order, join: :cross) + |> select assert cmd.sql == - "select * from customers" <> - " cross join orders on customers.id = orders.customer_id;" + "select * from customer" <> + " cross join order on customer.id = order.customer_id;" end test "join with USING" do - cmd = db(:t1) - |> join(:t2, using: [:num, :name]) - |> select + cmd = + db(:t1) + |> join(:t2, using: [:num, :name]) + |> select assert cmd.sql == - "select * from t1" <> - " inner join t2 using (num, name);" + "select * from t1" <> + " inner join t2 using (num, name);" end - end diff --git a/test/moebius/multi_connection_test.exs b/test/moebius/multi_connection_test.exs index 7b2d1ea..4b07dbd 100644 --- a/test/moebius/multi_connection_test.exs +++ b/test/moebius/multi_connection_test.exs @@ -1,25 +1,23 @@ # defmodule DB1, do: use Moebius.Database # defmodule DB2, do: use Moebius.Database -# -# + # defmodule Moebius.BasicSelectTest do -# + # use ExUnit.Case # import Moebius.Query -# + # setup_all do # children = [ # {DB1, [Moebius.get_connection(:test_db)]} # {DB2, [Moebius.get_connection(:chinook)]} # ] # Supervisor.start_link children, strategy: :one_for_one -# + # {:ok, [thing: true]} # end -# + # test "they connect to different dbs" do # {:ok, res} = db(:artist) |> limit(1) |> DB2.run -# IO.inspect res # end -# + # end diff --git a/test/moebius/query_filter_test.exs b/test/moebius/query_filter_test.exs index a4da370..9c13ced 100644 --- a/test/moebius/query_filter_test.exs +++ b/test/moebius/query_filter_test.exs @@ -1,14 +1,14 @@ defmodule Moebius.QueryFilterTest do use ExUnit.Case - #doctest Moebius.QueryFilter + # doctest Moebius.QueryFilter import Moebius.QueryFilter setup context do predicates = context[:where] || "" params = context[:params] || [] - cmd = %Moebius.QueryCommand{table_name: 'users', where: predicates, params: params} + cmd = %Moebius.QueryCommand{table_name: ~c"users", where: predicates, params: params} {:ok, [query: cmd]} end @@ -172,8 +172,9 @@ defmodule Moebius.QueryFilterTest do end test "basic select with a where string", %{query: query} do - cmd = filter(query, "name=$1 OR thing=$2", ["Steve", "Bill"]) - |> Moebius.Query.select + cmd = + filter(query, "name=$1 OR thing=$2", ["Steve", "Bill"]) + |> Moebius.Query.select() assert cmd.sql == "select * from users where name=$1 OR thing=$2;" end diff --git a/test/moebius/query_test.exs b/test/moebius/query_test.exs index 108b74c..c8e0a9b 100644 --- a/test/moebius/query_test.exs +++ b/test/moebius/query_test.exs @@ -21,7 +21,7 @@ defmodule Moebius.QueryTest do end test "multiple column sort" do - cmd = db(:users) |> sort([id: :desc, email: :asc]) + cmd = db(:users) |> sort(id: :desc, email: :asc) assert cmd.order == " order by id desc, email asc" end diff --git a/test/moebius/reported_issues_test.exs b/test/moebius/reported_issues_test.exs index e1fdcd2..89cf3fc 100644 --- a/test/moebius/reported_issues_test.exs +++ b/test/moebius/reported_issues_test.exs @@ -4,31 +4,32 @@ defmodule Moebius.GithubIssues do test "multiple filters #70" do db(:users) - |> insert(first: "Super", last: "Filter", email: "superfilter@test.com") - |> TestDb.run + |> insert(first: "Super", last: "Filter", email: "superfilter@test.com") + |> TestDb.run() - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> filter(first: "Super") |> filter(last: "Filter") - |> TestDb.first + |> TestDb.first() assert(res.email == "superfilter@test.com") - end test "It can update an array column #80" do db(:users) - |> insert(email: "array@test.com", first: "Test", last: "User", roles: ["admin"]) - |> TestDb.run + |> insert(email: "array@test.com", first: "Test", last: "User", roles: ["admin"]) + |> TestDb.run() {:ok, res: true} - {:ok, res} = db(:users) + {:ok, res} = + db(:users) |> filter(email: "array@test.com") |> update(roles: ["admin"]) - |> TestDb.first + |> TestDb.first() - #if we got here we're happy + # if we got here we're happy assert(res.email == "array@test.com") end @@ -41,7 +42,6 @@ defmodule Moebius.GithubIssues do # |> filter(last: nil) # |> TestDb.run # - # IO.inspect cmd # #assert(res.email == "null@test.com") # end end diff --git a/test/moebius/sql_file_test.exs b/test/moebius/sql_file_test.exs index 5834771..f143a18 100644 --- a/test/moebius/sql_file_test.exs +++ b/test/moebius/sql_file_test.exs @@ -10,7 +10,7 @@ defmodule Moebius.SQLFileTest do test "a cte can be loaded and run" do assert {:ok, %{email: "blurgg@test.com", id: _id}} = - sql_file(:cte, "blurgg@test.com") - |> TestDb.first + sql_file(:cte, "blurgg@test.com") + |> TestDb.first() end end diff --git a/test/moebius/transaction_test.exs b/test/moebius/transaction_test.exs index e6e61ed..ab6d23a 100644 --- a/test/moebius/transaction_test.exs +++ b/test/moebius/transaction_test.exs @@ -1,5 +1,4 @@ defmodule Moebius.TransactionTest do - use ExUnit.Case import Moebius.Query @@ -13,40 +12,38 @@ defmodule Moebius.TransactionTest do end test "using a callback without errors" do - - result = transaction fn(tx) -> - - {:ok, new_user} = db(:users) - |> insert(email: "frodo@test.com") - |> run(tx) - - db(:logs) + result = + transaction(fn tx -> + {:ok, new_user} = + db(:users) + |> insert(email: "frodo@test.com") + |> run(tx) + + db(:logs) |> insert(user_id: new_user.id, log: "Hi Frodo") |> run(tx) - new_user - end + new_user + end) assert result.email == "frodo@test.com" end - test "using a callback with errors" do - - assert{:error, "insert or update on table \"logs\" violates foreign key constraint \"logs_user_id_fkey\""} - = transaction fn(tx) -> - - new_user = db(:users) - |> insert(email: "bilbo@test.com") - |> run(tx) - - db(:logs) - |> insert(user_id: 22222, log: "Hi Bilbo") - |> run(tx) - - new_user - end - + assert {:error, + "insert or update on table \"logs\" violates foreign key constraint \"logs_user_id_fkey\""} = + transaction(fn tx -> + new_user = + db(:users) + |> insert(email: "bilbo@test.com") + |> run(tx) + + db(:logs) + |> insert(user_id: 22222, log: "Hi Bilbo") + |> run(tx) + + new_user + end) end # test "documents save within a transaction" do @@ -61,15 +58,16 @@ defmodule Moebius.TransactionTest do # end test "documents don't save when there's an error within a transaction" do - res = transaction fn(tx) -> - Moebius.DocumentQuery.db(:monkies) |> TestDb.save(%{name: "Mike"}, tx) - "select * from poopasdasd" |> TestDb.run(tx) - Moebius.DocumentQuery.db(:monkies) |> TestDb.save(%{name: "Larry"}, tx) - end + res = + transaction(fn tx -> + Moebius.DocumentQuery.db(:monkies) |> TestDb.save(%{name: "Mike"}, tx) + "select * from poopasdasd" |> TestDb.run(tx) + Moebius.DocumentQuery.db(:monkies) |> TestDb.save(%{name: "Larry"}, tx) + end) + case res do {:error, message} -> assert message - true -> flunk "Nope, a result came back" + true -> flunk("Nope, a result came back") end end - end diff --git a/test/moebius/update_test.exs b/test/moebius/update_test.exs index d54bdeb..42c2939 100644 --- a/test/moebius/update_test.exs +++ b/test/moebius/update_test.exs @@ -4,9 +4,10 @@ defmodule Moebius.UpdateTest do import Moebius.Query setup_all do - cmd = db(:users) - |> filter(id: 1) - |> update(email: "maggot@test.com") + cmd = + db(:users) + |> filter(id: 1) + |> update(email: "maggot@test.com") {:ok, cmd: cmd} end @@ -21,25 +22,26 @@ defmodule Moebius.UpdateTest do end test "a bulk update with a string filter" do - cmd = db(:users) - |> filter("id > 100") - |> update(email: "test@test.com") + cmd = + db(:users) + |> filter("id > 100") + |> update(email: "test@test.com") assert cmd.sql == "update users set email = $1 where id > 100 returning *;" assert length(cmd.params) == 1 end - test "a bulk update with a string filter and params" do - cmd = db(:users) - |> filter("email LIKE %$2", "test") - |> update(email: "ox@test.com") + cmd = + db(:users) + |> filter("email LIKE %$2", "test") + |> update(email: "ox@test.com") assert cmd.sql == "update users set email = $1 where email LIKE %$2 returning *;" assert length(cmd.params) == 2 end - #TODO: Move this to date tests + # TODO: Move this to date tests # test "it actually works" do # res = db(:date_night) @@ -49,6 +51,4 @@ defmodule Moebius.UpdateTest do # # assert res.date # end - - end diff --git a/test/test_helper.exs b/test/test_helper.exs index fcd7d03..57c2575 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,78 +1,7 @@ ExUnit.start() -defmodule TestDb do + +defmodule TestDb do use Moebius.Database end -children = [ - TestDb -] -Supervisor.start_link children, strategy: :one_for_one - -schema_sql = """ -drop index if exists idx_docs; -drop table if exists user_docs; -drop table if exists logs; -drop table if exists users; -drop table if exists products; -drop table if exists date_night; - -create table users( - id serial primary key, - email varchar(50) unique not null, - first varchar(50), - last varchar(50), - order_count integer not null default 10, - profile jsonb, - roles varchar[] -); - -create table products( - id serial primary key not null, - sku varchar(50) not null, - name varchar(255) not null, - price decimal(10,2) not null default 0, - description text, - search tsvector, - variants jsonb -); - -create table logs( - id serial primary key not null, - user_id integer references users(id), - log text -); - -create table user_docs( - id serial primary key not null, - body jsonb not null, - created_at timestamptz default now(), - updated_at timestamptz -); - -create index idx_docs on user_docs using GIN(body jsonb_path_ops); - -insert into users(email, first, last) values('rob@test.com','Rob','Blah'); -insert into users(email, first, last) values('jill@test.com','Jill','Gloop'); -insert into users(email, first, last) values('mary@test.com','Mary','Muggtler'); -insert into users(email, first, last) values('mike@test.com','Mike','Ghruoisl'); - - -create table date_night(id serial primary key, date timestamptz); -insert into date_night(date) values(now()); -insert into date_night(date) values(now() - '1 day' :: interval); -insert into date_night(date) values(now() + '2 days' :: interval); -insert into date_night(date) values(now() + '1 year' :: interval); - -drop table if exists sessions; -create table sessions( - id varchar(36) primary key not null, - body jsonb not null, - search tsvector, - created_at timestamptz not null default now(), - updated_at timestamptz -); - -create index idx_sessions_search on sessions using GIN(search); -create index idx_sessions on sessions using GIN(body jsonb_path_ops); -""" -Moebius.run_with_psql(schema_sql, db: "meebuss") +Supervisor.start_link([TestDb], strategy: :one_for_one)