terrazine 0.0.2 → 0.0.3

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.
@@ -0,0 +1,63 @@
1
+ module Terrazine
2
+ class Builder
3
+ private
4
+
5
+ # now it doesnt use Predicates
6
+
7
+ def build_operator(structure, prefix = nil)
8
+ operator = structure.first.to_s.sub(/^_/, '')
9
+ arguments = structure.drop(1)
10
+ # puts operator
11
+ send("operator_#{operator}", arguments, prefix)
12
+ end
13
+
14
+ def operator_missing(name, arguments, prefix)
15
+ "#{name}(#{build_columns arguments, prefix})"
16
+ end
17
+
18
+ def operator_params(arguments, _)
19
+ if arguments.count > 1
20
+ arguments.map { |i| build_param i }
21
+ else
22
+ build_param arguments.first
23
+ end
24
+ end
25
+
26
+ # without arguments smthng like this - "COUNT(#{prefix + '.'}*)"
27
+ def operator_count(arguments, prefix)
28
+ if arguments.count > 1
29
+ arguments.map { |i| "COUNT(#{build_columns(i, prefix)})" }.join ', '
30
+ else
31
+ "COUNT(#{build_columns(arguments.first, prefix)})"
32
+ end
33
+ end
34
+
35
+ def operator_nullif(arguments, prefix)
36
+ "NULLIF(#{build_columns(arguments.first, prefix)}, #{arguments[1]})"
37
+ end
38
+
39
+ def operator_array(arguments, prefix)
40
+ if [Hash, Constructor].include?(arguments.first.class)
41
+ "ARRAY(#{build_sql arguments.first})"
42
+ else # TODO? condition and error case
43
+ "ARRAY[#{build_columns arguments, prefix}]"
44
+ end
45
+ end
46
+
47
+ def operator_avg(arguments, prefix)
48
+ "AVG(#{build_columns(arguments.first, prefix)})"
49
+ end
50
+
51
+ def operator_values(arguments, _)
52
+ values = arguments.first.first.is_a?(Array) ? arguments.first : [arguments.first]
53
+ values.map! { |i| "(#{build_columns i})" }
54
+ "(VALUES#{values.join ', '}) AS #{arguments[1]} (#{build_columns arguments.last})"
55
+ end
56
+
57
+ def operator_case(arguments, _)
58
+ else_val = "ELSE #{arguments.pop} " unless arguments.last.is_a? Array
59
+ conditions = arguments.map { |i| "WHEN #{i.first} THEN #{i.last}" }.join ' '
60
+ "CASE #{conditions} #{else_val}END"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,18 @@
1
+ module Terrazine
2
+ class Builder
3
+
4
+ private
5
+
6
+ def build_param(value)
7
+ # no need for injections check - pg gem will check it
8
+ @params << value
9
+ "$#{@params.count}"
10
+ end
11
+
12
+ def wrap_result(sql)
13
+ res = @params.count.positive? ? [sql, @params] : sql
14
+ @params = []
15
+ res
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,160 @@
1
+ module Terrazine
2
+ class Builder
3
+ # TODO: :between, :!=, :>, :<, :>=, :<=
4
+ # Done: :eq, :not, :or, :and, :like, :ilike, :reg_#{type}, :in
5
+
6
+ # now it doesn't use Operators
7
+ # uses Expressions
8
+
9
+ private
10
+
11
+ # TODO? conditions like [:eq :name :Aeonax]
12
+ def build_predicates(structure)
13
+ construct_condition(structure, true)
14
+ end
15
+
16
+ # [:or, { u__name: 'Aeonax', u__role: 'liar'}, # same as [[:eq, :u__name, 'Aeonax'], ...]
17
+ # [:not, [:in, :id, [1, 2, 531]]]]
18
+ def construct_condition(structure, first_level = nil)
19
+ case structure
20
+ when Array
21
+ key = structure.first
22
+ return construct_condition(key) if structure.size < 2
23
+ if key.is_a? Symbol
24
+ parentizer send("condition_#{key}", structure.drop(1)), first_level, key
25
+ elsif key.is_a?(String) && key =~ /\?/
26
+ if [Hash, Constructor].include?(structure.second.class)
27
+ key.sub(/\?/, "(#{build_sql(structure.second)})")
28
+ else
29
+ key.sub(/\?/, build_param(structure.second))
30
+ end
31
+ else
32
+ parentizer condition_and(structure), first_level, :and
33
+ end
34
+ when Hash
35
+ res = condition_eq structure
36
+ first_level ? condition_and(res) : res
37
+ when Symbol
38
+ condition_column(structure)
39
+ when String
40
+ structure
41
+ else
42
+ raise "Unknow structure #{structure} class #{structure.class} for condition"
43
+ end
44
+ end
45
+
46
+ # common
47
+
48
+ def condition_column(structure)
49
+ structure.to_s.sub(/__/, '.')
50
+ end
51
+
52
+ def construct_condition_value(structure)
53
+ case structure
54
+ when Symbol
55
+ condition_column(structure)
56
+ when TrueClass, FalseClass
57
+ structure.to_s.upcase
58
+ when nil
59
+ 'NULL'
60
+ else
61
+ build_param structure
62
+ end
63
+ end
64
+
65
+ def parentizer(sql, first_level, key)
66
+ if first_level || ![:or, :and].include?(key)
67
+ sql
68
+ else
69
+ "(#{sql})"
70
+ end
71
+ end
72
+
73
+ #### Condition builders
74
+
75
+ # supporting eq if there is no array inside
76
+ def condition_not(structure)
77
+ value = if structure.count == 1
78
+ construct_condition structure.flatten(1)
79
+ else
80
+ condition_eq structure
81
+ end
82
+ "NOT #{value}"
83
+ end
84
+
85
+ def conditions_joiner(structure, joiner)
86
+ structure.map { |i| construct_condition i }.flatten.join(" #{joiner} ".upcase)
87
+ end
88
+
89
+ def condition_and(structure)
90
+ conditions_joiner structure, 'and'
91
+ end
92
+
93
+ def condition_or(structure)
94
+ conditions_joiner structure, 'or'
95
+ end
96
+
97
+ def condition_in(structure)
98
+ values = case structure.second
99
+ when Hash, Constructor
100
+ build_sql structure.second
101
+ else
102
+ build_param structure.second
103
+ end
104
+ "#{construct_condition_value structure.first} IN (#{values})"
105
+ end
106
+
107
+ def conditions_construct_eq(column, value)
108
+ return condition_in([column, value]) if value.is_a? Array
109
+ "#{construct_condition_value column} = #{construct_condition_value value}"
110
+ end
111
+
112
+ def condition_is(structure)
113
+ "#{construct_condition_value structure.first} IS #{construct_condition_value structure.second}"
114
+ end
115
+
116
+ def condition_eq(structure)
117
+ case structure
118
+ when Array
119
+ conditions_construct_eq structure.first, structure.second
120
+ when Hash
121
+ iterate_hash(structure, false) { |k, v| conditions_construct_eq k, v }
122
+ else
123
+ raise "Undefinded structure: #{structure} for equality condition builder"
124
+ end
125
+ end
126
+
127
+ def condition_between(structure)
128
+ "BETWEEN #{construct_condition structure}"
129
+ end
130
+
131
+ def condition_pattern(structure, pattern)
132
+ "#{construct_condition_value structure.first} #{pattern.upcase} " \
133
+ "#{construct_condition_value structure.second}"
134
+ end
135
+
136
+ def condition_like(structure)
137
+ condition_pattern structure, :like
138
+ end
139
+
140
+ def condition_ilike(structure)
141
+ condition_pattern structure, :ilike
142
+ end
143
+
144
+ def condition_reg(structure)
145
+ condition_pattern structure, '~'
146
+ end
147
+
148
+ def condition_reg_i(structure)
149
+ condition_pattern structure, '~*'
150
+ end
151
+
152
+ def condition_reg_f(structure)
153
+ condition_pattern structure, '!~'
154
+ end
155
+
156
+ def condition_reg_fi(structure)
157
+ condition_pattern structure, '!~*'
158
+ end
159
+ end
160
+ end
@@ -8,7 +8,9 @@ module Terrazine
8
8
 
