Fact.QueryItem (Fact v0.2.0)

View Source

Provides functions for constructing query item structures and converting them into query functions.

This module defines functions for building query items and combining them into lists. In Fact, a query item is a struct which defines criteria for matching events, on event types, event tags, and event data properties. A Fact database can be read using queries, and this module provides to_function/1 for conveniently converting a single query item or list of query items into a query function.

iex> Fact.QueryItem.tags("tag1")
%Fact.QueryItem{data: [], tags: ["tag1"], types: []}

iex> Fact.QueryItem.types("EventType1")
%Fact.QueryItem{data: [], tags: [], types: ["EventType1"]}

iex> Fact.QueryItem.data(name: "Jake")
%Fact.QueryItem{data: [name: ["Jake"]]}

Query items can be combined using the pipe operator.

iex> Fact.QueryItem.tags("tag1") |> Fact.QueryItem.types("EventType1")
%Fact.QueryItem{data: [], tags: ["tag1"], types: ["EventType1"]}

This module ensures query items are normalized and prevent duplicates.

iex> import Fact.QueryItem
iex> tags(["tag2","tag1"]) |> tags(["tag1","tag3","tag2"])
%Fact.QueryItem{data: [], tags: ["tag1", "tag2", "tag3"], types: []}
iex> types("EventType1") |> types(["EventType2","EventType1"])
%Fact.QueryItem{data: [], tags: [], types: ["EventType1", "EventType2"]}
iex> data(name: "Jake", name: "Cob", name: "Jacob") |> data(name: "Jake", name: "Statefarm")
%Fact.QueryItem{data: [name: ["Cob", "Jacob", "Jake", "Statefarm"]], tags: [], types: []}

There are two special representations for query items, all/1 and none/1. These are typically used as single query items when needed, but can be combined. Mathematically speaking, all/1 acts as the identity, and none/0 acts as the zero object in terms of combining query items.

iex> import Fact.QueryItem
iex> all()
:all
iex> none()
:none

Multiple query items can also be joined together to form a list which represents a compound query. At runtime, each query item is effectively combined with an OR, which often results in more Events being returned.

iex> import Fact.QueryItem
iex> join([
...>   types(["EventType1","EventType2"]),
...>   tags(["tag1", "tag2"]),
...>   types(["EventType2","EventType3"]) |> tags(["tag1","tag3"])
...> ])

Info

The normalization process used when joining may change the order of the query items. Ordering may not be consistent across different versions of OTP.

Summary

Functions

Returns a query item that matches all events.

Returns a query item that matches event data properties.

The hash/1 function produces a sha-1 hash of a query item or list of query items.

This combines multiple query items into a list of query items to describe a compound query.

Returns a query item that matches no events.

Returns a query item that matches events with all specified event tags.

Converts a query item or list of query items into a query function.

Returns a query item that matches events with any of the specified event types

Types

t()

@type t() ::
  %Fact.QueryItem{data: keyword(), tags: [String.t()], types: [String.t()]}
  | :all
  | :none

Functions

all(query_item \\ nil)

@spec all(t()) :: t()

Returns a query item that matches all events.

When combined with another query item, it acts as the identity query item, and returns the specified query item.

iex> import Fact.QueryItem
iex> all()
:all
iex> tags("tag1") |> all()
%Fact.QueryItem{data: [], tags: ["tag1"], types: []}
iex> all() |> tags("tag1")
%Fact.QueryItem{data: [], tags: ["tag1"], types: []}

data(query_item \\ %__MODULE__{}, data)

@spec data(
  t(),
  keyword()
) :: t()

Returns a query item that matches event data properties.

When duplicate keys are specified the individual values are evaluated as an OR when the query is executed.

iex> Fact.QueryItem.data(name: "Jake", name: "Jacob")
%Fact.QueryItem{data: [name: ["Jacob", "Jake"]], tags: [], types: []}

In SQL terms, assuming event_data is a jsonb column, this would be equalivant to:

SELECT *
FROM events
WHERE event_data->>'name' IN ('Jacob', 'Jake')

Distinct keys are effectively an AND when the query is executed.

iex> Fact.QueryItem.data(name: "Jake", name: "Jacob", hobby: "Homebrewing")
%Fact.QueryItem{
  data: [hobby: ["Homebrewing"], name: ["Jacob", "Jake"]], 
  tags: [], 
  types: []
}

In SQL terms, assuming event_data is a jsonb column, this would be equalivant to:

SELECT *
FROM events
WHERE event_data->>'hobby' = 'Homebrewing' 
  AND event_data->>'name' IN ('Jacob', 'Jake')

Duplicate key value pairs are ignored.

iex> import Fact.QueryItem
iex> data(name: "Jake", name: "Jacob", name: "Jacob") |> data(name: "Jake")
%Fact.QueryItem{data: [name: ["Jacob", "Jake"]], tags: [], types: []}

Raises an ArgumentError when an invalid tag (not a string) is specified:

iex> Fact.QueryItem.data({"key", "value"})
** (ArgumentError) invalid data keyword

iex> Fact.QueryItem.data([{"key", "value"}])
** (ArgumentError) invalid data keyword

hash(query_items)

@spec hash(t() | [t()]) :: String.t()

The hash/1 function produces a sha-1 hash of a query item or list of query items.

This function is used internally for normalization and caching.

join(query_items)

@spec join([t()]) :: [t()] | t()

