json2sql 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40d7668af9e81534fea68e0745ccb82901d144b1cd3f8851999442262f6e3167
4
- data.tar.gz: d12e2089964c6eecfbd752025f492d56f78bdf72102bb1879d0e57056765ae2a
3
+ metadata.gz: 5aced53d0fa190feb8e8d3fae69da28a27d7bcf4a87ff5eb220a3603b63780ec
4
+ data.tar.gz: 9b6b8e0c12c009823530de7af8394a8303a3edaed31a3f25bd3a21027821277f
5
5
  SHA512:
6
- metadata.gz: 1bdf680bb7009a436cbe5fcdfa70dd6b69036259f61c26b111b72ed3e8d07ed52528b2ffda79d15db4e0a7a6887cfea2e0c99ca504d421ac06c479eb4ac1a2ed
7
- data.tar.gz: 7f5187f7f7d1bc36284d7fb11bef6273dba90e4f7deb78cf286909831bd0e62a6a02e1354e6ed193e2f7c631f04ee72f87f16ff41c2a37af2a2e79b9b6adb4f4
6
+ metadata.gz: 48dfc9b428a3005570e77b2a4085d4b963eeae8740b1887341a8aa1c2eaf9194e33ececdfa603468d301121385c1b76fe93ffcdc66d0440bd112cb89ff72056d
7
+ data.tar.gz: e8ec74d97dc1ebe87127e271db0eb50a27909ad164105217740d21f291c2041d7f8351cc915276f3806920b0666a4d055cea49868b1172a7d2b624748c2efe0f
@@ -1,20 +1,31 @@
1
1
  module Json2sql
2
+
2
3
  # Builds a DELETE FROM statement for a single table.
3
4
  #
4
5
  # Input Hash:
5
6
  # "and" => { ... } – WHERE conditions (required to avoid deleting all rows)
6
7
  # "or" => { ... } – WHERE conditions (OR)
8
+
7
9
  class DeleteModel
10
+
8
11
  def initialize(sql, table, relation)
9
- @sql = sql
10
- @table = table.to_s
12
+
13
+ @sql = sql
14
+
15
+ @table = table.to_s
16
+
11
17
  @relation = relation
12
18
  end
13
19
 
14
20
  def build(params)
21
+
15
22
  @sql << "DELETE FROM "
23
+
16
24
  @sql << Sanitizer.keyword_wrap(@table)
25
+
17
26
  WhereModel.new(@sql, @table, @relation).build(params)
18
27
  end
28
+
19
29
  end
30
+
20
31
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds one or more DELETE statements from a Hash of table → params.
3
4
  #
4
5
  # Usage (single deletion):
@@ -13,24 +14,37 @@ module Json2sql
13
14
  # { "and" => { "user_id" => 2 } }
14
15
  # ]
15
16
  # )
17
+
16
18
  class DeleteRunner
19
+
17
20
  def self.build(input)
18
- input = Json2sql.normalize(input)
19
- sql = +""
21
+
22
+ sql = +""
23
+
24
+ input = Json2sql.normalize(input)
25
+
20
26
  relation = WhereRelation.none("")
21
27
 
22
28
  input.each do |table, value|
29
+
23
30
  tbl = table.to_s
24
31
 
25
32
  case value
33
+
26
34
  when Hash
35
+
27
36
  DeleteModel.new(sql, tbl, relation).build(value)
37
+
28
38
  sql << ";\n"
39
+
29
40
  when Array
41
+
30
42
  value.each do |item|
43
+
31
44
  next unless item.is_a?(Hash)
32
45
 
33
46
  DeleteModel.new(sql, tbl, relation).build(item)
47
+
34
48
  sql << ";\n"
35
49
  end
36
50
  end
@@ -38,5 +52,7 @@ module Json2sql
38
52
 
39
53
  sql
40
54
  end
55
+
41
56
  end
57
+
42
58
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds an INSERT INTO statement for a single table.
3
4
  #
4
5
  # Input Hash:
@@ -7,50 +8,75 @@ module Json2sql
7
8
  # Values:
8
9
  # Integer / Float → inserted as raw numbers
9
10
  # String → wrapped in single quotes with SQL escaping
11
+
10
12
  class InsertModel
13
+
11
14
  def initialize(sql, table)
12
- @sql = sql
15
+
16
+ @sql = sql
17
+
13
18
  @table = table.to_s
14
19
  end
15
20
 
16
21
  def build(params)
22
+
17
23
  @sql << "INSERT INTO "
24
+
18
25
  @sql << Sanitizer.keyword_wrap(@table)
26
+
19
27
  @sql << " ("
28
+
20
29
  build_columns(params)
30
+
21
31
  @sql << ") VALUES ("
32
+
22
33
  build_values(params)
34
+
23
35
  @sql << ")"
24
36
  end
25
37
 
26
38
  private
27
39
 
28
40
  def build_columns(params)
29
- columns = params["columns"]
41
+
42
+ columns = params["columns"]
43
+
30
44
  return unless columns.is_a?(Hash)
31
45
 
32
46
  separator = false