9
9
  def connection(conn = nil)
10
10
  @@connection ||= conn
11
- conn || @@connection
11
+ c = conn || @@connection
12
+ # Proc because of closing PG::Connection by rails on production -_-
13
+ c.is_a?(Proc) ? c.call : c
12
14
  end
13
15
 
14
16
  def connection!(conn = nil)
@@ -3,12 +3,10 @@ module Terrazine
3
3
  attr_reader :structure, :params
4
4
  def initialize(structure = {})
5
5
  @structure = structure
6
- # @params = []
7
- @builder = Builder.new(self)
8
6
  end
9
7
 
10
8
  # TODO? join hash inside array?
11
- # TODO!! join values of existing keys
9
+ # TODO!! join values of existing keys on hashes merge
12
10
  def structure_constructor(structure, modifier)
13
11
  return modifier unless structure
14
12
 
@@ -28,51 +26,18 @@ module Terrazine
28
26
  end
29
27
  end
30
28
 
31
- # just string
32
- ### select "name, email"
33
-
34
- # array of strings or symbols
35
- ### select [*selectable_fields]
36
-
37
- # hash with column aliases
38
- ### select _field_alias: :field
39
- ### => 'SELECT field AS field_alias '
40
-
41
- # array of fields and aliases - order doesnt matter
42
- ### select [{ _user_id: :id, _user_name: :name }, :password]
43
- ### => 'SELECT id AS user_id, name AS user_name, password '
44
-
45
- # functions - array with first value - function name with underscore as symbol
46
- ### select [:_nullif, :row, :value]
47
-
48
- # table alias/name
49
- ### select t_a: [{ _user_id: :id }, :field_2, [:_nullif, :row, :value]]
50
- ### => 'SELECT t_a.id AS user_id, t_a.password, NULLIF(t_a.row, value) '
51
-
52
- # any nesting and sub queries as new SQLConstructor or hash structure
53
- ### select u: [{ _some_count: [:_count, [:_nullif, :row, :value]] },
54
- ### :name, :email],
55
- ### _u_count: (another_constructor || another_structure)
56
- ### => 'SELECT COUNT(NULLIF(u,row, value)) AS some_count, u.name, u.email, (SELECT ...) AS u_count '
57
-
58
- # construct it
59
- ### constructor = SQLConstructor.new from: [:users, :u],
60
- ### join [[:mrgl, :m], { on: 'm.user_id = u.id'}]
61
- ### constructor.select :name
62
- ### constructor.select [{u: :id, _some_count: [:_count, another_constructor]}] if smthng
63
- ### constructor.select [{r: :rgl}, :zgl] if another_smthng
64
- ### constructor.build_sql
65
- ### => 'SELECT name, u.id, COUNT(SELECT ...) AS some_count, r.rgl, zgl FROM ...'
66
29
  def select(structure)
