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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +64 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +63 -0
- data/LICENSE.txt +21 -0
- data/README.md +195 -0
- data/Rakefile +6 -0
- data/algebra_db.gemspec +36 -0
- data/bin/console +91 -0
- data/bin/setup +8 -0
- data/lib/algebra_db.rb +16 -0
- data/lib/algebra_db/build.rb +20 -0
- data/lib/algebra_db/build/between.rb +25 -0
- data/lib/algebra_db/build/column.rb +13 -0
- data/lib/algebra_db/build/join.rb +35 -0
- data/lib/algebra_db/build/op.rb +13 -0
- data/lib/algebra_db/build/param.rb +9 -0
- data/lib/algebra_db/build/select_item.rb +22 -0
- data/lib/algebra_db/build/select_list.rb +64 -0
- data/lib/algebra_db/build/table_from.rb +10 -0
- data/lib/algebra_db/def.rb +7 -0
- data/lib/algebra_db/def/relationship.rb +20 -0
- data/lib/algebra_db/exec.rb +9 -0
- data/lib/algebra_db/exec/decoder.rb +20 -0
- data/lib/algebra_db/exec/delivery.rb +35 -0
- data/lib/algebra_db/exec/row_decoder.rb +15 -0
- data/lib/algebra_db/statement.rb +7 -0
- data/lib/algebra_db/statement/select.rb +84 -0
- data/lib/algebra_db/syntax_builder.rb +54 -0
- data/lib/algebra_db/table.rb +98 -0
- data/lib/algebra_db/value.rb +45 -0
- data/lib/algebra_db/value/array.rb +55 -0
- data/lib/algebra_db/value/bool.rb +31 -0
- data/lib/algebra_db/value/double.rb +20 -0
- data/lib/algebra_db/value/integer.rb +20 -0
- data/lib/algebra_db/value/jsonb.rb +19 -0
- data/lib/algebra_db/value/operations.rb +10 -0
- data/lib/algebra_db/value/operations/definition.rb +23 -0
- data/lib/algebra_db/value/operations/numeric.rb +18 -0
- data/lib/algebra_db/value/text.rb +9 -0
- data/lib/algebra_db/version.rb +3 -0
- metadata +146 -0
@@ -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
|