json2sql 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 40d7668af9e81534fea68e0745ccb82901d144b1cd3f8851999442262f6e3167
4
- data.tar.gz: d12e2089964c6eecfbd752025f492d56f78bdf72102bb1879d0e57056765ae2a
3
+ metadata.gz: 3caea0aaaaac6b6bdc5fe29341cf711d7ce2c34a0752fd06bfd246eacfc4c1df
4
+ data.tar.gz: 49341acb1ed445eaf48ed6dd0cee2fff492105f3f823d7e4d415671acf4208d0
5
5
  SHA512:
6
- metadata.gz: 1bdf680bb7009a436cbe5fcdfa70dd6b69036259f61c26b111b72ed3e8d07ed52528b2ffda79d15db4e0a7a6887cfea2e0c99ca504d421ac06c479eb4ac1a2ed
7
- data.tar.gz: 7f5187f7f7d1bc36284d7fb11bef6273dba90e4f7deb78cf286909831bd0e62a6a02e1354e6ed193e2f7c631f04ee72f87f16ff41c2a37af2a2e79b9b6adb4f4
6
+ metadata.gz: '09d2615939602fe80edcec47e521ddf58678aab8c048336795129fdda56061d6a38d109bfc0a4321cfc05298c83df0884a79d097b4e5655e4b544b0f0a4d343b'
7
+ data.tar.gz: c46959b88dd70ea6f8981ab7ae6484281e77ec75d82dc06a197af7f9cdefdd5566522a1e050e5a910ab0f7463fbd5fdc12ef5705d1a08358d571596b98f7ebf5
@@ -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.1"
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,16 +19,24 @@ 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
@@ -35,16 +44,21 @@ module Json2sql
35
44
  @sql << " WHERE "
36
45
 
37
46
  if has_relation
47
+
38
48
  @relation.build_table_relation(@sql, @table)
49
+
39
50
  @sql << " AND " if has_where_and || has_where_or
40
51
  end
41
52
 
42
53
  if has_where_and
54
+
43
55
  build_column_group(params["and"], " AND ")
56
+
44
57
  return
45
58
  end
46
59
 
47
60
  if has_where_or
61
+
48
62
  build_column_group(params["or"], " OR ")
49
63
  end
50
64
  end
@@ -56,11 +70,15 @@ module Json2sql
56
70
  # -------------------------------------------------------------------------
57
71
 
58
72
  def build_column_group(params, scope)
73
+
59
74
  @sql << "("
75
+
60
76
  glue = false
61
77
 
62
78
  params.each do |key, value|
79
+
63
80
  @sql << scope if glue
81
+
64
82
  glue = true
65
83
 
66
84
  build_column_types(value, scope, key.to_s)
@@ -70,20 +88,33 @@ module Json2sql
70
88
  end
71
89
 
72
90
  # Dispatch by Ruby type of the value.
91
+
73
92
  def build_column_types(params, scope, column)
93
+
74
94
  case params
75
95
  when TrueClass, FalseClass
96
+
76
97
  build_action_types(params, column, "=")
98
+
77
99
  when Integer
100
+
78
101
  build_action_types(params, column, "=")
102
+
79
103
  when String
104
+
80
105
  build_action_types(params, column, "contains")
106
+
81
107
  when Hash
108
+
82
109
  if column == "and"
110
+
83
111
  build_column_group(params, " AND ")
112
+
84
113
  elsif column == "or"
114
+
85
115
  build_column_group(params, " OR ")
86
- else
116
+
117
+ else
87
118
  build_action_group(params, scope, column)
88
119
  end
89
120
  end
@@ -94,10 +125,13 @@ module Json2sql
94
125
  # -------------------------------------------------------------------------
95
126
 
96
127
  def build_action_group(params, scope, column)
128
+
97
129
  glue = false
98
130
 
99
131
  params.each do |key, value|
132
+
100
133
  @sql << scope if glue
134
+
101
135
  glue = true
102
136
 
103
137
  build_action_types(value, column, key.to_s)
@@ -105,13 +139,18 @@ module Json2sql
105
139
  end
106
140
 
107
141
  def build_action_types(params, column, action)
142
+
108
143
  if action == "and"
144
+
109
145
  build_column_types(params, " AND ", column)
146
+
110
147
  return
111
148
  end
112
149
 
113
150
  if action == "or"
151
+
114
152
  build_column_types(params, " OR ", column)
153
+
115
154
  return
116
155
  end
117
156
 
@@ -123,79 +162,126 @@ module Json2sql
123
162
  # -------------------------------------------------------------------------
124
163
 
125
164
  def build_action_values(params, column, action) # rubocop:disable Metrics/MethodLength
165
+
126
166
  case params
127
167
  when TrueClass, FalseClass
168
+
128
169
  # Only "null" → IS NULL / IS NOT NULL. Boolean equality is not emitted
129
170
  # (matches C++ behaviour — use integer 1/0 for boolean equality).
130
171
  if action == "null"
172
+
131
173
  action_str = params ? " IS " : " IS NOT "
174
+
132
175
  @sql << Sanitizer.keyword_wrap(@table) << "."
176
+
133
177
  @sql << Sanitizer.keyword_wrap(column)
178
+
134
179
  @sql << action_str << "NULL"
135
180
  end
136
181
 
137
182
  when Integer
183
+
138
184
  action_name = get_action(action)