This combines multiple query items into a list of query items to describe a compound query.

Each query item is effectively combined with an OR.

iex> import Fact.QueryItem
iex> join([
...>   types(["EventType1","EventType2"]),
...>   tags(["tag1", "tag2"]),
...>   types(["EventType2","EventType3"]) |> tags(["tag1","tag3"])
...> ])

In SQL terms, this would be equivalent to:

SELECT e.*
FROM events e 
WHERE 
  (EXISTS (
     SELECT 1 FROM event_tags t 
     WHERE e.event_id = t.event_id AND t.tag = 'tag1')
   AND EXISTS (
     SELECT 1 FROM event_tags t 
     WHERE e.event_id = t.event_id AND t.tag = 'tag2'))
OR e.event_type IN ('EventType1', 'EventType2')
OR (EXISTS (
      SELECT 1 FROM event_tags t 
      WHERE e.event_id = t.event_id AND t.tag = 'tag1')
    AND EXISTS (
      SELECT 1 FROM event_tags t 
      WHERE e.event_id = t.event_id AND t.tag = 'tag3')
    AND e.event_type IN ('EventType2', 'EventType3'))

Duplicate query items are ignored.

iex> import Fact.QueryItem
iex> join([
...>   tags(["tag1","tag2"]),
...>   tags(["tag2","tag1"])
...> ])
%Fact.QueryItem{data: [], tags: ["tag1", "tag2"], types: []}

Joining all/1 with any other query items will produce :all. In SQL, this is equivalent to OR true.

iex> import Fact.QueryItem
iex> join([
...>   tags(["tag1","tag2"]),
...>   all()
...> ])
:all

Joining none/1 with any other query items will be ignored. In SQL, this is equivalent to OR false.

iex> import Fact.QueryItem
iex> join([
...>   tags(["tag1","tag2"]),
...>   none()
...> ])
%Fact.QueryItem{data: [], tags: ["tag1", "tag2"], types: []}

Raises an ArgumentError when an invalid query item is supplied.

iex> Fact.QueryItem.join([:invalid_query_item])
** (ArgumentError) invalid query item

none(query_item \\ %__MODULE__{})

@spec none(t()) :: t()

Returns a query item that matches no events.

When combined with another query item, it acts as the zero object, and returns :none.

iex> import Fact.QueryItem
iex> none()
:none
iex> tags("tag1") |> none()
:none
iex> none() |> tags("tag1")
:none

sources(query_items)

tags(query_item \\ %__MODULE__{}, tags)

@spec tags(t(), Fact.event_tag() | [Fact.event_tag(), ...]) :: t()

Returns a query item that matches events with all specified event tags.

Multiple tags are effectively an AND when a query is evaluated.

iex> Fact.QueryItem.tags(["tag1", "tag2"])
%Fact.QueryItem{data: [], tags: ["tag1", "tag2"], types: []}

In SQL terms, this would be equivalent to:

 SELECT e.*
 FROM events e
 WHERE EXISTS (
   SELECT 1 FROM event_tags t 
   WHERE e.event_id = t.event_id AND t.tag = 'tag1')
 AND EXISTS (
   SELECT 1 FROM event_tags t 
   WHERE e.event_id = t.event_id AND t.tag = 'tag2')

Duplicate tags are ignored.

iex> import Fact.QueryItem
iex> tags("tag1") |> tags(["tag1", "tag2", "tag2"])
%Fact.QueryItem{data: [], tags: ["tag1", "tag2"], types: []}

Raises an ArgumentError when an invalid tag (not a string) is specified:

iex> Fact.QueryItem.tags(:not_a_tag)
** (ArgumentError) invalid event tag

iex> Fact.QueryItem.tags([:not_a_tag])
** (ArgumentError) invalid event tag

to_function(query_items)

@spec to_function(t() | [t(), ...]) :: Fact.Query.t()

Converts a query item or list of query items into a query function.

iex> import Fact.QueryItem
iex> fun1 = tags("tag1") |> to_function()
iex> is_function(fun1, 1)
:true
iex> fun2 = join([
...>   types(["EventType1","EventType2"]),
...>   tags(["tag1", "tag2"]),
...>   types(["EventType2","EventType3"]) |> tags(["tag1","tag3"])
...> ]) |> to_function()
iex> is_function(fun2, 1)
:true

types(query_item \\ %__MODULE__{}, types)

@spec types(t(), Fact.event_type() | [Fact.event_type(), ...]) :: t()

Returns a query item that matches events with any of the specified event types

Multiple event types are effectively an OR when a query is evaluated.

iex> Fact.QueryItem.types(["EventType1", "EventType2"])
%Fact.QueryItem{data: [], tags: [], types: ["EventType1", "EventType2"]}

In SQL terms, this would be equivalent to:

 SELECT *
 FROM events
 WHERE event_type IN ('EventType1', 'EventType2')

Duplicate types are ignored.

iex> import Fact.QueryItem
iex> types(["EventType1", "EventType2"]) |> types(["EventType1", "EventType2"])
%Fact.QueryItem{data: [], tags: [], types: ["EventType1", "EventType2"]}

Raises an ArgumentError when an invalid type (not a string) is specified:

iex> Fact.QueryItem.types(:not_a_type)
** (ArgumentError) invalid event type

iex> Fact.QueryItem.types([:not_a_type])
** (ArgumentError) invalid event type