67
30
  @structure[:select] = structure_constructor(@structure[:select], structure)
68
31
  self
69
32
  end
70
33
 
71
- # distinct_select select_structure
72
- # distinct_select select_structure, distinct_field
73
- # distinct_select select_structure, [*distinct_fields]
74
- def distinct_select(structure, fields = nil)
75
- @structure[:distinct] = fields || true
34
+ def distinct(fields = true)
35
+ @structure[:distinct] = fields
36
+ self
37
+ end
38
+
39
+ def distinct_select(structure, fields = true)
40
+ @structure[:distinct] = fields
76
41
  select structure
77
42
  self
78
43
  end
@@ -87,34 +52,11 @@ module Terrazine
87
52
  end
88
53
 
89
54
  # TODO: join constructor AND better syntax
90
- # join 'users u ON u.id = m.user_id'
91
- # join ['users u ON u.id = m.user_id',
92
- # 'skills s ON u.id = s.user_id']
93
- # join [[:user, :u], { on: 'rgl = 123' }]
94
- # join [[[:user, :u], { option: :full, on: [:or, 'mrgl = 2', 'rgl = 22'] }],
95
- # [:master, { on: ['z = 12', 'mrgl = 12'] }]]
96
55
  def join(structure)
97
56
  @structure[:join] = structure
