wowsql-sdk 1.2.0 → 3.0.1
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 +4 -4
- data/README.md +359 -417
- data/lib/wowmysql.rb +3 -2
- data/lib/wowsql/auth.rb +533 -0
- data/lib/wowsql/client.rb +54 -25
- data/lib/wowsql/exceptions.rb +15 -14
- data/lib/wowsql/query_builder.rb +239 -104
- data/lib/wowsql/schema.rb +250 -0
- data/lib/wowsql/storage.rb +380 -0
- data/lib/wowsql/table.rb +118 -9
- data/lib/wowsql/version.rb +1 -1
- data/lib/wowsql.rb +3 -2
- metadata +23 -6
data/lib/wowsql/exceptions.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
module WOWSQL
|
|
2
|
-
|
|
3
|
-
class WOWSQLException < StandardError
|
|
2
|
+
class WOWSQLError < StandardError
|
|
4
3
|
attr_reader :status_code, :response
|
|
5
4
|
|
|
6
5
|
def initialize(message, status_code = nil, response = nil)
|
|
@@ -10,22 +9,24 @@ module WOWSQL
|
|
|
10
9
|
end
|
|
11
10
|
end
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
end
|
|
12
|
+
WOWSQLException = WOWSQLError
|
|
13
|
+
|
|
14
|
+
class StorageError < WOWSQLError; end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
super(message, 413, response)
|
|
16
|
+
class StorageLimitExceededError < StorageError
|
|
17
|
+
def initialize(message, status_code = 413, response = nil)
|
|
18
|
+
super(message, status_code, response)
|
|
21
19
|
end
|
|
22
20
|
end
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
StorageException = StorageError
|
|
23
|
+
StorageLimitExceededException = StorageLimitExceededError
|
|
24
|
+
|
|
25
|
+
class SchemaPermissionError < WOWSQLError
|
|
26
|
+
def initialize(message, status_code = 403, response = nil)
|
|
27
|
+
super(message, status_code, response)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
-
end
|
|
31
30
|
|
|
31
|
+
PermissionException = SchemaPermissionError
|
|
32
|
+
end
|
data/lib/wowsql/query_builder.rb
CHANGED
|
@@ -4,187 +4,322 @@ module WOWSQL
|
|
|
4
4
|
def initialize(client, table_name)
|
|
5
5
|
@client = client
|
|
6
6
|
@table_name = table_name
|
|
7
|
-
@
|
|
8
|
-
@select_columns = nil
|
|
9
|
-
@filters = []
|
|
10
|
-
@group_by = nil
|
|
11
|
-
@having = []
|
|
12
|
-
@order_items = nil
|
|
7
|
+
@options = {}
|
|
13
8
|
end
|
|
14
9
|
|
|
10
|
+
# Select specific columns or expressions.
|
|
11
|
+
#
|
|
12
|
+
# @param columns [Array<String>] Column names or expressions
|
|
13
|
+
# @return [QueryBuilder] self for chaining
|
|
15
14
|
def select(*columns)
|
|
16
|
-
|
|
15
|
+
if columns.length == 1 && columns[0].is_a?(Array)
|
|
16
|
+
@options['select'] = columns[0]
|
|
17
|
+
else
|
|
18
|
+
@options['select'] = columns.length > 1 ? columns.map(&:to_s) : (columns.first || '*')
|
|
19
|
+
end
|
|
17
20
|
self
|
|
18
21
|
end
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
# Add a filter condition.
|
|
24
|
+
#
|
|
25
|
+
# @param column [String, Hash] Column name or filter hash
|
|
26
|
+
# @param operator [String, nil] Operator (eq, neq, gt, gte, lt, lte, like, in, not_in, between, not_between, is, is_not)
|
|
27
|
+
# @param value [Object] Filter value
|
|
28
|
+
# @param logical_op [String] "AND" or "OR"
|
|
29
|
+
# @return [QueryBuilder] self for chaining
|
|
30
|
+
def filter(column, operator = nil, value = nil, logical_op: 'AND')
|
|
31
|
+
@options['filter'] ||= []
|
|
32
|
+
|
|
33
|
+
if column.is_a?(Hash)
|
|
34
|
+
if column.key?('column') || column.key?(:column)
|
|
35
|
+
col = column['column'] || column[:column]
|
|
36
|
+
op = column['operator'] || column[:operator]
|
|
37
|
+
val = column['value'] || column[:value]
|
|
38
|
+
lo = column['logical_op'] || column[:logical_op] || logical_op
|
|
39
|
+
@options['filter'] << { column: col, operator: op, value: val, logical_op: lo }
|
|
40
|
+
else
|
|
41
|
+
column.each do |col_name, col_value|
|
|
42
|
+
@options['filter'] << { column: col_name.to_s, operator: 'eq', value: col_value, logical_op: logical_op }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "filter() missing required argument: 'operator'" if operator.nil?
|
|
47
|
+
@options['filter'] << { column: column, operator: operator, value: value, logical_op: logical_op }
|
|
48
|
+
end
|
|
49
|
+
|
|
22
50
|
self
|
|
23
51
|
end
|
|
24
52
|
|
|
53
|
+
def eq(column, value)
|
|
54
|
+
filter(column, 'eq', value)
|
|
55
|
+
end
|
|
56
|
+
|
|
25
57
|
def neq(column, value)
|
|
26
|
-
|
|
27
|
-
self
|
|
58
|
+
filter(column, 'neq', value)
|
|
28
59
|
end
|
|
29
60
|
|
|
30
61
|
def gt(column, value)
|
|
31
|
-
|
|
32
|
-
self
|
|
62
|
+
filter(column, 'gt', value)
|
|
33
63
|
end
|
|
34
64
|
|
|
35
65
|
def gte(column, value)
|
|
36
|
-
|
|
37
|
-
self
|
|
66
|
+
filter(column, 'gte', value)
|
|
38
67
|
end
|
|
39
68
|
|
|
40
69
|
def lt(column, value)
|
|
41
|
-
|
|
42
|
-
self
|
|
70
|
+
filter(column, 'lt', value)
|
|
43
71
|
end
|
|
44
72
|
|
|
45
73
|
def lte(column, value)
|
|
46
|
-
|
|
47
|
-
self
|
|
74
|
+
filter(column, 'lte', value)
|
|
48
75
|
end
|
|
49
76
|
|
|
50
77
|
def like(column, pattern)
|
|
51
|
-
|
|
52
|
-
self
|
|
78
|
+
filter(column, 'like', pattern)
|
|
53
79
|
end
|
|
54
80
|
|
|
55
81
|
def is_null(column)
|
|
56
|
-
|
|
57
|
-
self
|
|
82
|
+
filter(column, 'is', nil)
|
|
58
83
|
end
|
|
59
84
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
def is_not_null(column)
|
|
86
|
+
filter(column, 'is_not', nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Filter where column is in list of values.
|
|
90
|
+
def in_list(column, values)
|
|
91
|
+
filter(column, 'in', values)
|
|
63
92
|
end
|
|
64
93
|
|
|
65
94
|
def not_in(column, values)
|
|
66
|
-
|
|
67
|
-
self
|
|
95
|
+
filter(column, 'not_in', values)
|
|
68
96
|
end
|
|
69
97
|
|
|
70
|
-
def between(column,
|
|
71
|
-
|
|
72
|
-
self
|
|
98
|
+
def between(column, min_val, max_val)
|
|
99
|
+
filter(column, 'between', [min_val, max_val])
|
|
73
100
|
end
|
|
74
101
|
|
|
75
|
-
def not_between(column,
|
|
76
|
-
|
|
77
|
-
self
|
|
102
|
+
def not_between(column, min_val, max_val)
|
|
103
|
+
filter(column, 'not_between', [min_val, max_val])
|
|
78
104
|
end
|
|
79
105
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
106
|
+
# Add an OR filter condition.
|
|
107
|
+
def or_filter(column, operator, value)
|
|
108
|
+
filter(column, operator, value, logical_op: 'OR')
|
|
83
109
|
end
|
|
84
110
|
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
# Order results by column(s).
|
|
112
|
+
#
|
|
113
|
+
# @param column [String, Array] Column name or array of order items
|
|
114
|
+
# @param direction [String, nil] 'asc' or 'desc'
|
|
115
|
+
# @return [QueryBuilder] self for chaining
|
|
116
|
+
def order_by(column, direction = nil)
|
|
117
|
+
if column.is_a?(String)
|
|
118
|
+
@options['order'] = column
|
|
119
|
+
@options['order_direction'] = direction if direction
|
|
120
|
+
else
|
|
121
|
+
order_items = column.map do |item|
|
|
122
|
+
if item.is_a?(Hash)
|
|
123
|
+
item
|
|
124
|
+
elsif item.is_a?(Array) && item.length == 2
|
|
125
|
+
{ column: item[0], direction: item[1] }
|
|
126
|
+
end
|
|
127
|
+
end.compact
|
|
128
|
+
@options['order'] = order_items
|
|
129
|
+
end
|
|
87
130
|
self
|
|
88
131
|
end
|
|
89
132
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
133
|
+
# Order results by a single column (alias for order_by).
|
|
134
|
+
def order(column, direction = 'asc')
|
|
135
|
+
order_by(column, direction)
|
|
93
136
|
end
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
|
|
138
|
+
# Limit number of results.
|
|
139
|
+
def limit(n)
|
|
140
|
+
@options['limit'] = n
|
|
97
141
|
self
|
|
98
142
|
end
|
|
99
143
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
@
|
|
144
|
+
# Skip records (pagination).
|
|
145
|
+
def offset(n)
|
|
146
|
+
@options['offset'] = n
|
|
103
147
|
self
|
|
104
148
|
end
|
|
105
149
|
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
# Group results by column(s).
|
|
151
|
+
#
|
|
152
|
+
# @param columns [String, Array<String>] Column(s) to group by
|
|
153
|
+
# @return [QueryBuilder] self for chaining
|
|
154
|
+
def group_by(*columns)
|
|
155
|
+
if columns.length == 1 && columns[0].is_a?(Array)
|
|
156
|
+
@options['group_by'] = columns[0]
|
|
157
|
+
elsif columns.length == 1 && columns[0].is_a?(String)
|
|
158
|
+
@options['group_by'] = [columns[0]]
|
|
159
|
+
else
|
|
160
|
+
@options['group_by'] = columns.map(&:to_s)
|
|
161
|
+
end
|
|
108
162
|
self
|
|
109
163
|
end
|
|
110
164
|
|
|
111
|
-
|
|
112
|
-
|
|
165
|
+
# Add a HAVING clause filter.
|
|
166
|
+
#
|
|
167
|
+
# @param column [String] Column or aggregate expression
|
|
168
|
+
# @param operator [String] Operator (eq, neq, gt, gte, lt, lte)
|
|
169
|
+
# @param value [Object] Filter value
|
|
170
|
+
# @return [QueryBuilder] self for chaining
|
|
171
|
+
def having(column, operator, value)
|
|
172
|
+
@options['having'] ||= []
|
|
173
|
+
@options['having'] << { column: column, operator: operator, value: value }
|
|
113
174
|
self
|
|
114
175
|
end
|
|
115
176
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
@filters.any? { |f| ['in', 'not_in', 'between', 'not_between'].include?(f[:operator]) }
|
|
177
|
+
# Execute the query.
|
|
178
|
+
#
|
|
179
|
+
# @return [Hash] Query response with data and metadata
|
|
180
|
+
def get(additional_options = nil)
|
|
181
|
+
final_options = @options.dup
|
|
182
|
+
final_options.merge!(additional_options) if additional_options
|
|
123
183
|
|
|
124
|
-
|
|
125
|
-
# Use POST endpoint for advanced queries
|
|
126
|
-
body = build_query_body
|
|
127
|
-
@client.request('POST', "/#{@table_name}/query", nil, body)
|
|
128
|
-
else
|
|
129
|
-
# Use GET endpoint for simple queries (backward compatibility)
|
|
130
|
-
build_get_params
|
|
131
|
-
@client.request('GET', "/#{@table_name}", @params, nil)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def first
|
|
136
|
-
result = limit(1).get
|
|
137
|
-
result['data']&.first
|
|
138
|
-
end
|
|
184
|
+
body = {}
|
|
139
185
|
|
|
140
|
-
|
|
186
|
+
if final_options['select']
|
|
187
|
+
sel = final_options['select']
|
|
188
|
+
body['select'] = sel.is_a?(String) ? sel.split(',').map(&:strip) : Array(sel)
|
|
189
|
+
end
|
|
141
190
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
if final_options['filter']
|
|
192
|
+
filters = final_options['filter']
|
|
193
|
+
body['filters'] = filters.is_a?(Array) ? filters : [filters]
|
|
194
|
+
end
|
|
145
195
|
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
if final_options['group_by']
|
|
197
|
+
gb = final_options['group_by']
|
|
198
|
+
body['group_by'] = gb.is_a?(String) ? gb.split(',').map(&:strip) : Array(gb)
|
|
199
|
+
end
|
|
148
200
|
|
|
149
|
-
|
|
201
|
+
if final_options['having']
|
|
202
|
+
hv = final_options['having']
|
|
203
|
+
body['having'] = hv.is_a?(Array) ? hv : [hv]
|
|
204
|
+
end
|
|
150
205
|
|
|
151
|
-
|
|
206
|
+
if final_options['order']
|
|
207
|
+
ord = final_options['order']
|
|
208
|
+
if ord.is_a?(String)
|
|
209
|
+
body['order_by'] = ord
|
|
210
|
+
body['order_direction'] = final_options['order_direction'] || 'asc'
|
|
211
|
+
else
|
|
212
|
+
body['order_by'] = ord
|
|
213
|
+
end
|
|
214
|
+
end
|
|
152
215
|
|
|
153
|
-
body['
|
|
216
|
+
body['limit'] = final_options['limit'] if final_options['limit']
|
|
217
|
+
body['offset'] = final_options['offset'] if final_options['offset']
|
|
154
218
|
|
|
155
|
-
|
|
219
|
+
has_advanced = body.key?('group_by') ||
|
|
220
|
+
body.key?('having') ||
|
|
221
|
+
body['order_by'].is_a?(Array) ||
|
|
222
|
+
(body['filters'] || []).any? { |f| %w[in not_in between not_between].include?(f[:operator]) }
|
|
156
223
|
|
|
157
|
-
if
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
224
|
+
if has_advanced
|
|
225
|
+
@client.request('POST', "/#{@table_name}/query", nil, body)
|
|
226
|
+
else
|
|
227
|
+
params = {}
|
|
228
|
+
if body['select']
|
|
229
|
+
params['select'] = body['select'].is_a?(Array) ? body['select'].join(',') : body['select']
|
|
230
|
+
end
|
|
231
|
+
if body['filters']
|
|
232
|
+
filter_strs = body['filters'].map do |f|
|
|
233
|
+
if f[:value].is_a?(Array)
|
|
234
|
+
return @client.request('POST', "/#{@table_name}/query", nil, body)
|
|
235
|
+
end
|
|
236
|
+
"#{f[:column]}.#{f[:operator]}.#{f[:value]}"
|
|
163
237
|
end
|
|
238
|
+
params['filter'] = filter_strs.join(',')
|
|
164
239
|
end
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
240
|
+
if body['order_by'].is_a?(String)
|
|
241
|
+
params['order'] = body['order_by']
|
|
242
|
+
params['order_direction'] = body['order_direction'] || 'asc'
|
|
243
|
+
end
|
|
244
|
+
params['limit'] = body['limit'] if body['limit']
|
|
245
|
+
params['offset'] = body['offset'] if body['offset']
|
|
246
|
+
@client.request('GET', "/#{@table_name}", params, nil)
|
|
168
247
|
end
|
|
248
|
+
end
|
|
169
249
|
|
|
170
|
-
|
|
171
|
-
|
|
250
|
+
# Execute the query (alias for get).
|
|
251
|
+
def execute
|
|
252
|
+
get
|
|
253
|
+
end
|
|
172
254
|
|
|
173
|
-
|
|
255
|
+
# Get first record matching query.
|
|
256
|
+
#
|
|
257
|
+
# @return [Hash, nil] First record or nil
|
|
258
|
+
def first
|
|
259
|
+
result = limit(1).get
|
|
260
|
+
data = result['data']
|
|
261
|
+
data && !data.empty? ? data[0] : nil
|
|
174
262
|
end
|
|
175
263
|
|
|
176
|
-
|
|
177
|
-
|
|
264
|
+
# Get exactly one record. Raises if zero or more than one found.
|
|
265
|
+
#
|
|
266
|
+
# @return [Hash] The single matching record
|
|
267
|
+
# @raise [WOWSQLError] If zero or more than one record found
|
|
268
|
+
def single
|
|
269
|
+
result = limit(2).get
|
|
270
|
+
data = result['data'] || []
|
|
271
|
+
raise WOWSQLError.new('No records found') if data.empty?
|
|
272
|
+
raise WOWSQLError.new('Multiple records found, expected exactly one') if data.length > 1
|
|
273
|
+
data[0]
|
|
274
|
+
end
|
|
178
275
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
276
|
+
# Get the total count of records matching the current filters.
|
|
277
|
+
#
|
|
278
|
+
# @return [Integer] Number of matching records
|
|
279
|
+
def count
|
|
280
|
+
saved_select = @options['select']
|
|
281
|
+
saved_group = @options.delete('group_by')
|
|
282
|
+
saved_having = @options.delete('having')
|
|
283
|
+
saved_order = @options.delete('order')
|
|
284
|
+
saved_dir = @options.delete('order_direction')
|
|
285
|
+
|
|
286
|
+
@options['select'] = ['COUNT(*) as count']
|
|
287
|
+
|
|
288
|
+
begin
|
|
289
|
+
result = get
|
|
290
|
+
ensure
|
|
291
|
+
if saved_select
|
|
292
|
+
@options['select'] = saved_select
|
|
293
|
+
else
|
|
294
|
+
@options.delete('select')
|
|
184
295
|
end
|
|
185
|
-
@
|
|
296
|
+
@options['group_by'] = saved_group if saved_group
|
|
297
|
+
@options['having'] = saved_having if saved_having
|
|
298
|
+
@options['order'] = saved_order if saved_order
|
|
299
|
+
@options['order_direction'] = saved_dir if saved_dir
|
|
186
300
|
end
|
|
301
|
+
|
|
302
|
+
data = result['data']
|
|
303
|
+
data && !data.empty? ? data[0]['count'].to_i : 0
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Paginate results with page-based interface.
|
|
307
|
+
#
|
|
308
|
+
# @param page [Integer] Page number (1-indexed)
|
|
309
|
+
# @param per_page [Integer] Records per page
|
|
310
|
+
# @return [Hash] Hash with data, page, per_page, total, total_pages
|
|
311
|
+
def paginate(page: 1, per_page: 20)
|
|
312
|
+
offset_val = ([page, 1].max - 1) * per_page
|
|
313
|
+
result = limit(per_page).offset(offset_val).get
|
|
314
|
+
total = result['total'] || result['count'] || 0
|
|
315
|
+
total_pages = total > 0 ? (total + per_page - 1) / per_page : 0
|
|
316
|
+
{
|
|
317
|
+
'data' => result['data'],
|
|
318
|
+
'page' => page,
|
|
319
|
+
'per_page' => per_page,
|
|
320
|
+
'total' => total,
|
|
321
|
+
'total_pages' => total_pages
|
|
322
|
+
}
|
|
187
323
|
end
|
|
188
324
|
end
|
|
189
325
|
end
|
|
190
|
-
|