terrazine 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []