terrazine 0.0.2

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f1a24d3310a23b60eb51584c7fdcec61a03c2b58
4
+ data.tar.gz: 3be3456371d57ae8b67918d5d3b09a6447ff9194
5
+ SHA512:
6
+ metadata.gz: c5a940bf57c92d76934211d9f6bbcca8ec9ed0bd8e42f6f788feb817309c6dcf3edb1d3f567718d1c847c051efc992a85e89ff985b86969d2ae3a114ab80a58a
7
+ data.tar.gz: 92462094a13958703b45b35305b5cc1107db055119a0847cfd2a5a34bf59d63525be94d3309e4972985e24ad5926ab2b29addb916fd1e7732eebf2f7b457abed
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ terrazine*.gem
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
@@ -0,0 +1,10 @@
1
+ # This check makes sense only in Ruby 1.9, since in 2.0+ utf-8 is the default source file encoding.
2
+ Style/Encoding:
3
+ Enabled: false
4
+
5
+ # Commonly used screens these days easily fit more than 80 characters.
6
+ Metrics/LineLength:
7
+ Max: 100
8
+
9
+ AllCops:
10
+ TargetRubyVersion: '2.3.1'
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gem 'pg-hstore', '1.2.0'
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Aeonax
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,182 @@
1
+ # Terrazin
2
+
3
+ ## Idea
4
+ Simple and comfortable, as possible, data structures parser in to SQL.
5
+
6
+ #### Data
7
+ Describing sql with data structures like [honeysql](https://github.com/jkk/honeysql) or [ql](https://github.com/niquola/ql) in clojure.
8
+
9
+ #### Constructor
10
+ Construct data structures inside Constructor instance.
11
+
12
+ #### Result
13
+ Get result and access any returned data rails like syntax.
14
+
15
+ #### Realization
16
+ This is my first gem and first close meeting with OOP... I would appreciate any help =)
17
+ And sorry for my English =(
18
+
19
+ ## Detailed description
20
+
21
+ ### Usage
22
+ Describe whole data structure, or create `Constructor` instance and combine parts of data by it instance methods. Then send result to `Terrazine.send_request(structure||constructor, params = {})` and it will return you `Terrazine::Result` instance. (description will be soon)
23
+
24
+ ### Constructor
25
+ You can create Constructor instance by calling `Terrazine.new_constructor`. It optional accepts data structure.
26
+
27
+ ```ruby
28
+ constructor = Terrazine.new_constructor
29
+ constructor_2 = Terrazine.new_constructor from: :calls
30
+ ```
31
+ #### Instance methods
32
+ Instance methods write or combine data inside constructor instance.
33
+ Not finished methods - just rewrites structure without combination with existing data.
34
+ - [ ] with
35
+ - [x] select/distinct_select
36
+ - [ ] from
37
+ - [ ] join
38
+ - [x] where
39
+ - [x] limit
40
+ - [x] paginate
41
+ - [x] merge - just merging instance structure with argument
42
+ - [x] build_sql
43
+
44
+ ### Data Structures
45
+
46
+ #### Select
47
+ Accepts
48
+ - `String` || `Symbol`
49
+ - `Hash` represents column alias - 'AS' (if key begins from `_`) OR table alias that will join to the values table prefix OR another data structure(present keyword `:select`).
50
+ - Another `Constructor` or `Hash` representing data structure
51
+ - `Array` can contain all of the above structures OR in case of first symbol/string begins from `_` it will represent SQL function
52
+ ```ruby
53
+ constructor.select "name, email"
54
+ constructor.select :birthdate
55
+ constructor.select m: [:common_rating, :work_rating, { _master_id: :id }]
56
+ constructor.select { _missed_calls_count: { select: [:_count, [:_nullif, :connected, :true]],
57
+ from: [:calls, :c],
58
+ where: ['c.client_id = u.id',
59
+ ['direction = ?', 0]]} }
60
+ constructor.structure
61
+ # => { select: ['name, email', :birthdate,
62
+ # { m: [:common_rating, :work_rating, { _master_id: :id }] },
63
+ # { _missed_calls_count: { select: [:_count, [:_nullif, :connected, :true]],
64
+ # from: [:calls, :c],
65
+ # where: ['c.client_id = u.id',
66
+ # ['direction = ?', 0]]} }] }
67
+
68
+ constructor.build_sql
69
+ # => ['SELECT name, email, birthdate, m.common_rating, m.work_rating, m.id AS master_id,
70
+ # (SELECT COUNT(NULLIF(connected, TRUE))
71
+ # FROM calls c
72
+ # WHERE c.client_id = u.id AND direction = $1) AS missed_calls_count',
73
+ # 0]
74
+ ```
75
+
76
+ #### From
77
+ Accepts
78
+ - `String` || `Symbol`
79
+ - `Array` can contains table_name and table_alias OR `VALUES` OR both
80
+ ```ruby
81
+ from 'table_name table_alias' || :table_name
82
+ from [:table_name, :table_alias]
83
+ from [[:table_name, :table_alias], [:_values, [1, 2], :values_name, [*values_column_names]]]
84
+ from [:mrgl, [:_values, [1, 2], :rgl, [:zgl, :gl]]]
85
+ ```
86
+ I do not like the `from` syntax, but how it can be made more convenient...?
87
+
88
+ #### Join
89
+ Accpets
90
+ - `String`
91
+ - `Array`:
92
+ First element same as `from` first element - table name or `Array` of table_name and table_alias, then `Hash` with keys:
93
+ - on - conditions(description will be bellow)
94
+ - options - optional contains `Symbol` or `String` of join type... rename to type?
95
+
96
+ `Array` can be nested
97
+ ```ruby
98
+ join 'users u ON u.id = m.user_id'
99
+ join ['users u ON u.id = m.user_id',
100
+ 'skills s ON u.id = s.user_id']
101
+ join [[:user, :u], { on: 'rgl = 123' }]
102
+ join [[[:user, :u], { option: :full, on: [:or, 'mrgl = 2', 'rgl = 22'] }],
103
+ [:master, { on: ['z = 12', 'mrgl = 12'] }]]
104
+ ```
105
+
106
+ #### Conditions
107
+ Current conditions implementation is sux... -_- Soon i'll change it.
108
+ Now it accepts `String` or `Array`.
109
+ First element of array is `Symbol` representation of join condition - `:or || :and` or by default `:and`.
110
+ ```ruby
111
+ conditions 'mrgl = 12'
112
+ conditions ['z = 12', 'mrgl = 12']
113
+ conditions ['NOT z = 13', [:or, 'mrgl = 2', 'rgl = 22']]
114
+ conditions [:or, ['NOT z = 13', [:or, 'mrgl = 2', 'rgl = 22']],
115
+ [:or, 'rgl = 12', 'zgl = lol']]
116
+ conditions [['NOT z = 13',
117
+ [:or, 'mrgl = 2', 'rgl = 22']],
118
+ [:or, 'rgl = 12', 'zgl = lol']]
119
+ # => 'NOT z = 13 AND (mrgl = 2 OR rgl = 22) AND (rgl = 12 OR zgl = lol)'
120
+ ```
121
+
122
+ #### With
123
+ ```ruby
124
+ with [:alias_name, { select: true, from: :users}]
125
+ with [[:alias_name, { select: true, from: :users}],
126
+ [:alias_name_2, { select: {u: [:name, :email]},
127
+ from: :rgl}]]
128
+ ```
129
+
130
+ #### Union
131
+ ```ruby
132
+ union: [{ select: true, from: [:o_list, [:_values, [1], :al, [:master]]] },
133
+ { select: true, from: [:co_list, [:_values, [0, :FALSE, :TRUE, 0],
134
+ :al, [:rating, :rejected,
135
+ :payment, :master]]] }]
136
+ ```
137
+
138
+ ### Result representation
139
+ #### ::Row
140
+ Result row - allow accessing data by field name via method - `row.name # => "mrgl"` or get hash representation with `row.to_h`
141
+ Contains
142
+ - `values`
143
+ - `pg_result` - `::Result` instance
144
+
145
+ #### ::Result < ::Row
146
+ Data can be accessed like from row - it use first row, or you can iterate rows.
147
+ Methods `each`, `each_with_index`, `first`, `last`, `map`, `count`, `present?` delegates to `rows`. `index` delegates to `fields`.
148
+ For data representation as `Hash` or `Array` exists method `present`
149
+ After initialize `PG::Result` cleared
150
+ ##### Contains
151
+ - `rows` - Array of `::Row`
152
+ - `fields` - Array of column/alias names of returned data
153
+ - `options`
154
+ ##### Options
155
+ - `:types` - hash representing which column require additional parsing and which type
156
+ - `:presenter_options`
157
+
158
+ #### ::Presenter
159
+ Used in `result.present(options = {})` - it represents data as `Hash` or `Array`. options are merged with `result.options[:presenter_options]`
160
+ Data will be presented as `Array` if `rows > 1` or `options[:array]` present.
161
+ ##### Available options
162
+ - `array` - if querry returns only one row, but on client you await for array of data.
163
+ - `structure` - `Hash` with field as key and value as modifier. Modifier will rewrite field value in result. Modifier acts:
164
+ - `Proc` - it will call proc with row as argument, and! then pass it to modifier_presentation again
165
+ - `::Result` - it will call `modifier.present`
166
+ - any else will be returned without changes
167
+
168
+ ## TODO:
169
+ - [ ] Parse data like arrays, booleans, nil to SQL
170
+ - [ ] Relocate functions builder in to class, finally I found how it can be done nice=))
171
+ - [ ] meditate about structure supporting another databases(now supports only postgress)
172
+ - [ ] should I bother with extra spaces?
173
+
174
+ ### Tests
175
+ - [ ] Constructor + Builder
176
+ - [ ] Result
177
+ - [ ] Request
178
+
179
+ ### Think of a better data structure for
180
+ - [ ] from
181
+ - [ ] join !!!
182
+ - [ ] where !!!!!! Support for rails like syntax with hash?
@@ -0,0 +1,6 @@
1
+ # require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def second
3
+ self[1]
4
+ end
5
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'helper.rb'
2
+ require_relative 'terrazine/config'
3
+ require_relative 'terrazine/builder'
4
+ require_relative 'terrazine/constructor'
5
+ require_relative 'terrazine/type_map'
6
+ require_relative 'terrazine/presenter'
7
+ require_relative 'terrazine/result'
8
+
9
+ module Terrazine
10
+
11
+ def self.connection
12
+ Config.connection
13
+ end
14
+
15
+ def self.config(params)
16
+ Config.set params
17
+ end
18
+
19
+ def self.send_request(structure, params = {})
20
+ sql = build_sql structure
21
+ connection = Config.connection!(params[:connection])
22
+
23
+ res = time_output(sql) { execute_request connection, sql }
24
+ Result.new res, params
25
+ end
26
+
27
+ def self.new_constructor(structure = {})
28
+ Constructor.new structure
29
+ end
30
+
31
+ def self.build_sql(structure)
32
+ case structure
33
+ when Hash
34
+ new_constructor(structure).build_sql
35
+ when Constructor
36
+ structure.build_sql
37
+ when String
38
+ structure
39
+ else
40
+ raise # TODO: Errors
41
+ end
42
+ end
43
+
44
+ def self.time_output(str = '')
45
+ time = Time.now
46
+ res = yield
47
+ puts "(\033[32m#{(Time.now - time) * 1000})ms \033[34m#{str}\033[0m"
48
+ res
49
+ end
50
+
51
+ # TODO: relocate
52
+ def self.execute_request(connection, sql)
53
+ if sql.is_a?(Array)
54
+ connection.exec_params(sql.first, sql.second)
55
+ else
56
+ connection.exec(sql)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,251 @@
1
+ module Terrazine
2
+ # build structures in to sql string
3
+ class Builder
4
+ attr_accessor :sql, :constructor
5
+
6
+ def initialize(constructor)
7
+ @constructor = constructor
8
+ @params = []
9
+ end
10
+
11
+ # TODO: update, delete, insert.....
12
+ def build_sql(structure)
13
+ structure = structure.is_a?(Constructor) ? structure.structure : structure
14
+ sql = ''
15
+ sql += "WITH #{build_with(structure[:with])} " if structure[:with]
16
+ # puts "build_sql, structure: #{structure}"
17
+ [:union, :select, :insert, :update, :delete, :set, :from, :join, :where,
18
+ :group, :order, :limit, :offset].each do |i|
19
+ next unless structure[i]
20
+ sql += send("build_#{i}".to_sym, structure[i])
21
+ end
22
+ sql
23
+ end
24
+
25
+ # get complete sql structure for constructor.
26
+ def get_sql(structure)
27
+ sql = build_sql structure
28
+ res = @params.count.positive? ? [sql, @params] : sql
29
+ @params = []
30
+ res
31
+ end
32
+
33
+ def build_with(structure)
34
+ if structure.second.is_a? Hash
35
+ "#{structure.first} AS (#{build_sql(structure.last)})"
36
+ else
37
+ structure.map { |v| build_with(v) }.join ', '
38
+ end
39
+ end
40
+
41
+ # def build_select_query(structure)
42
+ # puts "build_select_query, structure: #{structure}"
43
+ # sql += build_select(structure[:select], structure[:distinct]) if structure[:select]
44
+ # [:from, :join, :where, :order, :limit, :offset].each do |i|
45
+ # sql += send("build_#{i}", structure[i]) if structure[i]
46
+ # end
47
+ # end
48
+
49
+ def build_union(structure)
50
+ structure.map { |i| build_sql(i) }.join ' UNION '
51
+ end
52
+
53
+ def build_distinct_select(distinct)
54
+ case distinct
55
+ when Array
56
+ "DISTINCT ON(#{build_columns fields}) "
57
+ when true
58
+ 'DISTINCT '
59
+ end
60
+ end
61
+
62
+ def build_select(structure, distinct = nil)
63
+ # puts "build_select, structure #{structure}"
64
+ "SELECT #{build_distinct_select distinct}#{build_columns structure} "
65
+ end
66
+
67
+ def build_tables(structure)
68
+ case structure
69
+ when Array
70
+ if check_alias(structure.first) # VALUES function or ...?
71
+ build_function(structure)
72
+ # if it's a array with strings/values
73
+ elsif structure.select { |i| i.is_a? Array }.empty? # array of table_name and alias
74
+ structure.join ' '
75
+ else # array of tables/values
76
+ structure.map { |i| i.is_a?(Array) ? build_tables(i) : i }.join(', ')
77
+ end
78
+ when String, Symbol
79
+ structure
80
+ else
81
+ raise "Undefined structure for FROM - #{structure}"
82
+ end
83
+ end
84
+
85
+ def build_from(structure)
86
+ "FROM #{build_tables(structure)} "
87
+ end
88
+
89
+ def conditions_constructor(structure, joiner = :and, level = nil)
90
+ case structure
91
+ when Array
92
+ key = structure.first
93
+ # AND, OR support
94
+ if key.is_a? Symbol
95
+ res = structure.drop(1).map { |i| conditions_constructor(i) }.join " #{key} ".upcase
96
+ level ? res : "(#{res})"
97
+ # Sub Queries support - ['rgl IN ?', {...}]
98
+ elsif key =~ /\?/
99
+ if [Hash, Constructor].include?(structure.second.class)
100
+ key.sub(/\?/, "(#{build_sql(structure.second)})")
101
+ else
102
+ key.sub(/\?/, build_param(structure.second))
103
+ end
104
+ else
105
+ res = structure.map { |i| conditions_constructor(i) }.join " #{joiner} ".upcase
106
+ level ? res : "(#{res})"
107
+ end
108
+ when String
109
+ structure
110
+ end
111
+ end
112
+
113
+ # TODO? conditions like [:eq :name :Aeonax]
114
+ def build_conditions(structure)
115
+ conditions_constructor(structure, :and, true) + ' '
116
+ end
117
+
118
+ # TODO: -_-
119
+ def build_join(structure)
120
+ if structure.is_a? Array
121
+ # TODO: hash is sux here -_- !!!!!!
122
+ if structure.second.is_a? Hash
123
+ name = build_tables structure.first # (name.is_a?(Array) ? name.join(' ') : name)
124
+ v = structure.second
125
+ "#{v[:option].to_s.upcase + ' ' if v[:option]}JOIN #{name} ON #{build_conditions v[:on]}"
126
+ else
127
+ structure.map { |i| build_join(i) }.join
128
+ end
129
+ else
130
+ structure =~ /join/i ? structure : "JOIN #{structure} "
131
+ end
132
+ end
133
+
134
+ def build_where(structure)
135
+ "WHERE #{build_conditions(structure)} "
136
+ end
137
+
138
+ # TODO!
139
+ def build_order(structure)
140
+ "ORDER BY #{structure} "
141
+ end
142
+
143
+ def build_limit(limit)
144
+ "LIMIT #{limit || 8} "
145
+ end
146
+
147
+ def build_offset(offset)
148
+ "OFFSET #{offset || 0} "
149
+ end
150
+
151
+ private
152
+
153
+ def build_param(value)
154
+ # no need for injections check - pg gem will check it
155
+ @params << value
156
+ "$#{@params.count}"
157
+ end
158
+
159
+ # all functions and column aliases begins from _
160
+ def check_alias(val)
161
+ val.to_s =~ /^_/
162
+ end
163
+
164
+ def iterate_hash(data)
165
+ iterations = []
166
+ data.each { |k, v| iterations << yield(k, v) }
167
+ iterations.join ', '
168
+ end
169
+
170
+ def build_as(field, name)
171
+ "#{field} AS #{name.to_s.sub(/^_/, '')}" # update ruby for delete_prefix? =)
172
+ end
173
+
174
+ def build_columns(structure, prefix = nil)
175
+ case structure
176
+ when Array
177
+ # SQL function - in format: "_#{fn}"
178
+ if check_alias(structure.first)
179
+ build_function structure, prefix
180
+ else
181
+ structure.map { |i| build_columns i, prefix }.join ', '
182
+ end
183
+ when Hash
184
+ # sub_query
185
+ if structure[:select]
186
+ "(#{build_sql(structure)})"
187
+ # colum OR table alias
188
+ else
189
+ iterate_hash(structure) do |k, v|
190
+ if check_alias(k)
191
+ build_as(build_columns(v, prefix), k)
192
+ else
193
+ build_columns(v, k.to_s)
194
+ end
195
+ end
196
+ end
197
+ when Symbol, String
198
+ structure = structure.to_s
199
+ if prefix && structure !~ /, |\./
200
+ "#{prefix}.#{structure}"
201
+ else
202
+ structure
203
+ end
204
+ when Constructor
205
+ "(#{build_sql structure.structure})"
206
+ when true # choose everything -_-
207
+ build_columns('*', prefix)
208
+ else # TODO: values from value passing here... -_-
209
+ structure
210
+ # raise "Undefined class: #{structure.class} of #{structure}" # TODO: ERRORS class
211
+ end
212
+ end
213
+
214
+ # TODO!!!!!!! Relocate in class FunctionsBuilder? and send function name in it.
215
+ def build_function(structure, prefix = nil)
216
+ function = structure.first.to_s.sub(/^_/, '')
217
+ arguments = structure.drop(1)
218
+ case function.to_sym
219
+ when :param
220
+ build_param arguments.first
221
+ when :count # TODO? alias support on this lvl
222
+ if arguments.count > 1
223
+ arguments.map { |i| "COUNT(#{build_columns(i, prefix)})" }.join ','
224
+ else
225
+ "COUNT(#{build_columns(arguments.first, prefix)})"
226
+ end
227
+ when :nullif
228
+ # TODO? querry for value
229
+ "NULLIF(#{build_columns(arguments.first, prefix)}, #{arguments[1]})"
230
+ when :array # TODO? build_columns support
231
+ if [Hash, Constructor].include?(arguments.first.class)
232
+ "ARRAY(#{build_sql arguments.first})"
233
+ else # TODO? condition and error case
234
+ "ARRAY[#{arguments.join ', '}]"
235
+ end
236
+ when :avg
237
+ "AVG(#{build_columns(arguments.first, prefix)})"
238
+ when :values
239
+ "(VALUES(#{build_columns arguments.first, prefix})) AS #{structure[2]} (#{build_columns arguments.last})"
240
+ when :case
241
+ else_val = "ELSE #{arguments.pop} " unless arguments.last.is_a? Array
242
+ conditions = arguments.map { |i| "WHEN #{i.first} THEN #{i.last}" }.join ' '
243
+ "CASE #{conditions} #{else_val}END"
244
+ when :coalesce
245
+ "COALESCE(#{build_columns(arguments, prefix)})"
246
+ else
247
+ raise "Unknown function #{function}" # TODO: errors-_-
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,19 @@
1
+ module Terrazine
2
+ class Config
3
+ class << self
4
+ def set(params)
5
+ # another way?
6
+ @@connection = params[:connection] if params[:connection]
7
+ end
8
+
9
+ def connection(conn = nil)
10
+ @@connection ||= conn
11
+ conn || @@connection
12
+ end
13
+
14
+ def connection!(conn = nil)
15
+ connection(conn) || raise # TODO: error
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,163 @@
1
+ module Terrazine
2
+ class Constructor
3
+ attr_reader :structure, :params
4
+ def initialize(structure = {})
5
+ @structure = structure
6
+ # @params = []
7
+ @builder = Builder.new(self)
8
+ end
9
+
10
+ # TODO? join hash inside array?
11
+ # TODO!! join values of existing keys
12
+ def structure_constructor(structure, modifier)
13
+ return modifier unless structure
14
+
15
+ if structure.is_a?(Hash) && modifier.is_a?(Hash)
16
+ modifier.each do |k, v|
17
+ structure[k] = structure_constructor(structure[k], v)
18
+ end
19
+ structure
20
+ else
21
+ structure = structure.is_a?(Array) ? structure : [structure]
22
+ if modifier.is_a?(Array)
23
+ modifier.each { |i| structure_constructor structure, i }
24
+ else
25
+ structure << modifier
26
+ end
27
+ structure.uniq
28
+ end
29
+ end
30
+
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
+ def select(structure)
67
+ @structure[:select] = structure_constructor(@structure[:select], structure)
68
+ self
69
+ end
70
+
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
76
+ select structure
77
+ self
78
+ end
79
+
80
+ # TODO: from construction
81
+ # from [:mrgl, :m]
82
+ # from [:_values, [1, 2], :rgl, [:zgl, :gl]]
83
+ # => [[:mrgl, :m], [:_values, [1, 2], :rgl, [:zgl, :gl]]]
84
+ def from(structure)
85
+ @structure[:from] = structure
86
+ self
87
+ end
88
+
89
+ # 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
+ def join(structure)
97
+ @structure[:join] = structure
98
+ # puts @structure[:join]
99
+ self
100
+ end
101
+
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
+ def where(structure)
119
+ w = @structure[:where]
120
+ if w.is_a?(Array) && w.first.is_a?(Array)
121
+ @structure[:where].push structure
122
+ elsif w
123
+ @structure[:where] = [w, structure]
124
+ else
125
+ @structure[:where] = structure
126
+ end
127
+ self
128
+ end
129
+
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}]]
135
+
136
+ def limit(per)
137
+ @structure[:limit] = (per || 8).to_i
138
+ self
139
+ end
140
+
141
+ # TODO: serve - return count of all rows
142
+ # params - hash with keys :per, :page
143
+ def paginate(params)
144
+ limit params[:per]
145
+ @structure[:offset] = ((params[:page]&.to_i || 1) - 1) * @structure[:limit]
146
+ self
147
+ end
148
+
149
+ # just rewrite data. TODO: merge with merge without loss of data?
150
+ # constructor.merge(select: :content, order_by: 'f.id DESC', limit: 1)
151
+ def merge(params)
152
+ @structure.merge! params
153
+ self
154
+ end
155
+
156
+ # constructor.build_sql
157
+ # => 'SELECT .... FROM ...'
158
+ # => ['SELECT .... FROM .... WHERE id = $1', [22]]
159
+ def build_sql
160
+ @builder.get_sql @structure
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,39 @@
1
+ module Terrazine
2
+ # convinient for API presenter
3
+ class Presenter
4
+ class << self
5
+ # TODO: delete fields
6
+ def present(result, options)
7
+ if options[:array] || result.count > 1
8
+ return [] if result.count.zero?
9
+ result.map { |i| present_row i, options[:structure] }
10
+ else
11
+ return nil if result.count.zero?
12
+ present_row result, options[:structure]
13
+ end
14
+ end
15
+
16
+ def present_row(row, structure)
17
+ hash = row.to_h
18
+ if structure.present?
19
+ structure.each do |k, v|
20
+ hash[k] = present_value(row, v)
21
+ end
22
+ end
23
+ hash.compact
24
+ end
25
+
26
+ # TODO!!!
27
+ def present_value(row, modifier)
28
+ case modifier
29
+ when Result
30
+ modifier.present
31
+ when Proc
32
+ present_value row, modifier.call(row)
33
+ else
34
+ modifier
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,69 @@
1
+ require 'forwardable'
2
+
3
+ module Terrazine
4
+ # respresent result row
5
+ class Row
6
+ extend Forwardable
7
+ # attr_reader :pg_result, :values
8
+ def initialize(pg_result, values)
9
+ # @pg_result = pg_result
10
+ # @values = values
11
+ # Hiding from console a lot of data lines-_- ... another method?
12
+ define_singleton_method(:pg_result) { pg_result }
13
+ define_singleton_method(:values) { values }
14
+ end
15
+
16
+ def respond_to_missing?(method_name, include_all = true)
17
+ index(method_name.to_s) || super
18
+ end
19
+
20
+ def method_missing(method_name, *_)
21
+ indx = index(method_name.to_s)
22
+ indx || super
23
+ return unless values
24
+ values[indx]
25
+ end
26
+
27
+ def to_h
28
+ return {} unless values.present?
29
+ pg_result.fields.zip(values).to_h
30
+ end
31
+
32
+ def_delegator :pg_result, :index
33
+ end
34
+
35
+ # inheritance from row for delegation methods to first row... may be method missing?
36
+ class Result < Row
37
+ attr_reader :rows, :fields, :options
38
+
39
+ # TODO: as arguments keys, values and options? Future support of another db?
40
+ # arguments - PG::Result instance and hash of options
41
+ def initialize(result, options)
42
+ # how another db parsing data?
43
+ TypeMap.update(result, options[:types]) if options[:types]
44
+
45
+ @options = options
46
+ @fields = result.fields
47
+ @rows = []
48
+ result.each_row { |i| @rows << Row.new(self, i) }
49
+ result.clear # they advise to clear it, but maybe better to use it until presenter?
50
+ end
51
+
52
+ def present(o = {})
53
+ options = @options[:presenter_options] ? o.merge(@options[:presenter_options]) : o
54
+ Presenter.present(self, options)
55
+ end
56
+
57
+ # ResultRow inheritance support
58
+ def values
59
+ first&.values
60
+ end
61
+
62
+ def pg_result
63
+ self
64
+ end
65
+
66
+ def_delegators :@rows, :each, :each_with_index, :first, :last, :map, :count, :present?
67
+ def_delegator :@fields, :index
68
+ end
69
+ end
@@ -0,0 +1,63 @@
1
+ require 'pg'
2
+ require 'pg_hstore'
3
+
4
+ module Terrazine
5
+ # PG type map updater
6
+ class TypeMap
7
+ class << self
8
+ def update(pg_result, types)
9
+ # TODO! why it sometimes column_map?
10
+ t_m = pg_result.type_map
11
+ columns_map = t_m.is_a?(PG::TypeMapByColumn) ? t_m : t_m.build_column_map(pg_result)
12
+ coders = columns_map.coders.dup
13
+ types.each do |name, type|
14
+ coders[pg_result.fnumber(name.to_s)] = fetch_text_decoder type
15
+ end
16
+ pg_result.type_map = PG::TypeMapByColumn.new coders
17
+ end
18
+
19
+ def fetch_text_decoder(type)
20
+ # decoder inside decoder
21
+ # as example array of arrays with integers - type == [:array, :array, :integer]
22
+ if type.is_a?(Array)
23
+ decoder = new_text_decoder type.shift
24
+ assign_elements_type type, decoder
25
+ else
26
+ new_text_decoder type
27
+ end
28
+ end
29
+
30
+ def assign_elements_type(types, parent)
31
+ parent.elements_type = if types.count == 1
32
+ select_text_decoder(types.shift).new
33
+ else
34
+ type = types.shift
35
+ assign_elements_type(types, select_text_decoder(type))
36
+ end
37
+ parent
38
+ end
39
+
40
+ def new_text_decoder(type)
41
+ select_text_decoder(type).new
42
+ end
43
+
44
+ def select_text_decoder(type)
45
+ decoder = { array: PG::TextDecoder::Array,
46
+ float: PG::TextDecoder::Float,
47
+ boolaen: PG::TextDecoder::Boolean,
48
+ integer: PG::TextDecoder::Integer,
49
+ date: PG::TextDecoder::TimestampWithoutTimeZone,
50
+ hstore: Hstore,
51
+ json: PG::TextDecoder::JSON }[type]
52
+ raise "Undefined decoder #{type}" unless decoder
53
+ decoder
54
+ end
55
+ end
56
+ end
57
+
58
+ class Hstore < PG::SimpleDecoder
59
+ def decode(string, _tuple = nil, _field = nil)
60
+ PgHstore.load(string, true) if string.is_a? String
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Terrazine
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,90 @@
1
+ require_relative 'spec_helper'
2
+
3
+ # TODO.... -_-
4
+ describe Terrazine::Constructor do
5
+ before :each do
6
+ @constructor = Terrazine.new_constructor
7
+ end
8
+ before :all do
9
+ @permanent_c = Terrazine.new_constructor
10
+ end
11
+
12
+ it 'mrgl' do
13
+ expect(@constructor.class).to eql Terrazine::Constructor
14
+ end
15
+
16
+ context '`select`' do
17
+ it 'build simple structure' do
18
+ @constructor.select(:name)
19
+ @constructor.select('phone')
20
+ expect(structure(:select)).to eql [:name, 'phone']
21
+ end
22
+
23
+ it 'build hash structure' do
24
+ @constructor.select(u: [:name, :email])
25
+ @constructor.select _calls_count: [:_count, :connected]
26
+ expect(structure(:select)).to eq u: [:name, :email],
27
+ _calls_count: [:_count, :connected]
28
+ expect(@constructor.build_sql).to eq 'SELECT u.name, u.email, COUNT(connected) AS calls_count '
29
+ end
30
+
31
+ it 'build sub_queries' do
32
+ @constructor.select select: [:_count, [:_nullif, :connected, true]],
33
+ from: [:calls, :c],
34
+ 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 ) '
36
+ end
37
+
38
+ it 'build big structures' do
39
+ @permanent_c.select _calls_count: { select: [:_count, [:_nullif, :connected, true]],
40
+ from: [:calls, :c],
41
+ where: 'u.id = c.user_id' },
42
+ u: [:name, :phone, { _master: [:_nullif, :role, "'master'"] },
43
+ 'u.abilities, u.id', 'birthdate']
44
+ @permanent_c.select o: :client_name
45
+ @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 "
47
+ end
48
+ end
49
+
50
+ context '`from`' do
51
+ it 'build simple data structures' do
52
+ @constructor.from :users
53
+ expect(@constructor.build_sql).to eq 'FROM users '
54
+ @permanent_c.from [:users, :u]
55
+ expect(@permanent_c.build_sql).to match 'o.client_name, secure_id FROM users u $'
56
+ end
57
+
58
+ it 'build values' do
59
+ @constructor.from [:_values, [:_param, 'mrgl'], :r, ['type']]
60
+ expect(@constructor.build_sql).to eq ['FROM (VALUES($1)) AS r (type) ', ['mrgl']]
61
+ end
62
+
63
+ it 'build values and tables' do
64
+ @constructor.from [[:mrgl, :m], [:_values, [1, 2], :rgl, [:zgl, :gl]]]
65
+ expect(@constructor.build_sql).to eq 'FROM mrgl m, (VALUES(1, 2)) AS rgl (zgl, gl) '
66
+ end
67
+ end
68
+
69
+ context '`join`' do
70
+ it 'build simple join' do
71
+ @constructor.join 'users u ON u.id = m.user_id'
72
+ expect(@constructor.build_sql).to eq 'JOIN users u ON u.id = m.user_id '
73
+ @constructor.join ['users u ON u.id = m.user_id',
74
+ 'skills s ON u.id = s.user_id']
75
+ expect(@constructor.build_sql).to eq 'JOIN users u ON u.id = m.user_id JOIN skills s ON u.id = s.user_id '
76
+ end
77
+
78
+ it 'build big structures' do
79
+ @permanent_c.join [[[:masters, :m], { on: 'm.user_id = u.id' }],
80
+ [[:attachments, :a], { on: ['a.user_id = u.id',
81
+ 'a.type = 1'],
82
+ option: :left}]]
83
+ expect(@permanent_c.build_sql).to match 'FROM users u JOIN masters m ON m.user_id = u.id LEFT JOIN attachments a ON a.user_id = u.id AND a.type = 1 $'
84
+ end
85
+ end
86
+
87
+ context '`conditions`' do
88
+
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ require_relative '../lib/terrazine'
2
+
3
+ def structure(key)
4
+ @constructor.structure[key]
5
+ end
@@ -0,0 +1,5 @@
1
+ require_relative '../lib/terrazine'
2
+
3
+ describe Terrazine do
4
+
5
+ end
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+
3
+ require 'version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'terrazine'
7
+ spec.version = Terrazine::VERSION
8
+ spec.authors = ['Aeonax']
9
+ spec.email = ['aeonax.liar@gmail.com']
10
+
11
+ spec.summary = %q(Terrazine is a parser of data structures in to SQL)
12
+ spec.description = %q(You can take a look at [github]{https://github.com/Aeonax/terrazine}.)
13
+ spec.homepage = 'https://github.com/Aeonax/terrazine'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split("\n")
17
+ spec.test_files = `git ls-files -- {spec,features}/*`.split("\n")
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.16'
21
+ spec.add_development_dependency 'rake', '~> 10.0'
22
+ spec.add_development_dependency 'rspec', '~> 3.0'
23
+
24
+ spec.add_dependency 'pg-hstore', '1.2.0'
25
+
26
+ spec.required_ruby_version = '>= 2.3.0'
27
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terrazine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Aeonax
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg-hstore
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.0
69
+ description: You can take a look at [github]{https://github.com/Aeonax/terrazine}.
70
+ email:
71
+ - aeonax.liar@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rubocop.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - lib/helper.rb
83
+ - lib/terrazine.rb
84
+ - lib/terrazine/builder.rb
85
+ - lib/terrazine/config.rb
86
+ - lib/terrazine/constructor.rb
87
+ - lib/terrazine/presenter.rb
88
+ - lib/terrazine/result.rb
89
+ - lib/terrazine/type_map.rb
90
+ - lib/version.rb
91
+ - spec/constructor_spec.rb
92
+ - spec/spec_helper.rb
93
+ - spec/terrazin_spec.rb
94
+ - terrazine.gemspec
95
+ homepage: https://github.com/Aeonax/terrazine
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 2.3.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.5.2.1
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Terrazine is a parser of data structures in to SQL
119
+ test_files: []