185
+
139
186
  @sql << Sanitizer.keyword_wrap(@table) << "."
187
+
140
188
  @sql << Sanitizer.keyword_wrap(column)
189
+
141
190
  @sql << " #{action_name} #{params}"
142
191
 
143
192
  when Float
193
+
144
194
  action_name = get_action(action)
195
+
145
196
  @sql << Sanitizer.keyword_wrap(@table) << "."
197
+
146
198
  @sql << Sanitizer.keyword_wrap(column)
199
+
147
200
  @sql << " #{action_name} #{params}"
148
201
 
149
202
  when String
203
+
150
204
  build_action_string(params, column, action)
151
205
 
152
206
  when Array
207
+
153
208
  action_name = get_action(action)
209
+
154
210
  @sql << Sanitizer.keyword_wrap(@table) << "."
211
+
155
212
  @sql << Sanitizer.keyword_wrap(column)
213
+
156
214
  @sql << " #{action_name} ("
215
+
157
216
  build_array(params)
217
+
158
218
  @sql << ")"
159
219
 
160
220
  when Hash
221
+
161
222
  action_name = get_action(action)
223
+
162
224
  @sql << Sanitizer.keyword_wrap(@table) << "."
225
+
163
226
  @sql << Sanitizer.keyword_wrap(column)
227
+
164
228
  @sql << " #{action_name} ("
229
+
165
230
  build_object(params)
231
+
166
232
  @sql << ")"
167
233
  end
168
234
  end
169
235
 
170
236
  def build_action_string(params, column, action)
237
+
171
238
  action_name = get_action(action)
172
239
 
173
240
  case action_name
174
241
  when "last"
242
+
175
243
  @sql << Sanitizer.keyword_wrap(@table) << "."
244
+
176
245
  @sql << Sanitizer.keyword_wrap(column)
246
+
177
247
  @sql << " LIKE '%" << Sanitizer.value(params) << "'"
178
248
 
179
249
  when "first"
250
+
180
251
  @sql << Sanitizer.keyword_wrap(@table) << "."
252
+
181
253
  @sql << Sanitizer.keyword_wrap(column)
254
+
182
255
  @sql << " LIKE '" << Sanitizer.value(params) << "%'"
183
256
 
184
257
  when "contains"
258
+
185
259
  @sql << Sanitizer.keyword_wrap(@table) << "."
260
+
186
261
  @sql << Sanitizer.keyword_wrap(column)
262
+
187
263
  @sql << " LIKE '%" << Sanitizer.value(params) << "%'"
188
264
 
189
265
  else
266
+
190
267
  if params.start_with?("$.")
268
+
191
269
  @sql << Sanitizer.keyword_wrap(@table) << "."
270
+
192
271
  @sql << Sanitizer.keyword_wrap(column)
272
+
193
273
  @sql << " #{action_name} "
274
+
194
275
  @sql << Sanitizer.reference(params)
276
+
195
277
  else
278
+
196
279
  @sql << Sanitizer.keyword_wrap(@table) << "."
280
+
197
281
  @sql << Sanitizer.keyword_wrap(column)
282
+
198
283
  @sql << " #{action_name} "
284
+
199
285
  @sql << Sanitizer.value_wrap(params)
200
286
  end
201
287
  end
@@ -206,40 +292,57 @@ module Json2sql
206
292
  # -------------------------------------------------------------------------
207
293
 
208
294
  def build_array(array)
295
+
209
296
  if array.empty?
297
+
210
298
  @sql << "NULL"
299
+
211
300
  return
212
301
  end
213
302
 
214
303
  glue = false
304
+
215
305
  array.each do |item|
306
+
216
307
  @sql << ", " if glue
308
+
217
309
  glue = true
218
310
 
219
311
  case item
220
- when Integer, Float then @sql << item.to_s
221
- when String then @sql << Sanitizer.value_wrap(item)
312
+ when Float then @sql << item.to_s
313
+ when Integer then @sql << item.to_s
314
+ when String then @sql << Sanitizer.value_wrap(item)
222
315
  end
223
316
  end
224
317
  end
225
318
 
226
319
  # Builds a UNION of sub-SELECTs (used when action value is a Hash of tables).
320
+
227
321
  def build_object(object)
322
+
228
323
  if object.empty?
324
+
229
325
  @sql << "NULL"
326
+
230
327
  return
231
328
  end
232
329
 
233
330
  glue = false
331
+
234
332
  relation = WhereRelation.none(@table)
235
333
 
236
334
  object.each do |key, value|
335
+
237
336
  @sql << " UNION " if glue
337
+
238
338
  glue = true
239
339
 
240
340
  tbl = key.to_s
341
+
241
342
  @sql << "("
343
+
242
344
  SelectModel.new(@sql, tbl, relation).build_query_default(value)
345
+
243
346
  @sql << ")"
244
347
  end
245
348
  end
@@ -249,6 +352,7 @@ module Json2sql
249
352
  # -------------------------------------------------------------------------
250
353
 
251
354
  def get_action(action)
355
+
252
356
  case action
253
357
  when "=", "<", ">", "<=", ">=", "!=", "<>" then action
254
358
  when "in" then "IN"
@@ -258,5 +362,7 @@ module Json2sql
258
362
  else action
259
363
  end
260
364
  end
365
+
261
366
  end
367
+
262
368
  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.1
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