wowsql-sdk 3.0.1 → 3.1.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 +4 -4
- data/README.md +557 -325
- data/lib/wowsql/auth.rb +2 -2
- data/lib/wowsql/client.rb +60 -60
- data/lib/wowsql/query_builder.rb +197 -226
- data/lib/wowsql/schema.rb +22 -5
- data/lib/wowsql/storage.rb +2 -2
- data/lib/wowsql/table.rb +123 -110
- metadata +1 -1
data/lib/wowsql/query_builder.rb
CHANGED
|
@@ -1,99 +1,70 @@
|
|
|
1
|
+
require_relative 'exceptions'
|
|
2
|
+
|
|
1
3
|
module WOWSQL
|
|
2
|
-
# Fluent query builder
|
|
4
|
+
# Fluent query builder — all operations translate to PostgREST query parameters.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# result = client.table("users")
|
|
8
|
+
# .select("id", "email", "name")
|
|
9
|
+
# .eq("is_active", true)
|
|
10
|
+
# .order_by("created_at", "desc")
|
|
11
|
+
# .limit(20)
|
|
12
|
+
# .get
|
|
3
13
|
class QueryBuilder
|
|
4
14
|
def initialize(client, table_name)
|
|
5
|
-
@client
|
|
15
|
+
@client = client
|
|
6
16
|
@table_name = table_name
|
|
7
|
-
@
|
|
17
|
+
@filters = []
|
|
18
|
+
@select_cols = nil
|
|
19
|
+
@group_by = []
|
|
20
|
+
@having = []
|
|
21
|
+
@order = []
|
|
22
|
+
@limit_val = nil
|
|
23
|
+
@offset_val = nil
|
|
8
24
|
end
|
|
9
25
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
# @param columns [Array<String>] Column names or expressions
|
|
13
|
-
# @return [QueryBuilder] self for chaining
|
|
26
|
+
# ── Column selection ─────────────────────────────────────────
|
|
27
|
+
|
|
14
28
|
def select(*columns)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else
|
|
18
|
-
@options['select'] = columns.length > 1 ? columns.map(&:to_s) : (columns.first || '*')
|
|
19
|
-
end
|
|
29
|
+
cols = columns.length == 1 && columns[0].is_a?(Array) ? columns[0] : columns.map(&:to_s)
|
|
30
|
+
@select_cols = cols.empty? ? nil : cols
|
|
20
31
|
self
|
|
21
32
|
end
|
|
22
33
|
|
|
23
|
-
#
|
|
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'] ||= []
|
|
34
|
+
# ── Filters ──────────────────────────────────────────────────
|
|
32
35
|
|
|
36
|
+
def filter(column, operator = nil, value = nil, logical_op: 'AND')
|
|
33
37
|
if column.is_a?(Hash)
|
|
34
38
|
if column.key?('column') || column.key?(:column)
|
|
35
39
|
col = column['column'] || column[:column]
|
|
36
|
-
op
|
|
40
|
+
op = column['operator'] || column[:operator]
|
|
37
41
|
val = column['value'] || column[:value]
|
|
38
|
-
lo
|
|
39
|
-
@
|
|
42
|
+
lo = column['logical_op'] || column[:logical_op] || logical_op
|
|
43
|
+
@filters << { column: col.to_s, operator: op.to_s, value: val, logical_op: lo }
|
|
40
44
|
else
|
|
41
45
|
column.each do |col_name, col_value|
|
|
42
|
-
@
|
|
46
|
+
@filters << { column: col_name.to_s, operator: 'eq', value: col_value, logical_op: logical_op }
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
else
|
|
46
50
|
raise ArgumentError, "filter() missing required argument: 'operator'" if operator.nil?
|
|
47
|
-
@
|
|
51
|
+
@filters << { column: column.to_s, operator: operator.to_s, value: value, logical_op: logical_op }
|
|
48
52
|
end
|
|
49
|
-
|
|
50
53
|
self
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
def eq(column, value)
|
|
54
|
-
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def gte(column, value)
|
|
66
|
-
filter(column, 'gte', value)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def lt(column, value)
|
|
70
|
-
filter(column, 'lt', value)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def lte(column, value)
|
|
74
|
-
filter(column, 'lte', value)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def like(column, pattern)
|
|
78
|
-
filter(column, 'like', pattern)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def is_null(column)
|
|
82
|
-
filter(column, 'is', nil)
|
|
83
|
-
end
|
|
84
|
-
|
|
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)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def not_in(column, values)
|
|
95
|
-
filter(column, 'not_in', values)
|
|
96
|
-
end
|
|
56
|
+
def eq(column, value) filter(column, 'eq', value) end
|
|
57
|
+
def neq(column, value) filter(column, 'neq', value) end
|
|
58
|
+
def gt(column, value) filter(column, 'gt', value) end
|
|
59
|
+
def gte(column, value) filter(column, 'gte', value) end
|
|
60
|
+
def lt(column, value) filter(column, 'lt', value) end
|
|
61
|
+
def lte(column, value) filter(column, 'lte', value) end
|
|
62
|
+
def like(column, pattern) filter(column, 'like', pattern) end
|
|
63
|
+
def ilike(column, pattern) filter(column, 'ilike', pattern) end
|
|
64
|
+
def is_null(column) filter(column, 'is', nil) end
|
|
65
|
+
def is_not_null(column) filter(column, 'is_not', nil) end
|
|
66
|
+
def in_list(column, values) filter(column, 'in', values) end
|
|
67
|
+
def not_in(column, values) filter(column, 'not_in', values) end
|
|
97
68
|
|
|
98
69
|
def between(column, min_val, max_val)
|
|
99
70
|
filter(column, 'between', [min_val, max_val])
|
|
@@ -103,223 +74,223 @@ module WOWSQL
|
|
|
103
74
|
filter(column, 'not_between', [min_val, max_val])
|
|
104
75
|
end
|
|
105
76
|
|
|
106
|
-
# Add an OR filter condition.
|
|
107
77
|
def or_filter(column, operator, value)
|
|
108
78
|
filter(column, operator, value, logical_op: 'OR')
|
|
109
79
|
end
|
|
110
80
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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|
|
|
81
|
+
# ── Ordering / grouping ──────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
def order_by(column, direction = 'asc')
|
|
84
|
+
if column.is_a?(Array)
|
|
85
|
+
column.each do |item|
|
|
122
86
|
if item.is_a?(Hash)
|
|
123
|
-
item
|
|
87
|
+
@order << { column: (item[:column] || item['column']).to_s,
|
|
88
|
+
direction: (item[:direction] || item['direction'] || 'asc').to_s }
|
|
124
89
|
elsif item.is_a?(Array) && item.length == 2
|
|
125
|
-
{ column: item[0], direction: item[1] }
|
|
90
|
+
@order << { column: item[0].to_s, direction: item[1].to_s }
|
|
126
91
|
end
|
|
127
|
-
end
|
|
128
|
-
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
@order << { column: column.to_s, direction: (direction || 'asc').to_s }
|
|
129
95
|
end
|
|
130
96
|
self
|
|
131
97
|
end
|
|
132
98
|
|
|
133
|
-
# Order results by a single column (alias for order_by).
|
|
134
99
|
def order(column, direction = 'asc')
|
|
135
100
|
order_by(column, direction)
|
|
136
101
|
end
|
|
137
102
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
@
|
|
103
|
+
def group_by(*columns)
|
|
104
|
+
cols = columns.length == 1 && columns[0].is_a?(Array) ? columns[0] : columns
|
|
105
|
+
@group_by = cols.map(&:to_s)
|
|
141
106
|
self
|
|
142
107
|
end
|
|
143
108
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@options['offset'] = n
|
|
109
|
+
def having(column, operator, value)
|
|
110
|
+
@having << { column: column.to_s, operator: operator.to_s, value: value }
|
|
147
111
|
self
|
|
148
112
|
end
|
|
149
113
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
114
|
+
# ── Pagination ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def limit(n)
|
|
117
|
+
@limit_val = n.to_i
|
|
162
118
|
self
|
|
163
119
|
end
|
|
164
120
|
|
|
165
|
-
|
|
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 }
|
|
121
|
+
def offset(n)
|
|
122
|
+
@offset_val = n.to_i
|
|
174
123
|
self
|
|
175
124
|
end
|
|
176
125
|
|
|
177
|
-
#
|
|
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
|
|
183
|
-
|
|
184
|
-
body = {}
|
|
185
|
-
|
|
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
|
|
126
|
+
# ── Execution ────────────────────────────────────────────────
|
|
190
127
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
128
|
+
# Execute the query using PostgREST native query parameters.
|
|
129
|
+
#
|
|
130
|
+
# @return [Hash] { data: Array, count: Integer, total: Integer, limit: Integer, offset: Integer }
|
|
131
|
+
def get(_additional_options = nil)
|
|
132
|
+
params = {}
|
|
133
|
+
|
|
134
|
+
# SELECT
|
|
135
|
+
sel = @select_cols ? @select_cols.dup : []
|
|
136
|
+
unless @group_by.empty?
|
|
137
|
+
sel = (sel + @group_by).uniq
|
|
194
138
|
end
|
|
139
|
+
params['select'] = sel.join(',') unless sel.empty?
|
|
195
140
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
body['group_by'] = gb.is_a?(String) ? gb.split(',').map(&:strip) : Array(gb)
|
|
199
|
-
end
|
|
141
|
+
# FILTERS → PostgREST native column=op.value
|
|
142
|
+
filters_to_params(@filters).each { |k, v| params[k] = v }
|
|
200
143
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
144
|
+
# ORDER → PostgREST: order=col.asc,col2.desc
|
|
145
|
+
unless @order.empty?
|
|
146
|
+
parts = @order.map { |o| "#{o[:column]}.#{o[:direction] || 'asc'}" }
|
|
147
|
+
params['order'] = parts.join(',')
|
|
204
148
|
end
|
|
205
149
|
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
150
|
+
params['limit'] = @limit_val.to_s if @limit_val
|
|
151
|
+
params['offset'] = @offset_val.to_s if @offset_val
|
|
215
152
|
|
|
216
|
-
|
|
217
|
-
body['offset'] = final_options['offset'] if final_options['offset']
|
|
153
|
+
result = @client.request('GET', "/#{@table_name}", params, nil, 'Prefer' => 'count=exact')
|
|
218
154
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
body['order_by'].is_a?(Array) ||
|
|
222
|
-
(body['filters'] || []).any? { |f| %w[in not_in between not_between].include?(f[:operator]) }
|
|
155
|
+
data = normalise_data(result)
|
|
156
|
+
total = WOWSQLClient.parse_total_from_content_range(@client.last_content_range, data.length)
|
|
223
157
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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]}"
|
|
237
|
-
end
|
|
238
|
-
params['filter'] = filter_strs.join(',')
|
|
239
|
-
end
|
|
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)
|
|
247
|
-
end
|
|
158
|
+
{
|
|
159
|
+
'data' => data,
|
|
160
|
+
'count' => data.length,
|
|
161
|
+
'total' => total,
|
|
162
|
+
'limit' => @limit_val || 100,
|
|
163
|
+
'offset' => @offset_val || 0
|
|
164
|
+
}
|
|
248
165
|
end
|
|
249
166
|
|
|
250
|
-
|
|
251
|
-
def execute
|
|
252
|
-
get
|
|
253
|
-
end
|
|
167
|
+
alias execute get
|
|
254
168
|
|
|
255
|
-
# Get first record matching query.
|
|
256
|
-
#
|
|
257
|
-
# @return [Hash, nil] First record or nil
|
|
258
169
|
def first
|
|
259
170
|
result = limit(1).get
|
|
260
|
-
data
|
|
171
|
+
data = result['data']
|
|
261
172
|
data && !data.empty? ? data[0] : nil
|
|
262
173
|
end
|
|
263
174
|
|
|
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
175
|
def single
|
|
269
176
|
result = limit(2).get
|
|
270
|
-
data
|
|
271
|
-
raise WOWSQLError.new('No records found')
|
|
272
|
-
raise WOWSQLError.new('Multiple records found, expected
|
|
177
|
+
data = result['data'] || []
|
|
178
|
+
raise WOWSQLError.new('No records found') if data.empty?
|
|
179
|
+
raise WOWSQLError.new('Multiple records found, expected one') if data.length > 1
|
|
273
180
|
data[0]
|
|
274
181
|
end
|
|
275
182
|
|
|
276
|
-
# Get the total count of records matching the current filters.
|
|
277
|
-
#
|
|
278
|
-
# @return [Integer] Number of matching records
|
|
279
183
|
def count
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
184
|
+
saved = [@select_cols, @group_by, @having, @order, @limit_val, @offset_val]
|
|
185
|
+
@select_cols = nil
|
|
186
|
+
@group_by = []
|
|
187
|
+
@having = []
|
|
188
|
+
@order = []
|
|
189
|
+
@limit_val = 0
|
|
190
|
+
@offset_val = nil
|
|
285
191
|
|
|
286
|
-
|
|
192
|
+
begin
|
|
193
|
+
params = filters_to_params(@filters)
|
|
194
|
+
params['limit'] = '0'
|
|
195
|
+
@client.request('GET', "/#{@table_name}", params, nil, 'Prefer' => 'count=exact')
|
|
196
|
+
WOWSQLClient.parse_total_from_content_range(@client.last_content_range, 0)
|
|
197
|
+
ensure
|
|
198
|
+
@select_cols, @group_by, @having, @order, @limit_val, @offset_val = saved
|
|
199
|
+
end
|
|
200
|
+
end
|
|
287
201
|
|
|
202
|
+
def sum(column)
|
|
203
|
+
saved_sel, @select_cols = @select_cols, ["sum(#{column})"]
|
|
204
|
+
@limit_val = nil
|
|
205
|
+
@offset_val = nil
|
|
288
206
|
begin
|
|
289
207
|
result = get
|
|
208
|
+
(result.dig('data', 0, 'sum') || 0).to_f
|
|
290
209
|
ensure
|
|
291
|
-
|
|
292
|
-
@options['select'] = saved_select
|
|
293
|
-
else
|
|
294
|
-
@options.delete('select')
|
|
295
|
-
end
|
|
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
|
|
210
|
+
@select_cols = saved_sel
|
|
300
211
|
end
|
|
212
|
+
end
|
|
301
213
|
|
|
302
|
-
|
|
303
|
-
|
|
214
|
+
def avg(column)
|
|
215
|
+
saved_sel, @select_cols = @select_cols, ["avg(#{column})"]
|
|
216
|
+
@limit_val = nil
|
|
217
|
+
@offset_val = nil
|
|
218
|
+
begin
|
|
219
|
+
result = get
|
|
220
|
+
(result.dig('data', 0, 'avg') || 0).to_f
|
|
221
|
+
ensure
|
|
222
|
+
@select_cols = saved_sel
|
|
223
|
+
end
|
|
304
224
|
end
|
|
305
225
|
|
|
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
226
|
def paginate(page: 1, per_page: 20)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
227
|
+
page = [page.to_i, 1].max
|
|
228
|
+
offset_val = (page - 1) * per_page
|
|
229
|
+
result = limit(per_page).offset(offset_val).get
|
|
230
|
+
total = result['total'] || result['count'] || 0
|
|
316
231
|
{
|
|
317
|
-
'data'
|
|
318
|
-
'page'
|
|
319
|
-
'per_page'
|
|
320
|
-
'total'
|
|
321
|
-
'total_pages' =>
|
|
232
|
+
'data' => result['data'],
|
|
233
|
+
'page' => page,
|
|
234
|
+
'per_page' => per_page,
|
|
235
|
+
'total' => total,
|
|
236
|
+
'total_pages' => total > 0 ? (total + per_page - 1) / per_page : 0
|
|
322
237
|
}
|
|
323
238
|
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
# Translate filter list into PostgREST query parameters.
|
|
243
|
+
def filters_to_params(filters)
|
|
244
|
+
params = {}
|
|
245
|
+
filters.each do |f|
|
|
246
|
+
col = f[:column]
|
|
247
|
+
op = f[:operator]
|
|
248
|
+
val = f[:value]
|
|
249
|
+
|
|
250
|
+
case op
|
|
251
|
+
when 'eq' then params[col] = "eq.#{val}"
|
|
252
|
+
when 'neq' then params[col] = "neq.#{val}"
|
|
253
|
+
when 'gt' then params[col] = "gt.#{val}"
|
|
254
|
+
when 'gte' then params[col] = "gte.#{val}"
|
|
255
|
+
when 'lt' then params[col] = "lt.#{val}"
|
|
256
|
+
when 'lte' then params[col] = "lte.#{val}"
|
|
257
|
+
when 'like'
|
|
258
|
+
params[col] = "like.#{val.to_s.gsub('%', '*')}"
|
|
259
|
+
when 'ilike'
|
|
260
|
+
params[col] = "ilike.#{val.to_s.gsub('%', '*')}"
|
|
261
|
+
when 'is'
|
|
262
|
+
params[col] = val.nil? ? 'is.null' : "is.#{val}"
|
|
263
|
+
when 'is_not'
|
|
264
|
+
params[col] = val.nil? ? 'not.is.null' : "not.is.#{val}"
|
|
265
|
+
when 'in'
|
|
266
|
+
list = Array(val).join(',')
|
|
267
|
+
params[col] = "in.(#{list})"
|
|
268
|
+
when 'not_in'
|
|
269
|
+
list = Array(val).join(',')
|
|
270
|
+
params[col] = "not.in.(#{list})"
|
|
271
|
+
when 'between'
|
|
272
|
+
if val.is_a?(Array) && val.length == 2
|
|
273
|
+
params[col] = "gte.#{val[0]}"
|
|
274
|
+
params["#{col}_lte"] = "lte.#{val[1]}"
|
|
275
|
+
end
|
|
276
|
+
when 'not_between'
|
|
277
|
+
if val.is_a?(Array) && val.length == 2
|
|
278
|
+
params["#{col}_lt"] = "lt.#{val[0]}"
|
|
279
|
+
params["#{col}_gt"] = "gt.#{val[1]}"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
params
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def normalise_data(result)
|
|
287
|
+
case result
|
|
288
|
+
when Array then result
|
|
289
|
+
when Hash
|
|
290
|
+
result['data'] || result['rows'] || (result.key?('id') ? [result] : [])
|
|
291
|
+
else
|
|
292
|
+
[]
|
|
293
|
+
end
|
|
294
|
+
end
|
|
324
295
|
end
|
|
325
296
|
end
|
data/lib/wowsql/schema.rb
CHANGED
|
@@ -13,7 +13,7 @@ module WOWSQL
|
|
|
13
13
|
# "wowsql_service_..."
|
|
14
14
|
# )
|
|
15
15
|
# schema.create_table("users", [
|
|
16
|
-
# { "name" => "id", "type" => "
|
|
16
|
+
# { "name" => "id", "type" => "UUID", "auto_increment" => true },
|
|
17
17
|
# { "name" => "email", "type" => "VARCHAR(255)", "unique" => true, "nullable" => false },
|
|
18
18
|
# { "name" => "name", "type" => "VARCHAR(255)" },
|
|
19
19
|
# { "name" => "metadata", "type" => "JSONB", "default" => "'{}'" },
|
|
@@ -24,11 +24,11 @@ module WOWSQL
|
|
|
24
24
|
|
|
25
25
|
# @param project_url [String] Project subdomain or full URL
|
|
26
26
|
# @param service_key [String] Service role key (wowsql_service_...)
|
|
27
|
-
# @param base_domain [String] Base domain (default: "
|
|
27
|
+
# @param base_domain [String] Base domain (default: "wowsqlconnect.com")
|
|
28
28
|
# @param secure [Boolean] Use HTTPS (default: true)
|
|
29
29
|
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
30
30
|
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
31
|
-
def initialize(project_url, service_key, base_domain: '
|
|
31
|
+
def initialize(project_url, service_key, base_domain: 'wowsqlconnect.com', secure: true,
|
|
32
32
|
timeout: 30, verify_ssl: true)
|
|
33
33
|
if project_url.start_with?('http://') || project_url.start_with?('https://')
|
|
34
34
|
base = project_url.chomp('/')
|
|
@@ -64,10 +64,14 @@ module WOWSQL
|
|
|
64
64
|
#
|
|
65
65
|
# @param table_name [String] Name of the table
|
|
66
66
|
# @param columns [Array<Hash>] Column definitions (name, type, auto_increment, unique, nullable, default)
|
|
67
|
-
# @param primary_key [String
|
|
67
|
+
# @param primary_key [String] Primary key column name (must be the UUID column)
|
|
68
68
|
# @param indexes [Array<String>, nil] Columns to create indexes on
|
|
69
69
|
# @return [Hash]
|
|
70
|
-
def create_table(table_name, columns, primary_key
|
|
70
|
+
def create_table(table_name, columns, primary_key:, indexes: nil)
|
|
71
|
+
raise ArgumentError, 'primary_key is required; primary key must be UUID type.' if primary_key.nil? || primary_key.to_s.strip.empty?
|
|
72
|
+
|
|
73
|
+
assert_uuid_primary_key!(primary_key, columns)
|
|
74
|
+
|
|
71
75
|
request('POST', '/api/v2/schema/tables', nil, {
|
|
72
76
|
table_name: table_name,
|
|
73
77
|
columns: columns,
|
|
@@ -76,6 +80,19 @@ module WOWSQL
|
|
|
76
80
|
})
|
|
77
81
|
end
|
|
78
82
|
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def assert_uuid_primary_key!(primary_key, columns)
|
|
86
|
+
col = columns.find { |c| c['name'] == primary_key || c[:name] == primary_key }
|
|
87
|
+
raise ArgumentError, 'Primary key column not found in columns.' unless col
|
|
88
|
+
|
|
89
|
+
type_str = (col['type'] || col[:type]).to_s
|
|
90
|
+
first = type_str.strip.split(/\s+/, 2).first.to_s.upcase
|
|
91
|
+
raise ArgumentError, 'Primary key column must use PostgreSQL type UUID.' unless first == 'UUID'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
public
|
|
95
|
+
|
|
79
96
|
# Alter an existing table.
|
|
80
97
|
#
|
|
81
98
|
# Operations: add_column, drop_column, modify_column, rename_column
|
data/lib/wowsql/storage.rb
CHANGED
|
@@ -98,12 +98,12 @@ module WOWSQL
|
|
|
98
98
|
# @param api_key [String] API key for authentication
|
|
99
99
|
# @param project_slug [String] Explicit slug (used with base_url)
|
|
100
100
|
# @param base_url [String] Explicit base URL (used with project_slug)
|
|
101
|
-
# @param base_domain [String] Base domain (default: "
|
|
101
|
+
# @param base_domain [String] Base domain (default: "wowsqlconnect.com")
|
|
102
102
|
# @param secure [Boolean] Use HTTPS (default: true)
|
|
103
103
|
# @param timeout [Integer] Request timeout in seconds (default: 60)
|
|
104
104
|
# @param verify_ssl [Boolean] Verify SSL certificates (default: true)
|
|
105
105
|
def initialize(project_url = '', api_key = '', project_slug: '', base_url: '',
|
|
106
|
-
base_domain: '
|
|
106
|
+
base_domain: 'wowsqlconnect.com', secure: true, timeout: 60, verify_ssl: true)
|
|
107
107
|
if !project_slug.empty? && !base_url.empty?
|
|
108
108
|
@base_url = base_url.chomp('/')
|
|
109
109
|
@project_slug = project_slug
|