jungle_path 0.0.0
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 +21 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +22 -0
- data/README.md +5 -0
- data/jungle_path.gemspec +43 -0
- data/lib/jungle_path/api/helpers/auth.rb +45 -0
- data/lib/jungle_path/api/helpers/auth_local_user.rb +284 -0
- data/lib/jungle_path/api/helpers/auth_old.rb +232 -0
- data/lib/jungle_path/api/helpers/data_cache.rb +20 -0
- data/lib/jungle_path/api/helpers/defaults.rb +83 -0
- data/lib/jungle_path/api/helpers/logging.rb +36 -0
- data/lib/jungle_path/api/helpers/query_filters.rb +15 -0
- data/lib/jungle_path/api/helpers/rescues.rb +15 -0
- data/lib/jungle_path/api/helpers/result.rb +16 -0
- data/lib/jungle_path/api/helpers/standard_apis.rb +280 -0
- data/lib/jungle_path/api/helpers.rb +16 -0
- data/lib/jungle_path/api/template.erb +35 -0
- data/lib/jungle_path/api.rb +5 -0
- data/lib/jungle_path/app/a.gitignore +1 -0
- data/lib/jungle_path/app/api/server_base.rb +95 -0
- data/lib/jungle_path/app/api/server_custom.rb +121 -0
- data/lib/jungle_path/app/api/server_gen.rb +11 -0
- data/lib/jungle_path/app/auth/authorization.rb +96 -0
- data/lib/jungle_path/app/config/a.gitignore +1 -0
- data/lib/jungle_path/app/config/config.rb +240 -0
- data/lib/jungle_path/app/config/override.rb +3 -0
- data/lib/jungle_path/app/config.ru +28 -0
- data/lib/jungle_path/app/logs/log_files_go_here +0 -0
- data/lib/jungle_path/app/run.sh +4 -0
- data/lib/jungle_path/app/schemas/schema.rb +21 -0
- data/lib/jungle_path/app/schemas/schema_all_in_one.rb +181 -0
- data/lib/jungle_path/app.rb +8 -0
- data/lib/jungle_path/authentication/auth_provider/default.rb +83 -0
- data/lib/jungle_path/authentication/auth_provider.rb +7 -0
- data/lib/jungle_path/authentication/data_provider/default.rb +144 -0
- data/lib/jungle_path/authentication/data_provider.rb +7 -0
- data/lib/jungle_path/authentication/helpers.rb +19 -0
- data/lib/jungle_path/authentication/identity.rb +30 -0
- data/lib/jungle_path/authentication/password_hash.rb +124 -0
- data/lib/jungle_path/authentication.rb +9 -0
- data/lib/jungle_path/authorization/filter.rb +106 -0
- data/lib/jungle_path/authorization/paths.rb +71 -0
- data/lib/jungle_path/authorization.rb +5 -0
- data/lib/jungle_path/cache.rb +36 -0
- data/lib/jungle_path/config.rb +65 -0
- data/lib/jungle_path/controller/authentication.rb +129 -0
- data/lib/jungle_path/controller/base.rb +193 -0
- data/lib/jungle_path/controller/helpers.rb +47 -0
- data/lib/jungle_path/controller/template.erb +14 -0
- data/lib/jungle_path/controller.rb +7 -0
- data/lib/jungle_path/db_access/import/db_dir.rb +74 -0
- data/lib/jungle_path/db_access/import/delete.rb +30 -0
- data/lib/jungle_path/db_access/import/insert.rb +168 -0
- data/lib/jungle_path/db_access/import/schema.rb +34 -0
- data/lib/jungle_path/db_access/import/select.rb +68 -0
- data/lib/jungle_path/db_access/import.rb +15 -0
- data/lib/jungle_path/db_access/io/chunked_file_reader.rb +62 -0
- data/lib/jungle_path/db_access/io/config.rb +19 -0
- data/lib/jungle_path/db_access/io/copy.rb +73 -0
- data/lib/jungle_path/db_access/io/db.rb +82 -0
- data/lib/jungle_path/db_access/io/delete.rb +23 -0
- data/lib/jungle_path/db_access/io/init_db.rb +39 -0
- data/lib/jungle_path/db_access/io/insert.rb +24 -0
- data/lib/jungle_path/db_access/io/schema.rb +21 -0
- data/lib/jungle_path/db_access/io/select.rb +44 -0
- data/lib/jungle_path/db_access/io/update.rb +36 -0
- data/lib/jungle_path/db_access/io.rb +104 -0
- data/lib/jungle_path/db_model/column.rb +186 -0
- data/lib/jungle_path/db_model/params.rb +60 -0
- data/lib/jungle_path/db_model/schema.rb +100 -0
- data/lib/jungle_path/db_model/string.rb +9 -0
- data/lib/jungle_path/db_model/table.rb +307 -0
- data/lib/jungle_path/db_model.rb +34 -0
- data/lib/jungle_path/exceptions.rb +10 -0
- data/lib/jungle_path/gen/api.rb +52 -0
- data/lib/jungle_path/gen/controller.rb +0 -0
- data/lib/jungle_path/gen/db.rb +0 -0
- data/lib/jungle_path/gen/schema.rb +47 -0
- data/lib/jungle_path/gen/schema_tree/filter.rb +33 -0
- data/lib/jungle_path/gen/schema_tree/match_columns.rb +54 -0
- data/lib/jungle_path/gen/schema_tree/match_table_data.rb +22 -0
- data/lib/jungle_path/gen/schema_tree/match_tables.rb +70 -0
- data/lib/jungle_path/gen/schema_tree/node.rb +39 -0
- data/lib/jungle_path/gen/schema_tree.rb +105 -0
- data/lib/jungle_path/gen.rb +9 -0
- data/lib/jungle_path/json/base.rb +29 -0
- data/lib/jungle_path/json/time.rb +8 -0
- data/lib/jungle_path/json.rb +6 -0
- data/lib/jungle_path/logging.rb +23 -0
- data/lib/jungle_path/query/alias_info.rb +16 -0
- data/lib/jungle_path/query/engine.rb +878 -0
- data/lib/jungle_path/query/entity.rb +141 -0
- data/lib/jungle_path/query/field.rb +28 -0
- data/lib/jungle_path/query/field_primary_key.rb +27 -0
- data/lib/jungle_path/query/filter.rb +34 -0
- data/lib/jungle_path/query/float_value.rb +16 -0
- data/lib/jungle_path/query/from.rb +33 -0
- data/lib/jungle_path/query/int_value.rb +16 -0
- data/lib/jungle_path/query/limit.rb +19 -0
- data/lib/jungle_path/query/nested_hash_sorter.rb +94 -0
- data/lib/jungle_path/query/operator.rb +17 -0
- data/lib/jungle_path/query/query.rb +23 -0
- data/lib/jungle_path/query/sort_field.rb +34 -0
- data/lib/jungle_path/query/sql_string.rb +145 -0
- data/lib/jungle_path/query/string_value.rb +16 -0
- data/lib/jungle_path/query.rb +19 -0
- data/lib/jungle_path/rack/basic_credentials.rb +70 -0
- data/lib/jungle_path/rack/json_body_parser.rb +41 -0
- data/lib/jungle_path/rack.rb +6 -0
- data/lib/jungle_path/schema/auth.rb +83 -0
- data/lib/jungle_path/schema/base.rb +6 -0
- data/lib/jungle_path/schema/db.rb +10 -0
- data/lib/jungle_path/schema/version.rb +19 -0
- data/lib/jungle_path/schema.rb +8 -0
- data/lib/jungle_path/sql/auth_local_user.rb +5 -0
- data/lib/jungle_path/sql/general.rb +10 -0
- data/lib/jungle_path/sql/helpers.rb +11 -0
- data/lib/jungle_path/sql/key.rb +107 -0
- data/lib/jungle_path/sql/query_filter.rb +5 -0
- data/lib/jungle_path/sql/role.rb +5 -0
- data/lib/jungle_path/sql/user.rb +35 -0
- data/lib/jungle_path/sql/user_role.rb +5 -0
- data/lib/jungle_path/sql.rb +12 -0
- data/lib/jungle_path.rb +13 -0
- data/test.rb +33 -0
- data/test2.rb +15 -0
- metadata +200 -0
@@ -0,0 +1,878 @@
|
|
1
|
+
#require 'pry-byebug'
|
2
|
+
#require 'pp'
|
3
|
+
#use: binding.pry where you want a break point.
|
4
|
+
require 'set'
|
5
|
+
require 'jungle_path/query'
|
6
|
+
module JunglePath
|
7
|
+
module Query
|
8
|
+
class Engine
|
9
|
+
attr_reader :root, :tables, :user, :apply_limit_offset_to_sql
|
10
|
+
|
11
|
+
#def initialize(tables_hash, user=nil, apply_limit_offset_to_sql=true)
|
12
|
+
def initialize(node_tree, user=nil, apply_limit_offset_to_sql=true)
|
13
|
+
@tables = node_tree.tables_hash
|
14
|
+
@user = user
|
15
|
+
@apply_limit_offset_to_sql = apply_limit_offset_to_sql
|
16
|
+
#@root = Gen.gen_node_tree(tables_hash)
|
17
|
+
@root = node_tree
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.run(query, db, level=7)
|
21
|
+
dataset = run_query(query, db)
|
22
|
+
rows = dataset_to_array(query, dataset) if level > 0
|
23
|
+
rows = sort_by_ids(query, rows) if level > 1
|
24
|
+
rows = transform_results_to_hash_array(query, rows) if level > 2
|
25
|
+
rows = add_arrays(query, rows) if level > 3
|
26
|
+
rows = combine_results(query, rows) if level > 4
|
27
|
+
rows = sort_by_original_sort(query, rows) if level > 5
|
28
|
+
rows = wrap_outer_objects(query, rows) if level > 6
|
29
|
+
if rows
|
30
|
+
if query.apply_limit_offset_to_sql
|
31
|
+
return rows
|
32
|
+
else
|
33
|
+
puts "query.root.limit: #{query.root.limit}."
|
34
|
+
puts "query.root.offset: #{query.root.offset}."
|
35
|
+
limit = 0
|
36
|
+
offset = 0
|
37
|
+
if query.root.limit and query.root.limit > 0
|
38
|
+
limit = query.root.limit
|
39
|
+
end
|
40
|
+
if query.root.offset and query.root.offset > 0
|
41
|
+
offset = query.root.offset
|
42
|
+
end
|
43
|
+
puts "rows.length: #{rows.length}."
|
44
|
+
return rows if limit == 0 and offset == 0
|
45
|
+
if offset > 0 and limit == 0
|
46
|
+
rows = rows[offset, -1]
|
47
|
+
rows = [] unless rows
|
48
|
+
return rows
|
49
|
+
end
|
50
|
+
rows = rows[offset, limit]
|
51
|
+
rows = [] unless rows
|
52
|
+
return rows
|
53
|
+
end
|
54
|
+
else
|
55
|
+
return dataset
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.run_query(query, db)
|
60
|
+
ds = db[query.sql, *query.values]
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_query_from_string(query_string)
|
64
|
+
tokens = tokenize(query_string).reverse
|
65
|
+
entity_root, values = parse_tokens(tokens)
|
66
|
+
#sql, aliases, symbols, sort_ids, primary_key_field_count = Engine.generate_sql @tables, (entity_root)
|
67
|
+
sql, aliases, symbols, sort_ids, primary_key_field_count = JunglePath::Query::SQLString.generate( self, (entity_root) )
|
68
|
+
JunglePath::Query::Query.new(entity_root, sql, values, aliases, symbols, sort_ids, primary_key_field_count, @apply_limit_offset_to_sql)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_tokens(tokens)
|
72
|
+
aliases = ('a'..'z').to_a.reverse
|
73
|
+
doubles = aliases.map{|a| a + a} #also add in "aa", "bb", ... "zz"
|
74
|
+
aliases = doubles.push(*aliases)
|
75
|
+
values = []
|
76
|
+
entity_root = process_entity(nil, nil, tokens, aliases, values)
|
77
|
+
return entity_root, values
|
78
|
+
end
|
79
|
+
|
80
|
+
def next_token(tokens)
|
81
|
+
token = tokens.pop
|
82
|
+
token
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_entity(parent_entity, token, tokens, aliases, values, symbol=nil, root_entity=nil, in_fields=false)
|
86
|
+
#puts "process_entity - parent: #{parent_entity != nil}; token: #{token}."
|
87
|
+
expect_entity = false
|
88
|
+
expect_fields = false
|
89
|
+
expect_filter = false
|
90
|
+
expect_sort = false
|
91
|
+
expect_limit = false
|
92
|
+
expect_offset = false
|
93
|
+
entity = nil
|
94
|
+
token = next_token(tokens) unless token
|
95
|
+
while token
|
96
|
+
if !entity
|
97
|
+
expect_entity = true
|
98
|
+
if token =~ /^[a-zA-Z_]/ # starts with alpha or underscore
|
99
|
+
#binding.pry
|
100
|
+
node = get_entity_node(token, parent_entity)
|
101
|
+
if node
|
102
|
+
entity = JunglePath::Query::Entity.new(token, aliases.pop, node, parent_entity, symbol)
|
103
|
+
root_entity = entity unless root_entity
|
104
|
+
expect_entity = false
|
105
|
+
expect_fields = true
|
106
|
+
else
|
107
|
+
raise "Entity #{token} was not found."
|
108
|
+
end
|
109
|
+
else
|
110
|
+
raise "Expected token to be an entity name. Token: #{token}"
|
111
|
+
end
|
112
|
+
elsif expect_offset and token.start_with? "["
|
113
|
+
expect_offset = false
|
114
|
+
entity.offset = JunglePath::Query::Limit.parse(token) #same format as Limit.
|
115
|
+
elsif expect_limit and token.start_with? "["
|
116
|
+
expect_limit = false
|
117
|
+
entity.limit = JunglePath::Query::Limit.parse(token)
|
118
|
+
expect_offset = true
|
119
|
+
elsif expect_sort and token == "("
|
120
|
+
if in_fields
|
121
|
+
raise "Sorts need to go in outer query -- they cannot be nested. Qualify fields using entity names. No qualification is needed for the outer entity."
|
122
|
+
end
|
123
|
+
expect_sort = false
|
124
|
+
entity.sort = process_sort(entity, tokens, aliases, root_entity)
|
125
|
+
expect_limit = true
|
126
|
+
elsif expect_filter and token == "("
|
127
|
+
expect_filter = false
|
128
|
+
entity.filter = process_filter(entity, tokens, aliases, values, root_entity)
|
129
|
+
if in_fields and tokens.length > 0 and (tokens.last == ")" or tokens.last == ",")
|
130
|
+
#puts "break"
|
131
|
+
break
|
132
|
+
end
|
133
|
+
expect_sort = true
|
134
|
+
elsif expect_fields and token == "("
|
135
|
+
expect_fields = false
|
136
|
+
entity.fields = process_fields(entity, tokens, aliases, values, root_entity)
|
137
|
+
#puts "back in entity"
|
138
|
+
if in_fields and tokens.length > 0 and (tokens.last == ")" or tokens.last == ",")
|
139
|
+
#puts "break"
|
140
|
+
break
|
141
|
+
end
|
142
|
+
expect_filter = true
|
143
|
+
else
|
144
|
+
raise "Unexpected token: #{token}."
|
145
|
+
end
|
146
|
+
token = next_token(tokens)
|
147
|
+
end
|
148
|
+
entity
|
149
|
+
end
|
150
|
+
|
151
|
+
def process_fields(entity, tokens, aliases, values, root_entity)
|
152
|
+
#puts "process_fields - entity.name: #{entity.name}."
|
153
|
+
fields = []
|
154
|
+
table = nil
|
155
|
+
token = next_token(tokens)
|
156
|
+
while token
|
157
|
+
#puts "token: #{token}."
|
158
|
+
if token == ")"
|
159
|
+
break
|
160
|
+
elsif token == ","
|
161
|
+
# do nothing
|
162
|
+
elsif token =~ /^[a-zA-Z_]/ # starts with alpha or underscore
|
163
|
+
node = get_entity_child_node(token, entity)
|
164
|
+
|
165
|
+
#Force PK fields onto field list:
|
166
|
+
if fields.length == 0
|
167
|
+
table_name = entity.node.child_table_name
|
168
|
+
table_name = entity.name unless table_name
|
169
|
+
#puts "#{entity.name}."
|
170
|
+
#puts "#{entity.node.child_table_name}."
|
171
|
+
table = tables[table_name]
|
172
|
+
table.primary_key_columns.values.each do |pk_column|
|
173
|
+
fields << JunglePath::Query::FieldPrimaryKey.new(pk_column.name, node, entity)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
if node and node.child_table_name
|
178
|
+
# actually a table (entity).
|
179
|
+
field = process_entity(entity, token, tokens, aliases, values, node.symbol, root_entity, true)
|
180
|
+
field.field_node = node
|
181
|
+
fields << field
|
182
|
+
elsif node
|
183
|
+
#puts "token, node, entity: #{token}, #{node.name}, #{entity.name}."
|
184
|
+
field_symbol = token.downcase.to_sym
|
185
|
+
is_secure = table.secure_columns.include? field_symbol
|
186
|
+
fields << JunglePath::Query::Field.new(token, node, entity, is_secure) unless fields.map{|n| n.name}.include?(field_symbol)
|
187
|
+
else
|
188
|
+
raise "Token #{token} is not a valid table nor column name for entity: #{entity.name}.\nchild nodes: #{entity.node.nodes}."
|
189
|
+
end
|
190
|
+
else
|
191
|
+
raise "Token #{token} is not a valid table nor column name."
|
192
|
+
end
|
193
|
+
token = next_token(tokens)
|
194
|
+
end
|
195
|
+
fields
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_filter(entity, tokens, aliases, values, root_entity)
|
199
|
+
#puts "process_filter..."
|
200
|
+
filter = []
|
201
|
+
nest_count = 0
|
202
|
+
expect_field = true
|
203
|
+
expect_value = false
|
204
|
+
expect_operator = false
|
205
|
+
expect_list = false
|
206
|
+
expect_connector = false
|
207
|
+
expect_comma = false
|
208
|
+
token = next_token(tokens)
|
209
|
+
#puts "token: #{token}"
|
210
|
+
while token
|
211
|
+
if expect_operator and JunglePath::Query::Engine.operators.include? token
|
212
|
+
expect_operator = false
|
213
|
+
filter << JunglePath::Query::Operator.parse(token)
|
214
|
+
if token == ">>" or token == "<<"
|
215
|
+
expect_list = true
|
216
|
+
else
|
217
|
+
expect_value = true
|
218
|
+
end
|
219
|
+
elsif expect_operator
|
220
|
+
raise "Expected token #{token} to be one of the following operators: #{Engine.operators.inspect}."
|
221
|
+
elsif expect_field and token == "("
|
222
|
+
nest_count += 1
|
223
|
+
filter << token
|
224
|
+
elsif expect_field and token == ")" and nest_count > 0
|
225
|
+
nest_count -= 1
|
226
|
+
filter << token
|
227
|
+
elsif expect_field and token == ")"
|
228
|
+
break
|
229
|
+
elsif expect_value and token == 'null'
|
230
|
+
# values << token #skip this token will use the literal null in filter.
|
231
|
+
filter << "null"
|
232
|
+
expect_value = false
|
233
|
+
expect_field = true
|
234
|
+
expect_connector = true
|
235
|
+
elsif expect_value and token =~ /^[0-9]/ # starts with number
|
236
|
+
if token.include? "."
|
237
|
+
values << JunglePath::Query::FloatValue.parse(token)
|
238
|
+
filter << "?"
|
239
|
+
else
|
240
|
+
values << JunglePath::Query::IntValue.parse(token)
|
241
|
+
filter << "?"
|
242
|
+
end
|
243
|
+
expect_value = false
|
244
|
+
expect_field = true
|
245
|
+
expect_connector = true
|
246
|
+
elsif expect_value and token =~ /^["']/ # starts with quote
|
247
|
+
values << JunglePath::Query::StringValue.parse(token)
|
248
|
+
filter << "?"
|
249
|
+
expect_value = false
|
250
|
+
expect_field = true
|
251
|
+
expect_connector = true
|
252
|
+
elsif expect_value and token.downcase == "true"
|
253
|
+
values << true
|
254
|
+
filter << "?"
|
255
|
+
expect_value = false
|
256
|
+
expect_field = true
|
257
|
+
expect_connector = true
|
258
|
+
elsif expect_value and token.downcase == "false"
|
259
|
+
values << false
|
260
|
+
filter << "?"
|
261
|
+
expect_value = false
|
262
|
+
expect_field = true
|
263
|
+
expect_connector = true
|
264
|
+
elsif expect_list and token == "("
|
265
|
+
filter << "("
|
266
|
+
filter << process_value_list(entity, tokens, aliases, root_entity, values)
|
267
|
+
expect_list = false
|
268
|
+
expect_field = true
|
269
|
+
elsif expect_field and expect_connector and JunglePath::Query::Engine.connectors.include? token.downcase
|
270
|
+
filter << token.downcase
|
271
|
+
expect_connector = false
|
272
|
+
elsif expect_field and Engine.connectors.include? token.downcase
|
273
|
+
raise "Did not expect connector token #{token}."
|
274
|
+
elsif expect_field and token =~ /^[a-zA-Z_]/ # starts with alpha or underscore
|
275
|
+
node = get_entity_child_node(token, entity)
|
276
|
+
if node and !node.child_table_name
|
277
|
+
#filter << Field.new(name, :column, node)
|
278
|
+
filter << "#{entity.alias_}.#{node.name}"
|
279
|
+
expect_field = false
|
280
|
+
expect_connector = false
|
281
|
+
expect_operator = true
|
282
|
+
else
|
283
|
+
raise "Token #{token} is not a valid column name."
|
284
|
+
end
|
285
|
+
else
|
286
|
+
raise "Token #{token} is not a valid column name. filter: #{filter.inspect}; tokens: #{tokens.inspect}."
|
287
|
+
end
|
288
|
+
token = next_token(tokens)
|
289
|
+
end
|
290
|
+
filter
|
291
|
+
end
|
292
|
+
|
293
|
+
def process_value_list(entity, tokens, aliases, root_entity, values)
|
294
|
+
list = []
|
295
|
+
expect_value = true
|
296
|
+
expect_comma = false
|
297
|
+
token = next_token(tokens)
|
298
|
+
while token
|
299
|
+
#puts "token: #{token}."
|
300
|
+
#puts "list: #{list.inspect}."
|
301
|
+
if list.empty? and token == ")"
|
302
|
+
list << ")"
|
303
|
+
break
|
304
|
+
elsif expect_comma and token == ")"
|
305
|
+
list << ")"
|
306
|
+
break
|
307
|
+
elsif expect_comma and token == ","
|
308
|
+
list << ", "
|
309
|
+
expect_comma = false
|
310
|
+
expect_value = true
|
311
|
+
elsif expect_comma
|
312
|
+
raise "Missing comma from 'in (list)'. list: #{list.inspect}; values: #{values.inspect}; tokens: #{tokens.inspect}."
|
313
|
+
elsif expect_value and token == ")"
|
314
|
+
raise "Found unexpected closing parenthisis: #{token} in value list following a comma: (,)."
|
315
|
+
elsif expect_value and token =~ /^[0-9]/ # starts with number
|
316
|
+
if token.include? "."
|
317
|
+
values << JunglePath::Query::FloatValue.filter(token)
|
318
|
+
list << "?"
|
319
|
+
else
|
320
|
+
values << JunglePath::Query::IntValue.parse(token)
|
321
|
+
list << "?"
|
322
|
+
end
|
323
|
+
expect_value = false
|
324
|
+
expect_comma = true
|
325
|
+
elsif expect_value and token =~ /^["']/ # starts with quote
|
326
|
+
values = JunglePath::Query::StringValue.parse(token)
|
327
|
+
list << "?"
|
328
|
+
expect_value = false
|
329
|
+
expect_comma = true
|
330
|
+
else
|
331
|
+
raise "Found unexpected token: #{token} in value list."
|
332
|
+
end
|
333
|
+
token = next_token(tokens)
|
334
|
+
end
|
335
|
+
list
|
336
|
+
end
|
337
|
+
|
338
|
+
def process_sort(entity, tokens, aliases, root_entity)
|
339
|
+
sort = []
|
340
|
+
sort_field = nil
|
341
|
+
expect_sort = false
|
342
|
+
token = next_token(tokens)
|
343
|
+
while token
|
344
|
+
if token == ")"
|
345
|
+
break
|
346
|
+
elsif token == ","
|
347
|
+
#do nothing
|
348
|
+
#sort << ", "
|
349
|
+
elsif expect_sort and JunglePath::Query::Engine.sorts.include? token.downcase
|
350
|
+
expect_sort = false
|
351
|
+
sort[-1].sort = token.downcase
|
352
|
+
elsif token =~ /^[a-zA-Z_]/ # starts with alpha or underscore
|
353
|
+
parts = token.downcase.split('.')
|
354
|
+
if parts.length > 1
|
355
|
+
table = parts[0].to_sym
|
356
|
+
field = parts[1].to_sym
|
357
|
+
else
|
358
|
+
table = nil
|
359
|
+
field = parts[0].to_sym
|
360
|
+
end
|
361
|
+
table_entity = nil
|
362
|
+
entities = nil
|
363
|
+
if table
|
364
|
+
table_entity, entities = find_table_entity(table, root_entity)
|
365
|
+
raise "Token #{token} leading part #{table} is not a valid table from your query." unless table_entity
|
366
|
+
end
|
367
|
+
if field
|
368
|
+
table_entity = entity unless table_entity
|
369
|
+
node = table_entity.node.nodes[field]
|
370
|
+
#puts "table_entity.name: #{table_entity.name}."
|
371
|
+
#puts "table_entity.node.name: #{table_entity.node.name}."
|
372
|
+
#puts "table_entity.node.nodes: #{table_entity.node.nodes}."
|
373
|
+
#puts "pp node:"
|
374
|
+
#pp node
|
375
|
+
#puts "zzz"
|
376
|
+
if node and node.child_table_name
|
377
|
+
raise "Field #{field} is not a valid field for table #{table} in your query,"
|
378
|
+
elsif node == nil and !@root.nodes[table_entity.name].nodes[field]
|
379
|
+
raise "Field #{field} is not a valid field for table #{table_entity.name} in your query,"
|
380
|
+
else
|
381
|
+
#sort_field = SortField.new(table, field, table_entity.node, node)
|
382
|
+
#sort << sort_field
|
383
|
+
#sort << "#{table_entity.alias_}.#{field}"
|
384
|
+
sort << JunglePath::Query::SortField.new("#{table_entity.alias_}.#{field}", field, entities)
|
385
|
+
expect_sort = true
|
386
|
+
end
|
387
|
+
end
|
388
|
+
else
|
389
|
+
raise "Token #{token} is not a valid table nor column name (or combination)."
|
390
|
+
end
|
391
|
+
token = next_token(tokens)
|
392
|
+
end
|
393
|
+
sort
|
394
|
+
end
|
395
|
+
|
396
|
+
def get_entity_node(token, parent_entity)
|
397
|
+
name = JunglePath::Query::Entity.get_name_from_token(token)
|
398
|
+
if parent_entity
|
399
|
+
node = parent_entity.node.nodes[name]
|
400
|
+
unless node
|
401
|
+
if parent_entity.node.child_table_name
|
402
|
+
node = @root.nodes[parent_entity.node.child_table_name].nodes[name]
|
403
|
+
else
|
404
|
+
node = @root.nodes[parent_entity.name].nodes[name]
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
node = @root.nodes[name] unless node
|
409
|
+
|
410
|
+
#new:
|
411
|
+
if node and node.class == ::Array
|
412
|
+
node = JunglePath::Query::Entity.select_node_from_array_by_token(node, token)
|
413
|
+
end
|
414
|
+
|
415
|
+
node
|
416
|
+
end
|
417
|
+
|
418
|
+
def get_entity_child_node(token, entity)
|
419
|
+
#puts "entity: #{entity.name}."
|
420
|
+
#puts "token: #{token}."
|
421
|
+
#puts "entity.node: #{entity.node}."
|
422
|
+
field_name = JunglePath::Query::Entity.get_name_from_token(token)
|
423
|
+
#puts "field_name: #{field_name}."
|
424
|
+
if entity.node.nodes.length > 0
|
425
|
+
node = entity.node.nodes[field_name]
|
426
|
+
else
|
427
|
+
#puts "entity.node.child_table_name: #{entity.node.child_table_name}."
|
428
|
+
node = @root.nodes[entity.node.child_table_name].nodes[field_name]
|
429
|
+
end
|
430
|
+
|
431
|
+
#new:
|
432
|
+
if node and node.class == ::Array #see gen_node_tree.rb line 60+.
|
433
|
+
# node is actually array of nodes, so pick correct one... token s/have column indicator, otherwise just grab first one.
|
434
|
+
node = JunglePath::Query::Entity.select_node_from_array_by_token(node, token)
|
435
|
+
end
|
436
|
+
|
437
|
+
node
|
438
|
+
end
|
439
|
+
|
440
|
+
def find_table_entity(name, parent_entity, level=0, entities=nil)
|
441
|
+
#binding.pry
|
442
|
+
entities = [] unless entities
|
443
|
+
entity = nil
|
444
|
+
parent_entity.fields.each do |field|
|
445
|
+
if field.is_entity? and field.name == name
|
446
|
+
entities << name
|
447
|
+
entity = field
|
448
|
+
break
|
449
|
+
elsif field.is_entity?
|
450
|
+
entities.push field.name
|
451
|
+
entity, entities = find_table_entity(name, field, level + 1, entities)
|
452
|
+
if entity
|
453
|
+
break
|
454
|
+
end
|
455
|
+
entities.pop
|
456
|
+
end
|
457
|
+
end
|
458
|
+
return entity, entities
|
459
|
+
end
|
460
|
+
|
461
|
+
def tokenize(query)
|
462
|
+
tokens = []
|
463
|
+
in_single_line_comment = nil
|
464
|
+
in_comment = 0
|
465
|
+
in_string = nil
|
466
|
+
in_escape = false
|
467
|
+
parens = 0
|
468
|
+
in_bracket = false
|
469
|
+
token = ""
|
470
|
+
i = 0
|
471
|
+
prev_char = nil
|
472
|
+
next_char = nil
|
473
|
+
query.each_char do |c|
|
474
|
+
i += 1
|
475
|
+
next_char = query[i]
|
476
|
+
if in_escape and c == "\\"
|
477
|
+
in_escape = false
|
478
|
+
token << "\\"
|
479
|
+
elsif in_escape and c == "t"
|
480
|
+
in_escape = false
|
481
|
+
token << "\t"
|
482
|
+
elsif in_escape and c == "n"
|
483
|
+
in_escape = false
|
484
|
+
token << "\n"
|
485
|
+
elsif in_escape and c == "r"
|
486
|
+
in_escape = false
|
487
|
+
token << "\r"
|
488
|
+
elsif in_escape and c == "\'"
|
489
|
+
in_escape = false
|
490
|
+
token << "\'"
|
491
|
+
elsif in_escape and c == "\""
|
492
|
+
in_escape = false
|
493
|
+
token << "\""
|
494
|
+
elsif in_escape
|
495
|
+
raise "Invalid character #{c} was escaped. You can only escape as follows: \\\\ \\t \\n \\r \\\' \\\" got it? :)"
|
496
|
+
elsif in_string and c.strip.empty? # is whitespace?
|
497
|
+
token << c
|
498
|
+
elsif in_string and in_string == c
|
499
|
+
token = token << c
|
500
|
+
tokens << token
|
501
|
+
token = ""
|
502
|
+
in_string = nil
|
503
|
+
elsif in_string and c == "\\"
|
504
|
+
in_escape = true
|
505
|
+
elsif in_string
|
506
|
+
token << c
|
507
|
+
elsif in_comment > 0 and c == "*"
|
508
|
+
#do nothing
|
509
|
+
elsif in_comment > 0 and prev_char == "*" and c == "/"
|
510
|
+
in_comment -= 1
|
511
|
+
elsif in_single_line_comment and (c == "\n" or c == "\r")
|
512
|
+
in_single_line_comment = false
|
513
|
+
elsif in_single_line_comment
|
514
|
+
#do nothing
|
515
|
+
elsif c == "/" and next_char == "*"
|
516
|
+
in_comment += 1
|
517
|
+
elsif in_comment > 0
|
518
|
+
#do nothing
|
519
|
+
elsif c == "-" and next_char == "-"
|
520
|
+
if token.length > 0
|
521
|
+
tokens << token
|
522
|
+
token = ""
|
523
|
+
end
|
524
|
+
in_single_line_comment = true
|
525
|
+
elsif in_bracket and c == "]"
|
526
|
+
in_bracket = false
|
527
|
+
token << c
|
528
|
+
tokens << token
|
529
|
+
token = ""
|
530
|
+
elsif in_bracket
|
531
|
+
token << c
|
532
|
+
elsif c == "\""
|
533
|
+
if token.length > 0
|
534
|
+
tokens << token
|
535
|
+
token = ""
|
536
|
+
end
|
537
|
+
in_string = c
|
538
|
+
token << c
|
539
|
+
elsif c == "'"
|
540
|
+
if token.length > 0
|
541
|
+
tokens << token
|
542
|
+
token = ""
|
543
|
+
end
|
544
|
+
in_string = c
|
545
|
+
token << c
|
546
|
+
elsif c.strip.empty? # is whitespace?
|
547
|
+
if token.length > 0
|
548
|
+
tokens << token
|
549
|
+
token = ""
|
550
|
+
end
|
551
|
+
elsif c == ","
|
552
|
+
if token.length > 0
|
553
|
+
tokens << token
|
554
|
+
token = ""
|
555
|
+
end
|
556
|
+
tokens << ","
|
557
|
+
elsif c == "("
|
558
|
+
parens += 1
|
559
|
+
if token.length > 0
|
560
|
+
tokens << token
|
561
|
+
token = ""
|
562
|
+
end
|
563
|
+
tokens << "("
|
564
|
+
elsif c == ")"
|
565
|
+
parens -= 1
|
566
|
+
if parens < 0
|
567
|
+
raise "Extra ')' character found at #{i}."
|
568
|
+
end
|
569
|
+
if token.length > 0
|
570
|
+
tokens << token
|
571
|
+
token = ""
|
572
|
+
end
|
573
|
+
tokens << ")"
|
574
|
+
elsif c == "["
|
575
|
+
in_bracket = true
|
576
|
+
if token.length > 0
|
577
|
+
tokens << token
|
578
|
+
token = ""
|
579
|
+
end
|
580
|
+
token << c
|
581
|
+
else
|
582
|
+
token << c
|
583
|
+
end
|
584
|
+
prev_char = c
|
585
|
+
end
|
586
|
+
if in_bracket
|
587
|
+
raise "No closing ']' character was found."
|
588
|
+
end
|
589
|
+
if parens > 0
|
590
|
+
raise "Missing #{parens} ')' characters."
|
591
|
+
end
|
592
|
+
if in_string
|
593
|
+
raise "Unclosed string: missing #{in_string} character."
|
594
|
+
end
|
595
|
+
if token.length > 0
|
596
|
+
tokens << token
|
597
|
+
token = ""
|
598
|
+
end
|
599
|
+
tokens
|
600
|
+
end
|
601
|
+
|
602
|
+
def self.operators
|
603
|
+
Set.new(["==", "!=", ">", "<", ">=", "<=", "~", "=~", "~~", ">>", "<<", "is", "is!"])
|
604
|
+
end
|
605
|
+
|
606
|
+
def self.operator_map
|
607
|
+
{"==" => "=", "!=" => "!=", ">" => ">", "<" => "<", ">=" => ">=", "<=" => "<=", "~" => "ilike", "=~" => "regex", "~~" => "full text search", ">>" => "in", "<<" => "not in", "is" => "is", "is!" => "is not"}
|
608
|
+
end
|
609
|
+
|
610
|
+
def self.connectors
|
611
|
+
Set.new(["and", "or"])
|
612
|
+
end
|
613
|
+
|
614
|
+
def self.sorts
|
615
|
+
Set.new(["asc", "desc"])
|
616
|
+
end
|
617
|
+
|
618
|
+
def self.dataset_to_array(query, dataset)
|
619
|
+
results = []
|
620
|
+
dataset.each do |row|
|
621
|
+
results << row
|
622
|
+
end
|
623
|
+
results
|
624
|
+
end
|
625
|
+
|
626
|
+
def self.sort_by_ids(query, results)
|
627
|
+
keys = query.sort_ids
|
628
|
+
if results.length > 0
|
629
|
+
results = results.sort_by {|h| (h.reject {|x| !keys.include?(x)}).values }
|
630
|
+
end
|
631
|
+
results
|
632
|
+
end
|
633
|
+
|
634
|
+
def self.calc_paths(query)
|
635
|
+
# For each sort field, find the path to its value in the nested hash.
|
636
|
+
# Returns an array containing an arrays of hash keys. (One array of keys for each sort field.)
|
637
|
+
sort_fields = query.root.sort
|
638
|
+
paths = []
|
639
|
+
sort_fields.each do |sort_field|
|
640
|
+
keys = []
|
641
|
+
paths << keys
|
642
|
+
if sort_field.entities
|
643
|
+
sort_field.entities.each do |e|
|
644
|
+
keys << e.to_sym
|
645
|
+
end
|
646
|
+
end
|
647
|
+
keys << sort_field.field_name
|
648
|
+
end
|
649
|
+
paths
|
650
|
+
end
|
651
|
+
|
652
|
+
def self.calc_sort_orders(query)
|
653
|
+
# For each sort field, add its ordering (:asc or :desc) into the returned array.
|
654
|
+
sort_fields = query.root.sort
|
655
|
+
sort_orders = []
|
656
|
+
sort_fields.each do |sort_field|
|
657
|
+
sort_orders << sort_field.sort.to_sym
|
658
|
+
end
|
659
|
+
sort_orders
|
660
|
+
end
|
661
|
+
|
662
|
+
def self.wrap_outer_objects(query, rows)
|
663
|
+
rows.map {|row| {query.root.name => row}}
|
664
|
+
end
|
665
|
+
|
666
|
+
def self.sort_by_original_sort(query, rows)
|
667
|
+
if query.root.sort and query.root.sort.length > 0
|
668
|
+
paths = calc_paths(query)
|
669
|
+
sort_orders = calc_sort_orders(query)
|
670
|
+
sorter = JunglePath::Query::NestedHashSorter.new(paths, sort_orders)
|
671
|
+
begin
|
672
|
+
#strange: when I added this debugging code, modes_of_action.uql sorting on code started working...
|
673
|
+
#was getting: ArgumentError - comparison of Hash with Hash failed.
|
674
|
+
#very strange!
|
675
|
+
count = 0
|
676
|
+
rows = rows.sort do |a, b|
|
677
|
+
count += 1
|
678
|
+
val = sorter.sort(a, b)
|
679
|
+
val
|
680
|
+
end
|
681
|
+
rescue
|
682
|
+
puts "count: #{count}."
|
683
|
+
raise
|
684
|
+
end
|
685
|
+
end
|
686
|
+
rows
|
687
|
+
end
|
688
|
+
|
689
|
+
def self.transform_results_to_hash_array(query, rows)
|
690
|
+
# transform results into nested hashes (for converting to json later...)
|
691
|
+
results = []
|
692
|
+
rows.each do |row|
|
693
|
+
#binding.pry
|
694
|
+
a_stack = []
|
695
|
+
h_stack = []
|
696
|
+
current_alias = nil
|
697
|
+
current_hash = nil
|
698
|
+
row.each do |k, v| #each item in this row...
|
699
|
+
new_alias, field_name = k.to_s.split(".").map{|a| a.to_sym}
|
700
|
+
parent_alias = query.aliases[new_alias].parent_alias
|
701
|
+
#parent_alias = parent_alias.to_sym if parent_alias
|
702
|
+
|
703
|
+
#load stack:
|
704
|
+
if current_alias
|
705
|
+
a_stack.push(current_alias)
|
706
|
+
h_stack.push(current_hash)
|
707
|
+
end
|
708
|
+
|
709
|
+
#unload stack:
|
710
|
+
index = a_stack.index(new_alias)
|
711
|
+
if index
|
712
|
+
# We've seen this new alias before. Get it (and the related hash) out of the stacks:
|
713
|
+
begin
|
714
|
+
current_alias = a_stack.pop
|
715
|
+
current_hash = h_stack.pop
|
716
|
+
end until current_alias == new_alias
|
717
|
+
current_hash[field_name] = v
|
718
|
+
|
719
|
+
elsif current_alias
|
720
|
+
if a_stack.include?(parent_alias)
|
721
|
+
|
722
|
+
#get parent hash
|
723
|
+
begin
|
724
|
+
temp_parent_alias = a_stack.pop
|
725
|
+
temp_parent_hash = h_stack.pop
|
726
|
+
end until temp_parent_alias == parent_alias
|
727
|
+
|
728
|
+
# put new hash into parent hash:
|
729
|
+
new_hash = {}
|
730
|
+
temp_parent_hash[query.aliases[new_alias.to_sym].name ] = new_hash # (convert alias to entity name)
|
731
|
+
a_stack.push temp_parent_alias
|
732
|
+
h_stack.push temp_parent_hash
|
733
|
+
|
734
|
+
# we now have new currents:
|
735
|
+
current_alias = new_alias
|
736
|
+
current_hash = new_hash
|
737
|
+
current_hash[field_name] = v
|
738
|
+
else
|
739
|
+
raise "parent_alias: #{parent_alias} was not found in a_stack: #{a_stack}. (h_stack: #{h_stack}).\nresults:\n#{results}."
|
740
|
+
end
|
741
|
+
else
|
742
|
+
#completely new:
|
743
|
+
current_alias = new_alias
|
744
|
+
current_hash = {}
|
745
|
+
current_hash[field_name] = v
|
746
|
+
end
|
747
|
+
end
|
748
|
+
# get the earliest parent (which contains all of the rest) and add to results:
|
749
|
+
while h_stack.length > 0
|
750
|
+
current_hash = h_stack.pop
|
751
|
+
end
|
752
|
+
results << current_hash
|
753
|
+
#break # <= for debugging only
|
754
|
+
end
|
755
|
+
results
|
756
|
+
end
|
757
|
+
|
758
|
+
def self.add_arrays(query, rows)
|
759
|
+
results = []
|
760
|
+
rows.each do |hash|
|
761
|
+
symbols = query.symbols.reverse
|
762
|
+
results << add_array(symbols, hash)
|
763
|
+
end
|
764
|
+
results
|
765
|
+
end
|
766
|
+
|
767
|
+
def self.add_array(symbols, hash)
|
768
|
+
result = {}
|
769
|
+
hash.each do |name, value|
|
770
|
+
if value.class == Hash
|
771
|
+
symbol = symbols.pop
|
772
|
+
if symbol == "<="
|
773
|
+
result[name] = []
|
774
|
+
result[name] << add_array(symbols, value)
|
775
|
+
else
|
776
|
+
result[name] = add_array(symbols, value)
|
777
|
+
end
|
778
|
+
else
|
779
|
+
result[name] = value
|
780
|
+
end
|
781
|
+
end
|
782
|
+
result
|
783
|
+
end
|
784
|
+
|
785
|
+
def self.primary_keys_match?(row_a, row_b, pk_fld_count)
|
786
|
+
# most common:
|
787
|
+
if pk_fld_count == 1
|
788
|
+
if row_a.first[1] == row_b.first[1]
|
789
|
+
return true
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
# multiple PKs:
|
794
|
+
values_a = row_a.values
|
795
|
+
values_b = row_b.values
|
796
|
+
values_a.each_index do |i|
|
797
|
+
if i == pk_fld_count
|
798
|
+
break
|
799
|
+
end
|
800
|
+
if values_a[i] != values_b[i]
|
801
|
+
return false
|
802
|
+
end
|
803
|
+
end
|
804
|
+
return true
|
805
|
+
end
|
806
|
+
|
807
|
+
def self.combine_results(query, rows)
|
808
|
+
results = []
|
809
|
+
|
810
|
+
aliases = {}
|
811
|
+
query.aliases.values.each do |ai|
|
812
|
+
aliases[ai.name] = ai
|
813
|
+
end
|
814
|
+
|
815
|
+
rows.each do |new_row|
|
816
|
+
if results.length == 0
|
817
|
+
results << new_row
|
818
|
+
else
|
819
|
+
begin
|
820
|
+
previous_row = results[-1]
|
821
|
+
if primary_keys_match?(previous_row, new_row, aliases.values[0].primary_key_columns_count)
|
822
|
+
#merge the row...
|
823
|
+
results[-1 ] = merge_hash(previous_row, new_row, aliases)
|
824
|
+
else
|
825
|
+
# add the row...
|
826
|
+
results << new_row
|
827
|
+
end
|
828
|
+
rescue
|
829
|
+
puts "rescue..."
|
830
|
+
puts previous_row
|
831
|
+
puts ""
|
832
|
+
puts new_row
|
833
|
+
puts ""
|
834
|
+
raise
|
835
|
+
end
|
836
|
+
end
|
837
|
+
end
|
838
|
+
results
|
839
|
+
end
|
840
|
+
|
841
|
+
def self.merge_hash(a_hash, b_hash, aliases) #pk_fld_counts)
|
842
|
+
h = {}
|
843
|
+
a = a_hash.to_a # [[name, value], [name, value], ... ]
|
844
|
+
b = b_hash.to_a # [[name, value], [name, value], ... ]
|
845
|
+
a.each_index do |i|
|
846
|
+
a_key = a[i][0] # name
|
847
|
+
a_value = a[i][1] # value
|
848
|
+
b_value = b[i][1] # value
|
849
|
+
if a_value.class == Array
|
850
|
+
|
851
|
+
pk_fld_count = aliases[a_key].primary_key_columns_count
|
852
|
+
|
853
|
+
h[a_key] = a_value # a array
|
854
|
+
a_hash = a_value[-1] # do we merge last element?
|
855
|
+
b_hash = b_value[0] # compare to value from new row...
|
856
|
+
if primary_keys_match?(a_hash, b_hash, pk_fld_count)
|
857
|
+
h[a_key][-1] = merge_hash(a_hash, b_hash, aliases)
|
858
|
+
else
|
859
|
+
h[a_key] << b_hash # add value from new row to array
|
860
|
+
end
|
861
|
+
elsif a_value.class == Hash
|
862
|
+
pk_fld_count = aliases[a_key].primary_key_columns_count
|
863
|
+
|
864
|
+
if primary_keys_match?(a_value, b_value, pk_fld_count)
|
865
|
+
h[a_key] = merge_hash(a_value, b_value, aliases)
|
866
|
+
else
|
867
|
+
raise "unexpected multiple values found!!! Expected\na_value: '#{a_value}'\npk to match\nb_value: '#{b_value}'\npk."
|
868
|
+
end
|
869
|
+
else
|
870
|
+
# regular field so a_value and b_value are the same.
|
871
|
+
h[a_key] = a_value
|
872
|
+
end
|
873
|
+
end
|
874
|
+
h
|
875
|
+
end
|
876
|
+
end
|
877
|
+
end
|
878
|
+
end
|