47
+
33
48
  columns.each_key do |key|
49
+
34
50
  @sql << ", " if separator
51
+
35
52
  separator = true
53
+
36
54
  @sql << Sanitizer.keyword_wrap(key.to_s)
37
55
  end
38
56
  end
39
57
 
40
58
  def build_values(params)
59
+
41
60
  columns = params["columns"]
61
+
42
62
  return unless columns.is_a?(Hash)
43
63
 
44
64
  separator = false
65
+
45
66
  columns.each_value do |value|
67
+
46
68
  @sql << ", " if separator
69
+
47
70
  separator = true
48
71
 
49
72
  case value
50
- when Integer, Float then @sql << value.to_s
51
- when String then @sql << Sanitizer.value_wrap(value)
73
+ when Float then @sql << value.to_s
74
+ when Integer then @sql << value.to_s
75
+ when String then @sql << Sanitizer.value_wrap(value)
52
76
  end
53
77
  end
54
78
  end
79
+
55
80
  end
81
+
56
82
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds one or more INSERT statements from a Hash of table → params.
3
4
  #
4
5
  # Usage (single row):
@@ -13,23 +14,35 @@ module Json2sql
13
14
  # { "columns" => { "name" => "rails" } }
14
15
  # ]
15
16
  # )
17
+
16
18
  class InsertRunner
19
+
17
20
  def self.build(input)
21
+
22
+ sql = +""
23
+
18
24
  input = Json2sql.normalize(input)
19
- sql = +""
20
25
 
21
26
  input.each do |table, value|
27
+
22
28
  tbl = table.to_s
23
29
 
24
30
  case value
31
+
25
32
  when Hash
33
+
26
34
  InsertModel.new(sql, tbl).build(value)
35
+
27
36
  sql << ";\n"
37
+
28
38
  when Array
39
+
29
40
  value.each do |item|
41
+
30
42
  next unless item.is_a?(Hash)
31
43
 
32
44
  InsertModel.new(sql, tbl).build(item)
45
+
33
46
  sql << ";\n"
34
47
  end
35
48
  end
@@ -37,5 +50,7 @@ module Json2sql
37
50
 
38
51
  sql
39
52
  end
53
+
40
54
  end
55
+
41
56
  end
@@ -1,28 +1,34 @@
1
1
  module Json2sql
2
+
2
3
  module Sanitizer
4
+
3
5
  # Characters stripped from SQL identifiers (table/column names).