98
- # puts @structure[:join]
99
57
  self
100
58
  end
101
59
 
102
- # conditions 'mrgl = 12'
103
- # conditions ['z = 12', 'mrgl = 12']
104
- # conditions ['NOT z = 13', [:or, 'mrgl = 2', 'rgl = 22']]
105
- # conditions [:or, ['NOT z = 13', [:or, 'mrgl = 2', 'rgl = 22']],
106
- # [:or, 'rgl = 12', 'zgl = fuck']]
107
- # conditions [['NOT z = 13',
108
- # [:or, 'mrgl = 2', 'rgl = 22']],
109
- # [:or, 'rgl = 12', 'zgl = fuck']]
110
- # => 'NOT z = 13 AND (mrgl = 2 OR rgl = 22) AND (rgl = 12 OR zgl = fuck)'
111
- # conditions ['NOT z = 13', [:or, 'mrgl = 2',
112
- # ['rgl IN ?', {select: true, from: :users}]]]
113
-
114
- # constructor.where ['u.categories_cache ~ ?',
115
- # { select: :path, from: :categories,
116
- # where: ['id = ?', s_params[:category_id]] }]
117
- # constructor.where('m.cashless IS TRUE')
118
60
  def where(structure)
119
61
  w = @structure[:where]
120
62
  if w.is_a?(Array) && w.first.is_a?(Array)
@@ -127,22 +69,34 @@ module Terrazine
127
69
  self
128
70
  end
129
71
 
130
- # TODO: with -_-
131
- # with [:alias_name, { select: true, from: :users}]
132
- # with [[:alias_name, { select: true, from: :users}],
133
- # [:alias_name_2, { select: {u: [:name, :email]},
134
- # from: :rgl}]]
72
+ # TODO: with constructor -_-
73
+ def with(structure)
74
+ @structure[:with] = structure
75
+ self
76
+ end
135
77
 
78
+ # TODO: order constructor -_-
79
+ def order(structure)
80
+ @structure[:order] = structure
81
+ self
82
+ end
83
+
84
+ # TODO: default per used here and in builder...-_-
136
85
  def limit(per)
137
86
  @structure[:limit] = (per || 8).to_i
138
87
  self
139
88
  end
140
89
 
90
+ # same as limit =(
91
+ def offset(offset)
92
+ @structure[:offset] = offset || 0
93
+ end
94
+
141
95
  # TODO: serve - return count of all rows
142
96
  # params - hash with keys :per, :page
143
97
  def paginate(params)
144
98
  limit params[:per]
145
- @structure[:offset] = ((params[:page]&.to_i || 1) - 1) * @structure[:limit]
99
+ offset((params.fetch(:page, 1).to_i - 1) * @structure[:limit])
146
100
  self
147
101
  end
148
102
 
@@ -156,8 +110,8 @@ module Terrazine
156
110
  # constructor.build_sql
157
111
  # => 'SELECT .... FROM ...'
158
112
  # => ['SELECT .... FROM .... WHERE id = $1', [22]]
159
- def build_sql
160
- @builder.get_sql @structure
113
+ def build_sql(options = {})
114
+ Builder.new.get_sql @structure, options
161
115
  end
162
116
  end
163
117
  end
@@ -1,3 +1,3 @@
1
1
  module Terrazine
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require_relative 'spec_helper'
2
2
 
3
3
  # TODO.... -_-
4
+ # May be store structures with string representation? Because tests sux right now=(
4
5
  describe Terrazine::Constructor do
5
6
  before :each do
6
7
  @constructor = Terrazine.new_constructor
@@ -13,7 +14,26 @@ describe Terrazine::Constructor do
13
14
  expect(@constructor.class).to eql Terrazine::Constructor
14
15
  end
15
16
 
