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
data/bin/console
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'algebra_db'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
##
|
14
|
+
# Basic user table
|
15
|
+
class User < AlgebraDB::Table
|
16
|
+
self.table_name = :users
|
17
|
+
|
18
|
+
column :id, :Integer
|
19
|
+
column :first_name, :Text
|
20
|
+
column :last_name, :Text
|
21
|
+
|
22
|
+
relationship :users_not_me, User do |other_users|
|
23
|
+
other_users.id.neq(id)
|
24
|
+
end
|
25
|
+
|
26
|
+
relationship :audits, -> { UserAudit } do |user_audits|
|
27
|
+
user_audits.user_id.eq(id)
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Can return expressions!
|
32
|
+
def full_name
|
33
|
+
first_name.append(AlgebraDB::Build.param(' ')).append(last_name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Fake audit log for users
|
39
|
+
class UserAudit < AlgebraDB::Table
|
40
|
+
self.table_name = :user_audits
|
41
|
+
|
42
|
+
column :id, :Integer
|
43
|
+
column :user_id, :Integer
|
44
|
+
column :scopes_granted, AlgebraDB::Value::Array::Text
|
45
|
+
column :changes, AlgebraDB::Value::JSONB
|
46
|
+
|
47
|
+
relationship :user, User do |user|
|
48
|
+
user.id.eq(user_id)
|
49
|
+
end
|
50
|
+
|
51
|
+
relationship :similar_scopes, UserAudit do |other_audits|
|
52
|
+
other_audits.scopes_granted.overlaps(scopes_granted).and(
|
53
|
+
other_audits.id.neq(id)
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
COOL_QUERY = AlgebraDB::Statement::Select.run_syntax do
|
59
|
+
audits = all(UserAudit)
|
60
|
+
similar = join_relationship(audits.similar_scopes)
|
61
|
+
audit_users = join_relationship(audits.user)
|
62
|
+
similar_users = join_relationship(similar.user)
|
63
|
+
select(
|
64
|
+
parent_id: audits.id,
|
65
|
+
parent_scopes: audits.scopes_granted,
|
66
|
+
parent_user: audit_users.full_name,
|
67
|
+
child_id: similar.id,
|
68
|
+
child_scopes: similar.scopes_granted,
|
69
|
+
child_user: similar_users.full_name
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Connection wrapper that logs stuff
|
75
|
+
class LoggedConnection
|
76
|
+
def initialize(logger, connection)
|
77
|
+
@logger = logger
|
78
|
+
@connection = connection
|
79
|
+
end
|
80
|
+
|
81
|
+
attr_reader :logger, :connection
|
82
|
+
|
83
|
+
def exec_params(query, params, &block)
|
84
|
+
logger.debug(query)
|
85
|
+
connection.exec_params(query, params, &block)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
CONN = LoggedConnection.new(Logger.new($stdout), PG::Connection.new('postgres://localhost/algebra_db_test'))
|
90
|
+
|
91
|
+
Pry.start
|
data/bin/setup
ADDED
data/lib/algebra_db.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'algebra_db/version'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Root namespace for the gem.
|
6
|
+
module AlgebraDB
|
7
|
+
class Error < StandardError; end
|
8
|
+
autoload(:Build, 'algebra_db/build')
|
9
|
+
autoload(:Def, 'algebra_db/def')
|
10
|
+
autoload(:Exec, 'algebra_db/exec')
|
11
|
+
autoload(:Value, 'algebra_db/value')
|
12
|
+
autoload(:SyntaxBuilder, 'algebra_db/syntax_builder')
|
13
|
+
autoload(:Statement, 'algebra_db/statement')
|
14
|
+
autoload(:Table, 'algebra_db/table')
|
15
|
+
# Your code goes here...
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
##
|
3
|
+
# Namespace for syntax builders.
|
4
|
+
module Build
|
5
|
+
autoload(:Op, 'algebra_db/build/op')
|
6
|
+
autoload(:Between, 'algebra_db/build/between')
|
7
|
+
autoload(:Param, 'algebra_db/build/param')
|
8
|
+
autoload(:Column, 'algebra_db/build/column')
|
9
|
+
autoload(:TableFrom, 'algebra_db/build/table_from')
|
10
|
+
autoload(:SelectItem, 'algebra_db/build/select_item')
|
11
|
+
autoload(:Join, 'algebra_db/build/join')
|
12
|
+
autoload(:SelectList, 'algebra_db/build/select_list')
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns a raw parameter builder, with no value type.
|
16
|
+
def self.param(value)
|
17
|
+
Param.new(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Build
|
3
|
+
##
|
4
|
+
# A BETWEEN expression builder.
|
5
|
+
class Between < Struct.new(:between_type, :choose, :start, :finish) # rubocop:disable Style/StructInheritance
|
6
|
+
VALID_TYPES =
|
7
|
+
%i[between not_between between_symmetric not_between_symmetric].freeze
|
8
|
+
def initialize(between_type, choose, start, finish)
|
9
|
+
super(between_type, choose, start, finish)
|
10
|
+
|
11
|
+
return if VALID_TYPES.include?(between_type)
|
12
|
+
|
13
|
+
raise ArgumentError, "#{between_type} must be one of #{VALID_TYPES.inspect}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_syntax(builder)
|
17
|
+
choose.render_syntax(builder)
|
18
|
+
builder.text(between_type.to_s.gsub('_', ' ').upcase)
|
19
|
+
start.render_syntax(builder)
|
20
|
+
builder.text('AND')
|
21
|
+
finish.render_syntax(builder)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Build
|
3
|
+
##
|
4
|
+
# Syntax for a join.
|
5
|
+
class Join < Struct.new(:type, :table, :condition) # rubocop:disable Style/StructInheritance
|
6
|
+
JOIN_EXPRS = {
|
7
|
+
inner: 'INNER JOIN',
|
8
|
+
left: 'LEFT OUTER JOIN',
|
9
|
+
right: 'RIGHT OUTER JOIN',
|
10
|
+
inner_lateral: 'INNER JOIN LATERAL',
|
11
|
+
left_lateral: 'LEFT JOIN LATERAL',
|
12
|
+
right_lateral: 'RIGHT JOIN LATERAL'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
TYPES = JOIN_EXPRS.keys.freeze
|
16
|
+
|
17
|
+
def initialize(type, table, condition)
|
18
|
+
super(type, table, condition)
|
19
|
+
|
20
|
+
raise ArgumentError, "unrecognized join type #{type}" unless TYPES.include?(type)
|
21
|
+
end
|
22
|
+
|
23
|
+
def render_syntax(builder)
|
24
|
+
builder.text(join_expr)
|
25
|
+
table.render_syntax(builder)
|
26
|
+
builder.text('ON')
|
27
|
+
condition.render_syntax(builder)
|
28
|
+
end
|
29
|
+
|
30
|
+
def join_expr
|
31
|
+
JOIN_EXPRS[type]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Build
|
3
|
+
SelectItem = Struct.new(:value, :select_alias) do
|
4
|
+
def initialize(value, select_alias)
|
5
|
+
super(value, select_alias)
|
6
|
+
|
7
|
+
raise ArgumentError, "value can't be nil" if value.nil?
|
8
|
+
raise ArgumentError, "select_alias can't be nil" if select_alias.nil?
|
9
|
+
end
|
10
|
+
|
11
|
+
def render_syntax(builder)
|
12
|
+
value.render_syntax(builder)
|
13
|
+
builder.text 'AS'
|
14
|
+
builder.text(%("#{select_alias}"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def decoder
|
18
|
+
value.decoder
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Build
|
3
|
+
##
|
4
|
+
# Build up a select list.
|
5
|
+
class SelectList < Struct.new(:items) # rubocop:disable Style/StructInheritance
|
6
|
+
def initialize(*selects)
|
7
|
+
super(selects.flat_map { |i| convert_select_item(i) })
|
8
|
+
|
9
|
+
items.each do |i|
|
10
|
+
same_name = items.select { |i2| i2.select_alias == i.select_alias }
|
11
|
+
|
12
|
+
raise ArgumentError, "duplicate key #{i.select_alias}" if same_name.count > 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def render_syntax(builder)
|
17
|
+
builder.separate(items) do |i, b|
|
18
|
+
i.render_syntax(b)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Row decoder that delegates to the decoders of
|
24
|
+
# the items in the select list.
|
25
|
+
class RowDecoder < AlgebraDB::Exec::RowDecoder
|
26
|
+
def initialize(columns) # rubocop:disable Lint/MissingSuper
|
27
|
+
@columns = columns
|
28
|
+
@column_decoders = columns.map(&:decoder)
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :column_decoders
|
32
|
+
|
33
|
+
def pg_type_map
|
34
|
+
PG::TypeMapByColumn.new(column_decoders.map(&:pg_decoder))
|
35
|
+
end
|
36
|
+
|
37
|
+
def decode_row(row)
|
38
|
+
values = row.values.map.with_index do |r, i|
|
39
|
+
@column_decoders[i].decode_value(r)
|
40
|
+
end
|
41
|
+
row_struct.new(*values)
|
42
|
+
end
|
43
|
+
|
44
|
+
def row_struct
|
45
|
+
@row_struct ||= Struct.new(*@columns.map { |c| c.select_alias.to_sym })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def row_decoder
|
50
|
+
RowDecoder.new(items)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def convert_select_item(item)
|
56
|
+
if item.respond_to?(:to_select_item)
|
57
|
+
item.to_select_item
|
58
|
+
else
|
59
|
+
item.map { |k, v| SelectItem.new(v, k) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Def
|
3
|
+
##
|
4
|
+
# Defines a relationship between two tables.
|
5
|
+
class Relationship
|
6
|
+
def initialize(joined_table, relater_proc)
|
7
|
+
@joined_table = joined_table
|
8
|
+
@relater_proc = relater_proc
|
9
|
+
end
|
10
|
+
|
11
|
+
def join_clause(joined_relation)
|
12
|
+
@relater_proc.call(joined_relation)
|
13
|
+
end
|
14
|
+
|
15
|
+
def joined_table
|
16
|
+
@joined_table.is_a?(Proc) ? @joined_table.call : @joined_table
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Exec
|
3
|
+
##
|
4
|
+
# Informational class that holds a decoder.
|
5
|
+
class Decoder
|
6
|
+
##
|
7
|
+
# The decoder given to postgres for a string.
|
8
|
+
def pg_decoder
|
9
|
+
PG::TextDecoder::String.new
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Post-processing: after we use the pg decoder to load from
|
14
|
+
# DB, transform it here! By default, does nothing.
|
15
|
+
def decode_value(db_value)
|
16
|
+
db_value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AlgebraDB
|
2
|
+
module Exec
|
3
|
+
##
|
4
|
+
# Something we can hand off to a connection
|
5
|
+
# to get back ruby values to play with.
|
6
|
+
class Delivery
|
7
|
+
def initialize(query_builder, select_decoder)
|
8
|
+
@query_builder = query_builder
|
9
|
+
@select_decoder = select_decoder
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute!(connection)
|
13
|
+
return enum_for(:execute!, connection) unless block_given?
|
14
|
+
|
15
|
+
execute_raw!(connection) do |result|
|
16
|
+
result.type_map = @select_decoder.pg_type_map
|
17
|
+
result.each do |row|
|
18
|
+
yield @select_decoder.decode_row(row)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Execute a query raw, IE, don't do decoding.
|
25
|
+
def execute_raw!(connection)
|
26
|
+
sb = SyntaxBuilder.new.tap { |t| @query_builder.render_syntax(t) }
|
27
|
+
# rubocop:disable Style/ExplicitBlockArgument
|
28
|
+
connection.exec_params(sb.syntax, sb.params) do |res|
|
29
|
+
yield res
|
30
|
+
end
|
31
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|