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,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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,13 @@
1
+ module AlgebraDB
2
+ module Build
3
+ Column = Struct.new(:table, :column) do
4
+ def render_syntax(builder)
5
+ builder.text(%("#{table}"."#{column}"))
6
+ end
7
+
8
+ def default_select_item_alias
9
+ column
10
+ end
11
+ end
12
+ end
13
+ 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,13 @@
1
+ module AlgebraDB
2
+ module Build
3
+ Op = Struct.new(:operator, :lhs, :rhs) do
4
+ def render_syntax(builder)
5
+ builder.parenthesize do
6
+ lhs.render_syntax(builder)
7
+ builder.text(operator.to_s)
8
+ rhs.render_syntax(builder)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module AlgebraDB
2
+ module Build
3
+ Param = Struct.new(:value) do
4
+ def render_syntax(builder)
5
+ builder.param(value)
6
+ end
7
+ end
8
+ end
9
+ 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,10 @@
1
+ module AlgebraDB
2
+ module Build
3
+ TableFrom = Struct.new(:original_table, :table_alias) do
4
+ def render_syntax(builder)
5
+ builder.text(original_table.to_s)
6
+ builder.text(table_alias.to_s)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Contains definitional helpers.
4
+ module Def
5
+ autoload(:Relationship, 'algebra_db/def/relationship')
6
+ end
7
+ 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,9 @@
1
+ module AlgebraDB
2
+ ##
3
+ # Namespace for things that execute queries, and helpers.
4
+ module Exec
5
+ autoload(:Delivery, 'algebra_db/exec/delivery')
6
+ autoload(:Decoder, 'algebra_db/exec/decoder')
7
+ autoload(:RowDecoder, 'algebra_db/exec/row_decoder')
8
+ end
9
+ 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
@@ -0,0 +1,15 @@
1
+ module AlgebraDB
2
+ module Exec
3
+ ##
4
+ # Represents a thing that can decode a row.
5
+ class RowDecoder
6
+ def pg_type_map
7
+ PG::TypeMapAllStrings.new
8
+ end
9
+
10
+ def decode_row(row)
11
+ row
12
+ end
13
+ end
14
+ end
15
+ end