16
- context '`select`' do
17
+ context '`WITH`' do
18
+ it 'build array like syntax' do
19
+ @constructor.with [:name, { select: true }]
20
+ expect(@constructor.build_sql).to eq 'WITH name AS (SELECT * ) '
21
+ end
22
+
23
+ it 'build nested array like syntax' do
24
+ @constructor.with [[:name, { select: true }],
25
+ [:another_name, { select: :mrgl }]]
26
+ expect(@constructor.build_sql).to eq 'WITH name AS (SELECT * ), another_name AS (SELECT mrgl ) '
27
+ end
28
+
29
+ it 'build hash like syntax' do
30
+ @constructor.with name: { select: true },
31
+ another_name: { select: :mrgl }
32
+ expect(@constructor.build_sql).to eq 'WITH name AS (SELECT * ), another_name AS (SELECT mrgl ) '
33
+ end
34
+ end
35
+
36
+ context '`SELECT`' do
17
37
  it 'build simple structure' do
18
38
  @constructor.select(:name)
19
39
  @constructor.select('phone')
@@ -32,22 +52,37 @@ describe Terrazine::Constructor do
32
52
  @constructor.select select: [:_count, [:_nullif, :connected, true]],
33
53
  from: [:calls, :c],
34
54
  where: 'u.id = c.user_id'
35
- expect(@constructor.build_sql).to eq 'SELECT (SELECT COUNT(NULLIF(connected, true)) FROM calls c WHERE u.id = c.user_id ) '
55
+ expect(@constructor.build_sql).to eq 'SELECT (SELECT COUNT(NULLIF(connected, true)) FROM calls c WHERE u.id = c.user_id ) '
36
56
  end
37
57
 
38
58
  it 'build big structures' do
39
59
  @permanent_c.select _calls_count: { select: [:_count, [:_nullif, :connected, true]],
40
60
  from: [:calls, :c],
41
- where: 'u.id = c.user_id' },
61
+ where: { u__id: :c__user_id } },
42
62
  u: [:name, :phone, { _master: [:_nullif, :role, "'master'"] },
43
63
  'u.abilities, u.id', 'birthdate']
44
64
  @permanent_c.select o: :client_name
45
65
  @permanent_c.select :secure_id
46
- expect(@permanent_c.build_sql).to eq "SELECT (SELECT COUNT(NULLIF(connected, true)) FROM calls c WHERE u.id = c.user_id ) AS calls_count, u.name, u.phone, NULLIF(u.role, 'master') AS master, u.abilities, u.id, u.birthdate, o.client_name, secure_id "
66
+ expect(@permanent_c.build_sql).to eq "SELECT (SELECT COUNT(NULLIF(connected, true)) FROM calls c WHERE u.id = c.user_id ) AS calls_count, u.name, u.phone, NULLIF(u.role, 'master') AS master, u.abilities, u.id, u.birthdate, o.client_name, secure_id "
67
+ end
68
+
69
+ it 'build DISTINCT' do
70
+ @constructor.distinct_select([:id, :name, :phone])
71
+ expect(@constructor.build_sql).to eq 'SELECT DISTINCT id, name, phone '
72
+ end
73
+
74
+ it 'build DISTINCT ON field' do
75
+ @constructor.distinct_select([:id, :name, :phone], :id)
76
+ expect(@constructor.build_sql).to eq 'SELECT DISTINCT ON(id) id, name, phone '
77
+ end
78
+
79
+ it 'build DISTINCT ON array of field' do
80
+ @constructor.distinct_select([:id, :name, :phone], [:id, :phone])
81
+ expect(@constructor.build_sql).to eq 'SELECT DISTINCT ON(id, phone) id, name, phone '
47
82
  end
48
83
  end
49
84
 
50
- context '`from`' do
85
+ context '`FROM`' do
51
86
  it 'build simple data structures' do
52
87
  @constructor.from :users
53
88
  expect(@constructor.build_sql).to eq 'FROM users '
@@ -55,19 +90,25 @@ describe Terrazine::Constructor do
55
90
  expect(@permanent_c.build_sql).to match 'o.client_name, secure_id FROM users u $'
56
91
  end
57
92
 
58
- it 'build values' do
59
- @constructor.from [:_values, [:_param, 'mrgl'], :r, ['type']]
93
+ it 'build VALUES' do
94
+ @constructor.from [:_values, [:_params, 'mrgl'], :r, ['type']]
60
95
  expect(@constructor.build_sql).to eq ['FROM (VALUES($1)) AS r (type) ', ['mrgl']]
61
96
  end
