terrazine 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +10 -0
- data/Gemfile +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +6 -0
- data/lib/helper.rb +5 -0
- data/lib/terrazine.rb +59 -0
- data/lib/terrazine/builder.rb +251 -0
- data/lib/terrazine/config.rb +19 -0
- data/lib/terrazine/constructor.rb +163 -0
- data/lib/terrazine/presenter.rb +39 -0
- data/lib/terrazine/result.rb +69 -0
- data/lib/terrazine/type_map.rb +63 -0
- data/lib/version.rb +3 -0
- data/spec/constructor_spec.rb +90 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/terrazin_spec.rb +5 -0
- data/terrazine.gemspec +27 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -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'
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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?
|
data/Rakefile
ADDED
data/lib/terrazine.rb
ADDED
@@ -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
|
data/lib/version.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
data/terrazine.gemspec
ADDED
@@ -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: []
|