algebra_db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +64 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +8 -0
  6. data/.travis.yml +6 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +9 -0
  9. data/Gemfile.lock +63 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +195 -0
  12. data/Rakefile +6 -0
  13. data/algebra_db.gemspec +36 -0
  14. data/bin/console +91 -0
  15. data/bin/setup +8 -0
  16. data/lib/algebra_db.rb +16 -0
  17. data/lib/algebra_db/build.rb +20 -0
  18. data/lib/algebra_db/build/between.rb +25 -0
  19. data/lib/algebra_db/build/column.rb +13 -0
  20. data/lib/algebra_db/build/join.rb +35 -0
  21. data/lib/algebra_db/build/op.rb +13 -0
  22. data/lib/algebra_db/build/param.rb +9 -0
  23. data/lib/algebra_db/build/select_item.rb +22 -0
  24. data/lib/algebra_db/build/select_list.rb +64 -0
  25. data/lib/algebra_db/build/table_from.rb +10 -0
  26. data/lib/algebra_db/def.rb +7 -0
  27. data/lib/algebra_db/def/relationship.rb +20 -0
  28. data/lib/algebra_db/exec.rb +9 -0
  29. data/lib/algebra_db/exec/decoder.rb +20 -0
  30. data/lib/algebra_db/exec/delivery.rb +35 -0
  31. data/lib/algebra_db/exec/row_decoder.rb +15 -0
  32. data/lib/algebra_db/statement.rb +7 -0
  33. data/lib/algebra_db/statement/select.rb +84 -0
  34. data/lib/algebra_db/syntax_builder.rb +54 -0
  35. data/lib/algebra_db/table.rb +98 -0
  36. data/lib/algebra_db/value.rb +45 -0
  37. data/lib/algebra_db/value/array.rb +55 -0
  38. data/lib/algebra_db/value/bool.rb +31 -0
  39. data/lib/algebra_db/value/double.rb +20 -0
  40. data/lib/algebra_db/value/integer.rb +20 -0
  41. data/lib/algebra_db/value/jsonb.rb +19 -0
  42. data/lib/algebra_db/value/operations.rb +10 -0
  43. data/lib/algebra_db/value/operations/definition.rb +23 -0
  44. data/lib/algebra_db/value/operations/numeric.rb +18 -0
  45. data/lib/algebra_db/value/text.rb +9 -0
  46. data/lib/algebra_db/version.rb +3 -0
  47. metadata +146 -0