62
97
 
63
- it 'build values and tables' do
98
+ it 'build VALUES and tables' do
64
99
  @constructor.from [[:mrgl, :m], [:_values, [1, 2], :rgl, [:zgl, :gl]]]
65
100
  expect(@constructor.build_sql).to eq 'FROM mrgl m, (VALUES(1, 2)) AS rgl (zgl, gl) '
66
101
  end
102
+
103
+ it 'build VALUES with many rows' do
104
+ @constructor.from [:_values, [[:_params, 'mrgl'], [:_params, 'rgl']], :r, ['type']]
105
+ expect(@constructor.build_sql).to eq ['FROM (VALUES($1), ($2)) AS r (type) ',
106
+ ['mrgl', 'rgl']]
107
+ end
67
108
  end
68
109
 
69
- context '`join`' do
70
- it 'build simple join' do
110
+ context '`JOIN`' do
111
+ it 'build simple structure' do
71
112
  @constructor.join 'users u ON u.id = m.user_id'
72
113
  expect(@constructor.build_sql).to eq 'JOIN users u ON u.id = m.user_id '
73
114
  @constructor.join ['users u ON u.id = m.user_id',
@@ -84,7 +125,56 @@ describe Terrazine::Constructor do
84
125
  end
85
126
  end
86
127
 
87
- context '`conditions`' do
88
-
128
+ context '`ORDER`' do
129
+ it 'build string structre' do
130
+ @constructor.order 'name ASC'
131
+ expect(@constructor.build_sql).to eq 'ORDER BY name ASC '
132
+ end
133
+
134
+ it 'build array structure' do
135
+ @constructor.order [:name, :email]
136
+ expect(@constructor.build_sql).to eq 'ORDER BY name, email '
137
+ end
138
+
139
+ it 'build hash structure' do
140
+ @constructor.order name: :asc, phone: [:first, :desc]
141
+ expect(@constructor.build_sql).to eq 'ORDER BY name ASC, phone DESC NULLS FIRST '
142
+ end
143
+
144
+ it 'build complicated structure' do
145
+ @constructor.order [:role, { name: :asc, phone: [:last, :desc], amount: '<' }]
146
+ expect(@constructor.build_sql).to eq 'ORDER BY role, name ASC, phone DESC NULLS LAST, amount USING< '
147
+ end
148
+ end
149
+
150
+ context '`WHERE`' do
151
+ it 'build simple structure' do
152
+ @constructor.where [[:not, 'z = 13'],
153
+ [:or, 'mrgl = 2', 'rgl = 22'],
154
+ [:or, 'rgl = 12', 'zgl = lol']]
155
+ expect(@constructor.build_sql).to eq 'WHERE NOT z = 13 AND (mrgl = 2 OR rgl = 22) AND (rgl = 12 OR zgl = lol) '
156
+ end
157
+
158
+ it 'build intemidate structure' do
159
+ @constructor.where [{ role: 'manager', id: [0, 1, 153] },
160
+ [:not, [:like, :u__name, 'Aeonax']]]
161
+ expect(@constructor.build_sql).to eq ['WHERE role = $1 AND id IN ($2) AND NOT u.name LIKE $3 ', ['manager', [0, 1, 153], 'Aeonax']]
162
+ end
163
+ end
164
+
165
+ context 'Operators' do
166
+ it 'build missing operator' do
167
+ sql = Terrazine.build_sql([:_to_json,
168
+ [:_array_agg,
169
+ { select: [:id, :title,
170
+ { _copies_in_stock:
171
+ { select: [:_count, :id],
172
+ from: [:book_copies, :b_c],
173
+ where: ['b_c.shop_id = s.id',
174
+ 'b_c.book_id = b.id',
175
+ 'NOT b_c.sold = TRUE'] } }] }]],
176
+ key: 'operator')
177
+ expect(sql).to eq 'to_json(array_agg((SELECT id, title, (SELECT COUNT(id) FROM book_copies b_c WHERE b_c.shop_id = s.id AND b_c.book_id = b.id AND NOT b_c.sold = TRUE ) AS copies_in_stock )))'
178
+ end
89
179
  end
90
180
  end