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.
- 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: []
|