@@ -0,0 +1,7 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Namespace for statement modules.
4
+ module Statement
5
+ autoload(:Select, 'algebra_db/statement/select')
6
+ end
7
+ end
@@ -0,0 +1,84 @@
1
+ module AlgebraDB
2
+ module Statement
3
+ ##
4
+ # A select statement executor.
5
+ class Select
6
+ def self.run_syntax(&block)
7
+ new.tap { |t| t.instance_eval(&block) }
8
+ end
9
+
10
+ def initialize
11
+ @wheres = []
12
+ @froms = []
13
+ @joins = []
14
+ @select = nil
15
+ end
16
+
17
+ ##
18
+ # Give a {AlgebgraDB::Table} or something else
19
+ # that responds to #to_relation, gives you back
20
+ # a relation that you can use further in the query.
21
+ #
22
+ # If you use `relationish` in another query (by storing it in a variable out of scope),
23
+ # it will break!
24
+ def all(relationish)
25
+ unless relationish.respond_to?(:to_relation)
26
+ raise ArgumentError, "#{relationish} does not respond to to_relation!"
27
+ end
28
+
29
+ relation = relationish.to_relation(next_table_alias)
30
+ @froms << relation.from_clause
31
+ relation
32
+ end
33
+
34
+ def where(filter)
35
+ @wheres << filter
36
+ end
37
+
38
+ def select(*selects)
39
+ @select = Build::SelectList.new(*selects)
40
+ end
41
+
42
+ def join_relationship(relationship, type: :inner)
43
+ joins(relationship.joined_table, type: type) do |rel|
44
+ relationship.join_clause(rel)
45
+ end
46
+ end
47
+
48
+ def joins(other_table, type: :inner, &block)
49
+ relation = other_table.to_relation(next_table_alias)
50
+ join_clause = block.call(relation)
51
+ @joins << Build::Join.new(type, relation.from_clause, join_clause)
52
+ relation
53
+ end
54
+
55
+ def raw_param(ruby_value)
56
+ Build.param(ruby_value)
57
+ end
58
+
59
+ def to_delivery
60
+ raise ArgumentError, 'nothing selected' unless @select
61
+
62
+ Exec::Delivery.new(self, @select.row_decoder)
63
+ end
64
+
65
+ def render_syntax(builder)
66
+ raise ArgumentError, 'no select' unless @select
67
+
68
+ builder.text('SELECT')
69
+ @select.render_syntax(builder)
70
+ builder.text('FROM')
71
+ builder.separate(@froms) { |f, b| f.render_syntax(b) }
72
+ @joins.each { |j| j.render_syntax(builder) }
73
+ return if @wheres.empty?
74
+
75
+ builder.text('WHERE')
76
+ builder.separate(@wheres, separator: ' AND') { |w, b| w.render_syntax(b) }
77
+ end
78
+
79
+ def next_table_alias
80
+ :"t_#{@froms.count + @joins.count + 1}"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Class that builds syntax.
4
+ class SyntaxBuilder
5
+ def initialize(params = [])
6
+ @params = params
7
+ @syntax = +''
8
+ end
9
+
10
+ def text(str)
11
+ @syntax << str
12
+ @syntax << ' '
13
+ end
14
+
15
+ def text_nospace(str)
16
+ @syntax << str
17
+ end
18
+
19
+ def param(param)
20
+ text "$#{@params.length + 1}"
21
+ @params << param
22
+ end
23
+
24
+ def separate(listish, separator: ',')
25
+ raise ArgumentError, 'need a block' unless block_given?
26
+
27
+ len = listish.length
28
+ listish.each.with_index do |e, i|
29
+ yield e, self
30
+ unless (i + 1) == len
31
+ @syntax.strip!
32
+ text(separator)
33
+ end
34
+ end
35
+ end
36
+
37
+ def parenthesize
38
+ raise ArgumentError, 'need a block' unless block_given?
39
+
40
+ text_nospace('(')
41
+ yield self
42
+ @syntax.strip!
43
+ text(')')
44
+ end
45
+
46
+ def syntax
47
+ @syntax.dup
48
+ end
49
+
50
+ def params
51
+ @params.map(&:dup)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,98 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Represent a table in AlgebraDB.
4
+ # You should subclass this for your own tables.
5
+ #
6
+ # You should not call #new on this directly.
7
+ # Instead, use the *class* in a syntax runner.
8
+ class Table
9
+ class << self
10
+ ##
11
+ # Hash of column_name -> column_type
12
+ #
13
+ # This returns a value modified with {#dup}, so modifying it
14
+ # will have no effect on the table defintion.
15
+ def columns
16
+ (@columns || {}).dup
17
+ end
18
+
19
+ ##
20
+ # We customize the inspect method to return a quick defintion of this table.
21
+ # This makes working with it in a REPL much, much easier.
22
+ def inspect
23
+ str = "#<#{name}:#{object_id} "
24
+ str << "columns=[#{columns.keys.map(&:inspect).join(', ')}]>"
25
+ end
26
+
27
+ def column(name, value)
28
+ value = ::AlgebraDB::Value.const_get(value) if value.is_a?(Symbol)
29
+ @columns ||= {}
30
+ @columns[name.to_sym] = value
31
+ define_method(name) do
32
+ value.new(Build::Column.new(table_alias, name))
33
+ end
34
+ end
35
+
36
+ ##
37
+ # Does this table contain this column?
38
+ def column?(name)
39
+ columns.key?(name.to_sym)
40
+ end
41
+
42
+ def to_relation(relation_alias)
43
+ new(relation_alias)
44
+ end
45
+
46
+ def relationship(name, other_table, &block)
47
+ (@relationships ||= {})[name] = other_table
48
+ define_method(name) do
49
+ relater_proc =
50
+ if block.arity == 2
51
+ proc { |other_relation| block.call(self, other_relation) }
52
+ else
53
+ proc { |other_relation| instance_exec(other_relation, &block) }
54
+ end
55
+ Def::Relationship.new(other_table, relater_proc)
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Determine the relationships defined on this table.
61
+ # This is a pretty simple hash of the name of the relationship to the related table.
62
+ # It does not include any information on *how* to obtain that relationship: you need to use
63
+ # the defined instance method on a table instance to get that.
64
+ attr_reader :relationships
65
+ ##
66
+ # The name of this table in Postgres-land.
67
+ # This is anything you want, and we don't default this.
68
+ # TODO: maybe default this to follow some kind of rails conventions?
69
+ attr_accessor :table_name
70
+ end
71
+
72
+ def initialize(table_alias)
73
+ @table_alias = table_alias
74
+ end
75
+
76
+ def from_clause
77
+ Build::TableFrom.new(self.class.table_name, @table_alias)
78
+ end
79
+
80
+ def column(name)
81
+ self.class.columns.fetch(name.to_sym).new(
82
+ Build::Column.new(table_alias, name)
83
+ )
84
+ end
85
+
86
+ def columns
87
+ self.class.columns.keys.map do |k|
88
+ column(k)
89
+ end
90
+ end
91
+
92
+ def to_select_item
93
+ columns.flat_map(&:to_select_item)
94
+ end
95
+
96
+ attr_reader :table_alias
97
+ end
98
+ end
@@ -0,0 +1,45 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Base class for value types in the DB.
4
+ class Value < Struct.new(:builder) # rubocop:disable Style/StructInheritance
5
+ autoload(:Array, 'algebra_db/value/array')
6
+ autoload(:Text, 'algebra_db/value/text')
7
+ autoload(:Integer, 'algebra_db/value/integer')
8
+ autoload(:Bool, 'algebra_db/value/bool')
9
+ autoload(:Double, 'algebra_db/value/double')
10
+ autoload(:JSONB, 'algebra_db/value/jsonb')
11
+ autoload(:Operations, 'algebra_db/value/operations')
12
+
13
+ extend AlgebraDB::Value::Operations::Definition
14
+
15
+ def render_syntax(syntax_builder)
16
+ builder.render_syntax(syntax_builder)
17
+ end
18
+
19
+ def to_select_item
20
+ unless builder.respond_to?(:default_select_item_alias)
21
+ raise ArgumentError, "#{builder.inspect} has no default alias for us as a select item"
22
+ end
23
+
24
+ Build::SelectItem.new(self, builder.default_select_item_alias)
25
+ end
26
+
27
+ def decoder
28
+ Exec::Decoder.new
29
+ end
30
+
31
+ {
32
+ eq: '=', neq: '<>',
33
+ lt: '<', lt_eq: '<=',
34
+ gt: '>', gt_eq: '>='
35
+ }.each { |k, v| binop(k, v, :Bool) }
36
+
37
+ Build::Between::VALID_TYPES.each do |t|
38
+ define_method(t) do |lhs, rhs|
39
+ Value::Bool.new(
40
+ Build::Between.new(t, self, lhs, rhs)
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,55 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Represents Postgres arrays with an inner type.
5
+ class Array < Value
6
+ def self.of(other)
7
+ Class.new(other) do
8
+ include AlgebraDB::Value::Array::ArrayOps
9
+
10
+ def decoder
11
+ AlgebraDB::Value::Array::Decoder.new(super)
12
+ end
13
+ end
14
+ end
15
+ ##
16
+ # Decodes into ruby arrays.
17
+ class Decoder < AlgebraDB::Exec::Decoder
18
+ def initialize(inner_decoder) # rubocop:disable Lint/MissingSuper
19
+ @inner_decoder = inner_decoder
20
+ end
21
+
22
+ def pg_decoder
23
+ PG::TextDecoder::Array.new.tap do |decoder|
24
+ decoder.elements_type = @inner_decoder.pg_decoder
25
+ end
26
+ end
27
+
28
+ def decode_value(db_value)
29
+ db_value.map { |v| @inner_decoder.decode_value(v) }
30
+ end
31
+ end
32
+
33
+ ##
34
+ # Array operations, mixed into generated array value classes.
35
+ module ArrayOps
36
+ extend Operations::Definition
37
+
38
+ binop(:contains, :'@>', :Bool)
39
+ binop(:is_contained_by, :'<@', :Bool)
40
+ binop(:overlaps, :'&&', :Bool)
41
+ binop(:concat, :'||', :Bool)
42
+ end
43
+
44
+ def decoder
45
+ AlgebraDB::Value::Array::Decoder.new(builder.decoder)
46
+ end
47
+
48
+ # Convenience constants
49
+ Text = of(::AlgebraDB::Value::Text)
50
+ Double = of(::AlgebraDB::Value::Double)
51
+ Integer = of(::AlgebraDB::Value::Integer)
52
+ JSONB = of(::AlgebraDB::Value::JSONB)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,31 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Represents a Postgres boolean value.
5
+ class Bool < Value
6
+ def and(other)
7
+ Value::Bool.new(
8
+ Build::Op.new('AND', self, other)
9
+ )
10
+ end
11
+
12
+ def or(other)
13
+ Value::Bool.new(
14
+ Build::Op.new('OR', self, other)
15
+ )
16
+ end
17
+
18
+ ##
19
+ # Specialization of this decoder.
20
+ class Decoder < AlgebraDB::Exec::Decoder
21
+ def decode_value(db_value)
22
+ db_value == 't'
23
+ end
24
+ end
25
+
26
+ def decoder
27
+ Decoder.new
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Represents a Postgres double value.
5
+ class Double < Value
6
+ include Operations::Numeric
7
+ ##
8
+ # Specialization of this decoder.
9
+ class Decoder < AlgebraDB::Exec::Decoder
10
+ def pg_decoder
11
+ PG::TextDecoder::Float.new
12
+ end
13
+ end
14
+
15
+ def decoder
16
+ Decoder.new
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Represents a Postgres integer value.
5
+ class Integer < Value
6
+ include Operations::Numeric
7
+ ##
8
+ # Specialization of this decoder.
9
+ class Decoder < AlgebraDB::Exec::Decoder
10
+ def pg_decoder
11
+ PG::TextDecoder::Integer.new
12
+ end
13
+ end
14
+
15
+ def decoder
16
+ Decoder.new
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Represents Postgres JSONB values.
5
+ class JSONB < Value
6
+ ##
7
+ # Decoder just decodes to a hash.
8
+ class Decoder < AlgebraDB::Exec::Decoder
9
+ def pg_decoder
10
+ PG::TextDecoder::JSON.new
11
+ end
12
+ end
13
+
14
+ def decoder
15
+ Decoder.new
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ module AlgebraDB
2
+ class Value
3
+ ##
4
+ # Easily define operations on values.
5
+ module Operations
6
+ autoload(:Definition, 'algebra_db/value/operations/definition')
7
+ autoload(:Numeric, 'algebra_db/value/operations/numeric')
8
+ end
9
+ end
10
+ end