4
6
  KEYWORD_DANGEROUS = /[ `;"'\\]/
5
7
 
6
8
  # Removes dangerous characters from an identifier string.
7
9
  def self.keyword(input)
10
+
8
11
  input.to_s.gsub(KEYWORD_DANGEROUS, "")
9
12
  end
10
13
 
11
14
  # Escapes a value string for safe embedding between SQL quotes.
12
15
  # ' → '' and \ → \\
13
16
  def self.value(input)
17
+
14
18
  input.to_s.gsub("\\", "\\\\\\\\").gsub("'", "''")
15
19
  end
16
20
 
17
21
  # Wraps an identifier in the given quote character (default: backtick).
18
22
  # Dangerous characters inside the identifier are stripped.
19
23
  def self.keyword_wrap(input, wrap = "`")
24
+
20
25
  "#{wrap}#{keyword(input)}#{wrap}"
21
26
  end
22
27
 
23
28
  # Wraps a value in the given quote character (default: single-quote).
24
29
  # Single quotes and backslashes inside the value are escaped.
25
30
  def self.value_wrap(input, wrap = "'")
31
+
26
32
  "#{wrap}#{value(input)}#{wrap}"
27
33
  end
28
34
 
@@ -30,6 +36,7 @@ module Json2sql
30
36
  # backtick-quoted SQL reference (e.g. "`users`.`id`").
31
37
  # Strips the leading "$." and splits on ".".
32
38
  def self.reference(input)
39
+
33
40
  str = input.to_s[2..] # strip leading "$."
34
41
  result = +"`"
35
42
  str.each_char do |c|
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds a SELECT SQL statement for a single table.
3
4
  #
4
5
  # Input Hash keys (all optional):
@@ -12,95 +13,147 @@ module Json2sql
12
13
  # "options" => ["total"] – wrap response with data/total JSON
13
14
  # "children" => { "table" => {...} } – nested child arrays
14
15
  # "parents" => { "table" => {...} } – nested parent objects
16
+
15
17
  class SelectModel
18
+
16
19
  def initialize(sql, table, relation)
17
- @sql = sql
18
- @table = table.to_s
20
+
21
+ @sql = sql
22
+
23
+ @table = table.to_s
24
+
19
25
  @relation = relation
20
26
  end
21
27
 
22
28
  # SELECT COUNT(*) AS `table` FROM `table` WHERE ...
29
+
23
30
  def build_query_count(params)
31
+
24
32
  @sql << "SELECT COUNT(*) AS "
33
+
25
34
  @sql << Sanitizer.keyword_wrap(@table)
35
+
26
36
  @sql << " FROM "
37
+
27
38
  @sql << Sanitizer.keyword_wrap(@table)
39
+
28
40
  WhereModel.new(@sql, @table, @relation).build(params)
29
41
  end
30
42
 
31
43
  # Plain SELECT col1, col2 FROM `table` WHERE ... ORDER BY ... LIMIT ... OFFSET ...
44
+
32
45
  def build_query_default(params)
46
+
33
47
  @sep = false
48
+
34
49
  @sql << "SELECT "
50
+
35
51
  build_columns_default(params)
52
+
36
53
  @sql << " FROM "
54
+
37
55
  @sql << Sanitizer.keyword_wrap(@table)
56
+
38
57
  WhereModel.new(@sql, @table, @relation).build(params)
39
- build_order(params)
58
+
59
+ build_order(params)
40
60
  build_limit(params)
41
61
  build_offset(params)
42
62
  end
43
63
 
44
64
  # SELECT JSON_ARRAYAGG(JSON_OBJECT(...)) AS `table`
45
65
  # FROM LATERAL (SELECT * FROM `table` WHERE ... ORDER ... LIMIT ...) AS `table`
66
+
46
67
  def build_query_array(params)
68
+
47
69
  @sep = false
70
+
48
71
  @sql << "SELECT JSON_ARRAYAGG(JSON_OBJECT("
72
+
49
73
  build_columns_json(params)
50
74
  build_columns_array(params)
51
75
  build_columns_object(params)
76
+
52
77
  @sql << ")) AS "
78
+
53
79
  @sql << Sanitizer.keyword_wrap(@table)
80
+
54
81
  @sql << " FROM LATERAL (SELECT * FROM "
82
+
55
83
  @sql << Sanitizer.keyword_wrap(@table)
84
+
56
85
  WhereModel.new(@sql, @table, @relation).build(params)
86
+
57
87
  build_order(params)
58
88
  build_limit(params)
59
89
  build_offset(params)
90
+
60
91
  @sql << ") AS "
92
+
61
93
  @sql << Sanitizer.keyword_wrap(@table)
62
94
  end
63
95
 
64
96
  # SELECT JSON_OBJECT(...) AS `table`
65
97
  # FROM LATERAL (SELECT * FROM `table` WHERE ...) AS `table`
98
+
66
99
  def build_query_object(params)
100
+
67
101
  @sep = false
102
+
68
103
  @sql << "SELECT JSON_OBJECT("
104
+
69
105
  build_columns_json(params)
70
106
  build_columns_array(params)
71
107
  build_columns_object(params)
108
+
72
109
  @sql << ") AS "
110
+
73
111
  @sql << Sanitizer.keyword_wrap(@table)
112
+
74
113
  @sql << " FROM LATERAL (SELECT * FROM "
114
+
75
115
  @sql << Sanitizer.keyword_wrap(@table)
116
+
76
117
  WhereModel.new(@sql, @table, @relation).build(params)
118
+
77
119
  build_order(params)
78
120
  build_limit(params)
79
121
  build_offset(params)
122
+
80
123
  @sql << ") AS "
124
+
81
125
  @sql << Sanitizer.keyword_wrap(@table)
82
126
  end
83
127
 
84
128
  # Smart dispatcher:
85
129
  # - no options → build_query_array
86
130
  # - options includes "total" → wraps with JSON_OBJECT('data', ..., 'total', COUNT(*))
131
+
87
132
  def build_query_options(params)
133
+
88
134
  options = params["options"]
89
135
 
90
136
  unless options.is_a?(Array) && !options.empty?
137
+
91
138
  build_query_array(params)
139
+
92
140
  return
93
141
  end
94
142
 
95
143
  total = options.include?("total")
96
144
 
97
145
  @sql << "SELECT JSON_OBJECT('data', ("
146
+
98
147
  build_query_array(params)
148
+
99
149
  @sql << ")"
100
150
 
101
151
  if total
152
+
102
153
  @sql << ", 'total', ("
154
+
103
155
  build_query_count(params)
156
+
104
157
  @sql << ")"
105
158
  end
106
159
 
@@ -113,93 +166,137 @@ module Json2sql
113
166
  # comma placement across multiple calls within a single query build.
114
167
 
115
168
  # Appends plain column references: `table`.`col`, `table`.`col2`, ...
169
+
116
170
  def build_columns_default(params)
171
+
117
172
  columns = params["columns"]
173
+
118
174
  return unless columns.is_a?(Array)
119
175
 
120
176
  columns.each do |column|
177
+
121
178
  next unless column.is_a?(String) || column.is_a?(Symbol)
122
179
 
123
180
  @sql << ", " if @sep
181
+
124
182
  @sep = true
183
+
125
184
  @sql << Sanitizer.keyword_wrap(@table) << "."
185
+
126
186
  @sql << Sanitizer.keyword_wrap(column.to_s)
127
187
  end
128
188
  end
129
189
 
130
190
  # Appends JSON key-value pairs for columns: 'col', `table`.`col`, ...
191
+
131
192
  def build_columns_json(params)
193
+
132
194
  columns = params["columns"]
195
+
133
196
  return unless columns.is_a?(Array)
134
197
 
135
198
  columns.each do |column|
199
+
136
200
  next unless column.is_a?(String) || column.is_a?(Symbol)
137
201
 
138
202
  @sql << ", " if @sep
203
+
139
204
  @sep = true
205
+
140
206
  col = column.to_s
207
+
141
208
  @sql << Sanitizer.keyword_wrap(col, "'")
209
+
142
210
  @sql << ", "
211
+
143
212
  @sql << Sanitizer.keyword_wrap(@table) << "."
213
+
144
214
  @sql << Sanitizer.keyword_wrap(col)
145
215
  end
146
216
  end
147
217
 
148
218
  # Appends nested child arrays (subquery → JSON_ARRAYAGG).
149
219
  # Uses WhereRelation::PARENT because child table references parent.
220
+
150
221
  def build_columns_array(params)
222
+
151
223
  children = params["children"]
224
+
152
225
  return unless children.is_a?(Hash)
153
226
 
154
227
  relation = WhereRelation.parent(@table)
155
228
 
156
229
  children.each do |key, value|
230
+
157
231
  next unless value.is_a?(Hash)
158
232
 
159
233
  @sql << ", " if @sep
234
+
160
235
  @sep = true
236
+
161
237
  tbl = key.to_s
238
+
162
239
  @sql << Sanitizer.keyword_wrap(tbl, "'")
240
+
163
241
  @sql << ", ("
242
+
164
243
  SelectModel.new(@sql, tbl, relation).build_query_options(value)
244
+
165
245
  @sql << ")"
166
246
  end
167
247
  end
168
248
 
169
249
  # Appends nested parent objects (subquery → JSON_OBJECT, single row).
170
250
  # Uses WhereRelation::CHILD because parent table is referenced from child.
251
+
171
252
  def build_columns_object(params)
253
+
172
254
  parents = params["parents"]
255
+
173
256
  return unless parents.is_a?(Hash)
174
257
 
175
258
  relation = WhereRelation.child(@table)
176
259
 
177
260
  parents.each do |key, value|
261
+
178
262
  next unless value.is_a?(Hash)
179
263
 
180
264
  @sql << ", " if @sep
265
+
181
266
  @sep = true
267
+
182
268
  tbl = key.to_s
269
+
183
270
  @sql << Sanitizer.keyword_wrap(tbl, "'")
271
+
184
272
  @sql << ", ("
273
+
185
274
  SelectModel.new(@sql, tbl, relation).build_query_object(value)
275
+
186
276
  @sql << ")"
187
277
  end
188
278
  end
189
279
 
190
280
  def build_order(params)
281
+
191
282
  order = params["order"]
283
+
192
284
  return unless order.is_a?(Hash) && !order.empty?
193
285
 
194
286
  @sql << " ORDER BY "
287
+
195
288
  glue = false
196
289
 
197
290
  order.each do |key, value|
291
+
198
292
  @sql << ", " if glue
293
+
199
294
  glue = true
200
295
 
201
296
  column = key.to_s
297
+
202
298
  @sql << Sanitizer.keyword_wrap(@table) << "."
299
+
203
300
  @sql << Sanitizer.keyword_wrap(column)
204
301
 
205
302
  case value.to_s.downcase
@@ -210,13 +307,19 @@ module Json2sql
210
307
  end
211
308
 
212
309
  def build_limit(params)
310
+
213
311
  limit = params["limit"]
312
+
214
313
  @sql << " LIMIT #{limit}" if limit.is_a?(Integer)
215
314
  end
216
315
 
217
316
  def build_offset(params)
317
+
218
318
  offset = params["offset"]
319
+
219
320
  @sql << " OFFSET #{offset}" if offset.is_a?(Integer)
220
321
  end
322
+
221
323
  end
324
+
222
325
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds a top-level SELECT statement from a Hash of table → params.
3
4
  #
4
5
  # Usage:
@@ -16,29 +17,43 @@ module Json2sql
16
17
  # Output wraps every table in JSON_OBJECT so the client receives a single
17
18
  # JSON document:
18
19
  # SELECT JSON_OBJECT('users', (...));
20
+
19
21
  class SelectRunner
22
+
20
23
  def self.build(input)
21
- input = Json2sql.normalize(input)
22
- sql = +""
24
+
25
+ sql = +""
26
+
23
27
  separator = false
24
- relation = WhereRelation.none("")
28
+
29
+ input = Json2sql.normalize(input)
30
+
31
+ relation = WhereRelation.none("")
25
32
 
26
33
  sql << "SELECT JSON_OBJECT("
27
34
 
28
35
  input.each do |table, value|
36
+
29
37
  next unless value.is_a?(Hash)
30
38
 
31
39
  sql << ", " if separator
40
+
32
41
  separator = true
33
42
 
34
43
  sql << Sanitizer.keyword_wrap(table.to_s, "'")
44
+
35
45
  sql << ", ("
46
+
36
47
  SelectModel.new(sql, table.to_s, relation).build_query_options(value)
48
+
37
49
  sql << ")"
38
50
  end
39
51
 
40
52
  sql << ");\n"
53
+
41
54
  sql
42
55
  end
56
+
43
57
  end
58
+
44
59
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds an UPDATE statement for a single table.
3
4
  #
4
5
  # Input Hash:
@@ -7,42 +8,63 @@ module Json2sql
7
8
  # "or" => { ... } – WHERE conditions (OR)
8
9
  #
9
10
  # Value types follow the same rules as InsertModel.
11
+
10
12
  class UpdateModel
13
+
11
14
  def initialize(sql, table, relation)
12
- @sql = sql
13
- @table = table.to_s
15
+
16
+ @sql = sql
17
+
18
+ @table = table.to_s
19
+
14
20
  @relation = relation
15
21
  end
16
22
 
17
23
  def build(params)
24
+
18
25
  @sql << "UPDATE "
26
+
19
27
  @sql << Sanitizer.keyword_wrap(@table)
28
+
20
29
  @sql << " SET "
30
+
21
31
  build_columns(params)
32
+
22
33
  WhereModel.new(@sql, @table, @relation).build(params)
23
34
  end
24
35
 
25
36
  private
26
37
 
27
38
  def build_columns(params)
39
+
28
40
  columns = params["columns"]
41
+
29
42
  return unless columns.is_a?(Hash)
30
43
 
31
44
  separator = false
45
+
32
46
  columns.each do |key, value|
47
+
33
48
  @sql << ", " if separator
49
+
34
50
  separator = true
35
51
 
36
52
  column = key.to_s
53
+
37
54
  @sql << Sanitizer.keyword_wrap(@table) << "."
55
+
38
56
  @sql << Sanitizer.keyword_wrap(column)
57
+
39
58
  @sql << " = "
40
59
 
41
60
  case value
42
- when Integer, Float then @sql << value.to_s
43
- when String then @sql << Sanitizer.value_wrap(value)
61
+ when Float then @sql << value.to_s
62
+ when Integer then @sql << value.to_s
63
+ when String then @sql << Sanitizer.value_wrap(value)
44
64
  end
45
65
  end
46
66
  end
67
+
47
68
  end
69
+
48
70
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds one or more UPDATE statements from a Hash of table → params.
3
4
  #
4
5
  # Usage (single row):
@@ -16,24 +17,37 @@ module Json2sql
16
17
  # { "columns" => { "value" => "en" }, "and" => { "key" => "lang" } }
17
18
  # ]
18
19
  # )
20
+
19
21
  class UpdateRunner
22
+
20
23
  def self.build(input)
21
- input = Json2sql.normalize(input)
22
- sql = +""
24
+
25
+ sql = +""
26
+
27
+ input = Json2sql.normalize(input)
28
+
23
29
  relation = WhereRelation.none("")
24
30
 
25
31
  input.each do |table, value|
32
+
26
33
  tbl = table.to_s
27
34
 
28
35
  case value
36
+
29
37
  when Hash
38
+
30
39
  UpdateModel.new(sql, tbl, relation).build(value)
40
+
31
41
  sql << ";\n"
42
+
32
43
  when Array
44
+
33
45
  value.each do |item|
46
+
34
47
  next unless item.is_a?(Hash)
35
48
 
36
49
  UpdateModel.new(sql, tbl, relation).build(item)
50
+
37
51
  sql << ";\n"
38
52
  end
39
53
  end
@@ -41,5 +55,7 @@ module Json2sql
41
55
 
42
56
  sql
43
57
  end
58
+
44
59
  end
60
+
45
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Json2sql
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
@@ -1,4 +1,5 @@
1
1
  module Json2sql
2
+
2
3
  # Builds a SQL WHERE clause from a Hash describing the conditions.
3
4
  #
4
5
  # Input structure mirrors the JSON format used in the C++ backend:
@@ -18,35 +19,46 @@ module Json2sql
18
19
  # Supported operators: = < > <= >= != <>
19
20
  # in !in like !like
20
21
  # String pseudo-actions: contains (LIKE %v%), first (LIKE v%), last (LIKE %v)
22
+
21
23
  class WhereModel
24
+
22
25
  def initialize(sql, table, relation)
23
- @sql = sql
24
- @table = table.to_s
26
+
27
+ @sql = sql
28
+
29
+ @table = table.to_s
30
+
25
31
  @relation = relation
26
32
  end
27
33
 
28
34
  def build(params)
35
+
29
36
  has_relation = @relation.kind != WhereRelation::NONE
37
+
30
38
  has_where_and = params["and"].is_a?(Hash)
39
+
31
40
  has_where_or = params["or"].is_a?(Hash)
32
41
 
33
42
  return unless has_relation || has_where_and || has_where_or
34
43
 
35
- @sql << " WHERE "
44
+ parts = []
36
45
 
37
- if has_relation
38
- @relation.build_table_relation(@sql, @table)
39
- @sql << " AND " if has_where_and || has_where_or
40
- end
46
+ parts << with_buffer { @relation.build_table_relation(@sql, @table) } if has_relation
41
47
 
42
- if has_where_and
43
- build_column_group(params["and"], " AND ")
44
- return
45
- end
48
+ scope = has_where_and ? " AND " : " OR "
49
+
50
+ group = params["and"] || params["or"]
46
51
 
47
- if has_where_or
48
- build_column_group(params["or"], " OR ")
52
+ if group
53
+
54
+ frag = build_column_group(group, scope)
55
+
56
+ parts << frag unless frag.empty?
49
57
  end
58
+
59
+ return if parts.empty?
60
+
61
+ @sql << " WHERE " << parts.join(" AND ")
50
62
  end
51
63
 
52
64
  private
@@ -56,36 +68,58 @@ module Json2sql
56
68
  # -------------------------------------------------------------------------
57
69
 
58
70
  def build_column_group(params, scope)
59
- @sql << "("
60
- glue = false
61
71
 
62
- params.each do |key, value|
63
- @sql << scope if glue
64
- glue = true
72
+ fragments = params.filter_map do |key, value|
65
73
 
66
- build_column_types(value, scope, key.to_s)
74
+ frag = with_buffer { build_column_types(value, scope, key.to_s) }
75
+
76
+ frag.empty? ? nil : frag
67
77
  end
68
78
 
69
- @sql << ")"
79
+ return "" if fragments.empty?
80
+
81
+ "(" + fragments.join(scope) + ")"
70
82
  end
71
83
 
72
84
  # Dispatch by Ruby type of the value.
85
+
73
86
  def build_column_types(params, scope, column)
87
+
74
88
  case params
89
+
75
90
  when TrueClass, FalseClass
91
+
76
92
  build_action_types(params, column, "=")
93
+
77
94
  when Integer
95
+
78
96
  build_action_types(params, column, "=")
97
+
79
98
  when String
99
+
80
100
  build_action_types(params, column, "contains")
101
+
81
102
  when Hash
103
+
82
104
  if column == "and"
83
- build_column_group(params, " AND ")
84
- elsif column == "or"
85
- build_column_group(params, " OR ")
86
- else
87
- build_action_group(params, scope, column)
105
+
106
+ frag = build_column_group(params, " AND ")
107
+
108
+ @sql << frag unless frag.empty?
109
+
110
+ return
88
111
  end
112
+
113
+ if column == "or"
114
+
115
+ frag = build_column_group(params, " OR ")
116
+
117
+ @sql << frag unless frag.empty?
118
+
119
+ return
120
+ end
121
+
122
+ build_action_group(params, scope, column)
89
123
  end
90
124
  end
91
125
 
@@ -94,24 +128,30 @@ module Json2sql
94
128
  # -------------------------------------------------------------------------
95
129
 
96
130
  def build_action_group(params, scope, column)
97
- glue = false
98
131
 
99
- params.each do |key, value|
100
- @sql << scope if glue
101
- glue = true
132
+ fragments = params.filter_map do |key, value|
102
133
 
103
- build_action_types(value, column, key.to_s)
134
+ frag = with_buffer { build_action_types(value, column, key.to_s) }
135
+
136
+ frag.empty? ? nil : frag
104
137
  end
138
+
139
+ @sql << fragments.join(scope) unless fragments.empty?
105
140
  end
106
141
 
107
142
  def build_action_types(params, column, action)
143
+
108
144
  if action == "and"
145
+
109
146
  build_column_types(params, " AND ", column)
147
+
110
148
  return
111
149
  end
112
150
 
113
151
  if action == "or"
152
+
114
153
  build_column_types(params, " OR ", column)
154
+
115
155
  return
116
156
  end
117
157
 
@@ -123,81 +163,123 @@ module Json2sql
123
163
  # -------------------------------------------------------------------------
124
164
 
125
165
  def build_action_values(params, column, action) # rubocop:disable Metrics/MethodLength
166
+
126
167
  case params
168
+
127
169
  when TrueClass, FalseClass
170
+
128
171
  # Only "null" → IS NULL / IS NOT NULL. Boolean equality is not emitted
129
172
  # (matches C++ behaviour — use integer 1/0 for boolean equality).
130
- if action == "null"
131
- action_str = params ? " IS " : " IS NOT "
132
- @sql << Sanitizer.keyword_wrap(@table) << "."
133
- @sql << Sanitizer.keyword_wrap(column)
134
- @sql << action_str << "NULL"
135
- end
173
+ return unless action == "null"
174
+
175
+ action_str = params ? " IS " : " IS NOT "
176
+
177
+ @sql << Sanitizer.keyword_wrap(@table) << "."
178
+
179
+ @sql << Sanitizer.keyword_wrap(column)
180
+
181
+ @sql << action_str << "NULL"
136
182
 
137
183
  when Integer
184
+
138
185
  action_name = get_action(action)
186
+
139
187
  @sql << Sanitizer.keyword_wrap(@table) << "."
188
+
140
189
  @sql << Sanitizer.keyword_wrap(column)
190
+
141
191
  @sql << " #{action_name} #{params}"
142
192
 
143
193
  when Float
194
+
144
195
  action_name = get_action(action)
196
+
145
197
  @sql << Sanitizer.keyword_wrap(@table) << "."
198
+
146
199
  @sql << Sanitizer.keyword_wrap(column)
200
+
147
201
  @sql << " #{action_name} #{params}"
148
202
 
149
203
  when String
204
+
150
205
  build_action_string(params, column, action)
151
206
 
152
207
  when Array
208
+
153
209
  action_name = get_action(action)
210
+
154
211
  @sql << Sanitizer.keyword_wrap(@table) << "."
212
+
155
213
  @sql << Sanitizer.keyword_wrap(column)
214
+
156
215
  @sql << " #{action_name} ("
216
+
157
217
  build_array(params)
218
+
158
219
  @sql << ")"
159
220
 
160
221
  when Hash
222
+
161
223
  action_name = get_action(action)
224
+
162
225
  @sql << Sanitizer.keyword_wrap(@table) << "."
226
+
163
227
  @sql << Sanitizer.keyword_wrap(column)
228
+
164
229
  @sql << " #{action_name} ("
230
+
165
231
  build_object(params)
232
+
166
233
  @sql << ")"
167
234
  end
168
235
  end
169
236
 
170
237
  def build_action_string(params, column, action)
238
+
171
239
  action_name = get_action(action)
172
240
 
173
241
  case action_name
242
+
174
243
  when "last"
244
+
175
245
  @sql << Sanitizer.keyword_wrap(@table) << "."
246
+
176
247
  @sql << Sanitizer.keyword_wrap(column)
248
+
177
249
  @sql << " LIKE '%" << Sanitizer.value(params) << "'"
178
250
 
179
251
  when "first"
252
+
180
253
  @sql << Sanitizer.keyword_wrap(@table) << "."
254
+
181
255
  @sql << Sanitizer.keyword_wrap(column)
256
+
182
257
  @sql << " LIKE '" << Sanitizer.value(params) << "%'"
183
258
 
184
259
  when "contains"
260
+
185
261
  @sql << Sanitizer.keyword_wrap(@table) << "."
262
+
186
263
  @sql << Sanitizer.keyword_wrap(column)
264
+
187
265
  @sql << " LIKE '%" << Sanitizer.value(params) << "%'"
188
266
 
189
267
  else
268
+
269
+ @sql << Sanitizer.keyword_wrap(@table) << "."
270
+
271
+ @sql << Sanitizer.keyword_wrap(column)
272
+
273
+ @sql << " #{action_name} "
274
+
190
275
  if params.start_with?("$.")
191
- @sql << Sanitizer.keyword_wrap(@table) << "."
192
- @sql << Sanitizer.keyword_wrap(column)
193
- @sql << " #{action_name} "
276
+
194
277
  @sql << Sanitizer.reference(params)
195
- else
196
- @sql << Sanitizer.keyword_wrap(@table) << "."
197
- @sql << Sanitizer.keyword_wrap(column)
198
- @sql << " #{action_name} "
199
- @sql << Sanitizer.value_wrap(params)
278
+
279
+ return
200
280
  end
281
+
282
+ @sql << Sanitizer.value_wrap(params)
201
283
  end
202
284
  end
203
285
 
@@ -206,49 +288,87 @@ module Json2sql
206
288
  # -------------------------------------------------------------------------
207
289
 
208
290
  def build_array(array)
291
+
209
292
  if array.empty?
293
+
210
294
  @sql << "NULL"
295
+
211
296
  return
212
297
  end
213
298
 
214
299
  glue = false
300
+
215
301
  array.each do |item|
302
+
216
303
  @sql << ", " if glue
304
+
217
305
  glue = true
218
306
 
219
307
  case item
220
- when Integer, Float then @sql << item.to_s
221
- when String then @sql << Sanitizer.value_wrap(item)
308
+ when Float then @sql << item.to_s
309
+ when Integer then @sql << item.to_s
310
+ when String then @sql << Sanitizer.value_wrap(item)
222
311
  end
223
312
  end
224
313
  end
225
314
 
226
315
  # Builds a UNION of sub-SELECTs (used when action value is a Hash of tables).
316
+
227
317
  def build_object(object)
318
+
228
319
  if object.empty?
320
+
229
321
  @sql << "NULL"
322
+
230
323
  return
231
324
  end
232
325
 
233
326
  glue = false
327
+
234
328
  relation = WhereRelation.none(@table)
235
329
 
236
330
  object.each do |key, value|
331
+
237
332
  @sql << " UNION " if glue
333
+
238
334
  glue = true
239
335
 
240
336
  tbl = key.to_s
337
+
241
338
  @sql << "("
339
+
242
340
  SelectModel.new(@sql, tbl, relation).build_query_default(value)
341
+
243
342
  @sql << ")"
244
343
  end
245
344
  end
246
345
 
346
+ # -------------------------------------------------------------------------
347
+ # Buffer helper — temporarily swap @sql for a fresh string so that any
348
+ # downstream << calls are captured in isolation. Returns the captured fragment.
349
+ # -------------------------------------------------------------------------
350
+
351
+ def with_buffer
352
+
353
+ saved = @sql
354
+
355
+ @sql = +""
356
+
357
+ yield
358
+
359
+ @sql
360
+
361
+ ensure
362
+
363
+ @sql = saved
364
+ end
365
+
247
366
  # -------------------------------------------------------------------------
248
367
  # Operator mapping
249
368
  # -------------------------------------------------------------------------
250
369
 
251
370
  def get_action(action)
371
+
252
372
  case action
253
373
  when "=", "<", ">", "<=", ">=", "!=", "<>" then action
254
374
  when "in" then "IN"
@@ -258,5 +378,7 @@ module Json2sql
258
378
  else action
259
379
  end
260
380
  end
381
+
261
382
  end
383
+
262
384
  end
@@ -1,54 +1,79 @@
1
1
  module Json2sql
2
+
2
3
  class WhereRelation
3
- NONE = :none
4
- CHILD = :child
4
+
5
+ NONE = :none
6
+ CHILD = :child
5
7
  PARENT = :parent
6
8
 
7
9
  attr_reader :table, :kind
8
10
 
9
11
  def initialize(table, kind)
12
+
10
13
  @table = table.to_s
14
+
11
15
  @kind = kind
12
16
  end
13
17
 
14
18
  # Factory: no relationship (top-level query).
19
+
15
20
  def self.none(table)
21
+
16
22
  new(table, NONE)
17
23
  end
18
24
 
19
25
  # Factory: foreign key is on the child table pointing to the parent.
20
26
  # Produces: `parent`.`child_id` = `current`.`id`
27
+
21
28
  def self.child(table)
29
+
22
30
  new(table, CHILD)
23
31
  end
24
32
 
25
33
  # Factory: foreign key is on the current/parent table pointing to the child.
26
34
  # Produces: `current`.`parent_id` = `parent`.`id`
35
+
27
36
  def self.parent(table)
37
+
28
38
  new(table, PARENT)
29
39
  end
30
40
 
31
41
  # Appends the JOIN condition for this relationship into sql.
32
42
  # +current+ is the name of the table being queried.
43
+
33
44
  def build_table_relation(sql, current)
45
+
34
46
  current = current.to_s
35
47
 
36
48
  if kind == CHILD
49
+
37
50
  sql << Sanitizer.keyword_wrap(table)
51
+
38
52
  sql << "."
53
+
39
54
  sql << build_table_id(current)
55
+
40
56
  sql << " = "
57
+
41
58
  sql << Sanitizer.keyword_wrap(current)
59
+
42
60
  sql << ".`id`"
61
+
43
62
  return
44
63
  end
45
64
 
46
65
  if kind == PARENT
66
+
47
67
  sql << Sanitizer.keyword_wrap(current)
68
+
48
69
  sql << "."
70
+
49
71
  sql << build_table_id(table)
72
+
50
73
  sql << " = "
74
+
51
75
  sql << Sanitizer.keyword_wrap(table)
76
+
52
77
  sql << ".`id`"
53
78
  end
54
79
  end
@@ -58,19 +83,18 @@ module Json2sql
58
83
  # "users" → "`user_id`"
59
84
  # "categories" → "`category_id`"
60
85
  # "admins" → "`admin_id`"
86
+
61
87
  def build_table_id(tbl)
62
- tbl = tbl.to_s
63
- base = Sanitizer.keyword(tbl)
64
-
65
- name = if base.end_with?("ies")
66
- base[0..-4] + "y"
67
- elsif base.end_with?("s")
68
- base[0..-2]
69
- else
70
- base
71
- end
72
-
73
- "`#{name}_id`"
88
+
89
+ base = Sanitizer.keyword(tbl.to_s)
90
+
91
+ return "`#{base[0..-4]}y_id`" if base.end_with?("ies")
92
+
93
+ return "`#{base[0..-2]}_id`" if base.end_with?("s")
94
+
95
+ "`#{base}_id`"
74
96
  end
97
+
75
98
  end
99
+
76
100
  end
data/lib/json2sql.rb CHANGED
@@ -22,17 +22,19 @@ require_relative "json2sql/delete_runner"
22
22
  # Json2sql::InsertRunner.build(hash) → String
23
23
  # Json2sql::UpdateRunner.build(hash) → String
24
24
  # Json2sql::DeleteRunner.build(hash) → String
25
+
25
26
  module Json2sql
27
+
26
28
  # Deep-converts all Hash keys to Strings and recurses into nested Hashes
27
29
  # and Arrays. Leaves all other values (Integers, Strings, etc.) unchanged.
30
+
28
31
  def self.normalize(obj)
29
- case obj
30
- when Hash
31
- obj.each_with_object({}) { |(k, v), h| h[k.to_s] = normalize(v) }
32
- when Array
33
- obj.map { |v| normalize(v) }
34
- else
35
- obj
36
- end
32
+
33
+ return obj.each_with_object({}) { |(k, v), h| h[k.to_s] = normalize(v) } if obj.is_a?(Hash)
34
+
35
+ return obj.map { |v| normalize(v) } if obj.is_a?(Array)
36
+
37
+ obj
37
38
  end
39
+
38
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json2sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago da Silva
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-13 00:00:00.000000000 Z
11
+ date: 2026-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest