json2sql 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/json2sql/delete_model.rb +20 -0
- data/lib/json2sql/delete_runner.rb +42 -0
- data/lib/json2sql/insert_model.rb +56 -0
- data/lib/json2sql/insert_runner.rb +41 -0
- data/lib/json2sql/sanitizer.rb +49 -0
- data/lib/json2sql/select_model.rb +222 -0
- data/lib/json2sql/select_runner.rb +44 -0
- data/lib/json2sql/update_model.rb +48 -0
- data/lib/json2sql/update_runner.rb +45 -0
- data/lib/json2sql/version.rb +5 -0
- data/lib/json2sql/where_model.rb +262 -0
- data/lib/json2sql/where_relation.rb +76 -0
- data/lib/json2sql.rb +38 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 40d7668af9e81534fea68e0745ccb82901d144b1cd3f8851999442262f6e3167
|
|
4
|
+
data.tar.gz: d12e2089964c6eecfbd752025f492d56f78bdf72102bb1879d0e57056765ae2a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1bdf680bb7009a436cbe5fcdfa70dd6b69036259f61c26b111b72ed3e8d07ed52528b2ffda79d15db4e0a7a6887cfea2e0c99ca504d421ac06c479eb4ac1a2ed
|
|
7
|
+
data.tar.gz: 7f5187f7f7d1bc36284d7fb11bef6273dba90e4f7deb78cf286909831bd0e62a6a02e1354e6ed193e2f7c631f04ee72f87f16ff41c2a37af2a2e79b9b6adb4f4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tiago da Silva
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds a DELETE FROM statement for a single table.
|
|
3
|
+
#
|
|
4
|
+
# Input Hash:
|
|
5
|
+
# "and" => { ... } – WHERE conditions (required to avoid deleting all rows)
|
|
6
|
+
# "or" => { ... } – WHERE conditions (OR)
|
|
7
|
+
class DeleteModel
|
|
8
|
+
def initialize(sql, table, relation)
|
|
9
|
+
@sql = sql
|
|
10
|
+
@table = table.to_s
|
|
11
|
+
@relation = relation
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build(params)
|
|
15
|
+
@sql << "DELETE FROM "
|
|
16
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
17
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds one or more DELETE statements from a Hash of table → params.
|
|
3
|
+
#
|
|
4
|
+
# Usage (single deletion):
|
|
5
|
+
# sql = Json2sql::DeleteRunner.build(
|
|
6
|
+
# "users" => { "and" => { "id" => 42 } }
|
|
7
|
+
# )
|
|
8
|
+
#
|
|
9
|
+
# Usage (multiple deletions — value is an Array):
|
|
10
|
+
# sql = Json2sql::DeleteRunner.build(
|
|
11
|
+
# "sessions" => [
|
|
12
|
+
# { "and" => { "user_id" => 1 } },
|
|
13
|
+
# { "and" => { "user_id" => 2 } }
|
|
14
|
+
# ]
|
|
15
|
+
# )
|
|
16
|
+
class DeleteRunner
|
|
17
|
+
def self.build(input)
|
|
18
|
+
input = Json2sql.normalize(input)
|
|
19
|
+
sql = +""
|
|
20
|
+
relation = WhereRelation.none("")
|
|
21
|
+
|
|
22
|
+
input.each do |table, value|
|
|
23
|
+
tbl = table.to_s
|
|
24
|
+
|
|
25
|
+
case value
|
|
26
|
+
when Hash
|
|
27
|
+
DeleteModel.new(sql, tbl, relation).build(value)
|
|
28
|
+
sql << ";\n"
|
|
29
|
+
when Array
|
|
30
|
+
value.each do |item|
|
|
31
|
+
next unless item.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
DeleteModel.new(sql, tbl, relation).build(item)
|
|
34
|
+
sql << ";\n"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
sql
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds an INSERT INTO statement for a single table.
|
|
3
|
+
#
|
|
4
|
+
# Input Hash:
|
|
5
|
+
# "columns" => { "col_name" => value, ... }
|
|
6
|
+
#
|
|
7
|
+
# Values:
|
|
8
|
+
# Integer / Float → inserted as raw numbers
|
|
9
|
+
# String → wrapped in single quotes with SQL escaping
|
|
10
|
+
class InsertModel
|
|
11
|
+
def initialize(sql, table)
|
|
12
|
+
@sql = sql
|
|
13
|
+
@table = table.to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build(params)
|
|
17
|
+
@sql << "INSERT INTO "
|
|
18
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
19
|
+
@sql << " ("
|
|
20
|
+
build_columns(params)
|
|
21
|
+
@sql << ") VALUES ("
|
|
22
|
+
build_values(params)
|
|
23
|
+
@sql << ")"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_columns(params)
|
|
29
|
+
columns = params["columns"]
|
|
30
|
+
return unless columns.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
separator = false
|
|
33
|
+
columns.each_key do |key|
|
|
34
|
+
@sql << ", " if separator
|
|
35
|
+
separator = true
|
|
36
|
+
@sql << Sanitizer.keyword_wrap(key.to_s)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_values(params)
|
|
41
|
+
columns = params["columns"]
|
|
42
|
+
return unless columns.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
separator = false
|
|
45
|
+
columns.each_value do |value|
|
|
46
|
+
@sql << ", " if separator
|
|
47
|
+
separator = true
|
|
48
|
+
|
|
49
|
+
case value
|
|
50
|
+
when Integer, Float then @sql << value.to_s
|
|
51
|
+
when String then @sql << Sanitizer.value_wrap(value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds one or more INSERT statements from a Hash of table → params.
|
|
3
|
+
#
|
|
4
|
+
# Usage (single row):
|
|
5
|
+
# sql = Json2sql::InsertRunner.build(
|
|
6
|
+
# "users" => { "columns" => { "name" => "João", "email" => "j@x.com" } }
|
|
7
|
+
# )
|
|
8
|
+
#
|
|
9
|
+
# Usage (multiple rows — value is an Array):
|
|
10
|
+
# sql = Json2sql::InsertRunner.build(
|
|
11
|
+
# "tags" => [
|
|
12
|
+
# { "columns" => { "name" => "ruby" } },
|
|
13
|
+
# { "columns" => { "name" => "rails" } }
|
|
14
|
+
# ]
|
|
15
|
+
# )
|
|
16
|
+
class InsertRunner
|
|
17
|
+
def self.build(input)
|
|
18
|
+
input = Json2sql.normalize(input)
|
|
19
|
+
sql = +""
|
|
20
|
+
|
|
21
|
+
input.each do |table, value|
|
|
22
|
+
tbl = table.to_s
|
|
23
|
+
|
|
24
|
+
case value
|
|
25
|
+
when Hash
|
|
26
|
+
InsertModel.new(sql, tbl).build(value)
|
|
27
|
+
sql << ";\n"
|
|
28
|
+
when Array
|
|
29
|
+
value.each do |item|
|
|
30
|
+
next unless item.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
InsertModel.new(sql, tbl).build(item)
|
|
33
|
+
sql << ";\n"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sql
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
module Sanitizer
|
|
3
|
+
# Characters stripped from SQL identifiers (table/column names).
|
|
4
|
+
KEYWORD_DANGEROUS = /[ `;"'\\]/
|
|
5
|
+
|
|
6
|
+
# Removes dangerous characters from an identifier string.
|
|
7
|
+
def self.keyword(input)
|
|
8
|
+
input.to_s.gsub(KEYWORD_DANGEROUS, "")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Escapes a value string for safe embedding between SQL quotes.
|
|
12
|
+
# ' → '' and \ → \\
|
|
13
|
+
def self.value(input)
|
|
14
|
+
input.to_s.gsub("\\", "\\\\\\\\").gsub("'", "''")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Wraps an identifier in the given quote character (default: backtick).
|
|
18
|
+
# Dangerous characters inside the identifier are stripped.
|
|
19
|
+
def self.keyword_wrap(input, wrap = "`")
|
|
20
|
+
"#{wrap}#{keyword(input)}#{wrap}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Wraps a value in the given quote character (default: single-quote).
|
|
24
|
+
# Single quotes and backslashes inside the value are escaped.
|
|
25
|
+
def self.value_wrap(input, wrap = "'")
|
|
26
|
+
"#{wrap}#{value(input)}#{wrap}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Converts a JSON path reference (e.g. "$.users.id") into a
|
|
30
|
+
# backtick-quoted SQL reference (e.g. "`users`.`id`").
|
|
31
|
+
# Strips the leading "$." and splits on ".".
|
|
32
|
+
def self.reference(input)
|
|
33
|
+
str = input.to_s[2..] # strip leading "$."
|
|
34
|
+
result = +"`"
|
|
35
|
+
str.each_char do |c|
|
|
36
|
+
case c
|
|
37
|
+
when "."
|
|
38
|
+
result << "`.`"
|
|
39
|
+
when " ", "`", ";", '"', "'", "\\"
|
|
40
|
+
# skip dangerous characters
|
|
41
|
+
else
|
|
42
|
+
result << c
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
result << "`"
|
|
46
|
+
result
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds a SELECT SQL statement for a single table.
|
|
3
|
+
#
|
|
4
|
+
# Input Hash keys (all optional):
|
|
5
|
+
# "columns" => ["id", "name", ...] – columns to SELECT
|
|
6
|
+
# "where" => { "and" => {...} } – WHERE conditions (see WhereModel)
|
|
7
|
+
# "and" => {...} – shorthand for top-level AND WHERE
|
|
8
|
+
# "or" => {...} – shorthand for top-level OR WHERE
|
|
9
|
+
# "order" => { "col" => "asc" } – ORDER BY
|
|
10
|
+
# "limit" => 10 – LIMIT
|
|
11
|
+
# "offset" => 20 – OFFSET
|
|
12
|
+
# "options" => ["total"] – wrap response with data/total JSON
|
|
13
|
+
# "children" => { "table" => {...} } – nested child arrays
|
|
14
|
+
# "parents" => { "table" => {...} } – nested parent objects
|
|
15
|
+
class SelectModel
|
|
16
|
+
def initialize(sql, table, relation)
|
|
17
|
+
@sql = sql
|
|
18
|
+
@table = table.to_s
|
|
19
|
+
@relation = relation
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# SELECT COUNT(*) AS `table` FROM `table` WHERE ...
|
|
23
|
+
def build_query_count(params)
|
|
24
|
+
@sql << "SELECT COUNT(*) AS "
|
|
25
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
26
|
+
@sql << " FROM "
|
|
27
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
28
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Plain SELECT col1, col2 FROM `table` WHERE ... ORDER BY ... LIMIT ... OFFSET ...
|
|
32
|
+
def build_query_default(params)
|
|
33
|
+
@sep = false
|
|
34
|
+
@sql << "SELECT "
|
|
35
|
+
build_columns_default(params)
|
|
36
|
+
@sql << " FROM "
|
|
37
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
38
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
39
|
+
build_order(params)
|
|
40
|
+
build_limit(params)
|
|
41
|
+
build_offset(params)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# SELECT JSON_ARRAYAGG(JSON_OBJECT(...)) AS `table`
|
|
45
|
+
# FROM LATERAL (SELECT * FROM `table` WHERE ... ORDER ... LIMIT ...) AS `table`
|
|
46
|
+
def build_query_array(params)
|
|
47
|
+
@sep = false
|
|
48
|
+
@sql << "SELECT JSON_ARRAYAGG(JSON_OBJECT("
|
|
49
|
+
build_columns_json(params)
|
|
50
|
+
build_columns_array(params)
|
|
51
|
+
build_columns_object(params)
|
|
52
|
+
@sql << ")) AS "
|
|
53
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
54
|
+
@sql << " FROM LATERAL (SELECT * FROM "
|
|
55
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
56
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
57
|
+
build_order(params)
|
|
58
|
+
build_limit(params)
|
|
59
|
+
build_offset(params)
|
|
60
|
+
@sql << ") AS "
|
|
61
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# SELECT JSON_OBJECT(...) AS `table`
|
|
65
|
+
# FROM LATERAL (SELECT * FROM `table` WHERE ...) AS `table`
|
|
66
|
+
def build_query_object(params)
|
|
67
|
+
@sep = false
|
|
68
|
+
@sql << "SELECT JSON_OBJECT("
|
|
69
|
+
build_columns_json(params)
|
|
70
|
+
build_columns_array(params)
|
|
71
|
+
build_columns_object(params)
|
|
72
|
+
@sql << ") AS "
|
|
73
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
74
|
+
@sql << " FROM LATERAL (SELECT * FROM "
|
|
75
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
76
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
77
|
+
build_order(params)
|
|
78
|
+
build_limit(params)
|
|
79
|
+
build_offset(params)
|
|
80
|
+
@sql << ") AS "
|
|
81
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Smart dispatcher:
|
|
85
|
+
# - no options → build_query_array
|
|
86
|
+
# - options includes "total" → wraps with JSON_OBJECT('data', ..., 'total', COUNT(*))
|
|
87
|
+
def build_query_options(params)
|
|
88
|
+
options = params["options"]
|
|
89
|
+
|
|
90
|
+
unless options.is_a?(Array) && !options.empty?
|
|
91
|
+
build_query_array(params)
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
total = options.include?("total")
|
|
96
|
+
|
|
97
|
+
@sql << "SELECT JSON_OBJECT('data', ("
|
|
98
|
+
build_query_array(params)
|
|
99
|
+
@sql << ")"
|
|
100
|
+
|
|
101
|
+
if total
|
|
102
|
+
@sql << ", 'total', ("
|
|
103
|
+
build_query_count(params)
|
|
104
|
+
@sql << ")"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@sql << ")"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# @sep is a shared separator flag used by build_columns_* to coordinate
|
|
113
|
+
# comma placement across multiple calls within a single query build.
|
|
114
|
+
|
|
115
|
+
# Appends plain column references: `table`.`col`, `table`.`col2`, ...
|
|
116
|
+
def build_columns_default(params)
|
|
117
|
+
columns = params["columns"]
|
|
118
|
+
return unless columns.is_a?(Array)
|
|
119
|
+
|
|
120
|
+
columns.each do |column|
|
|
121
|
+
next unless column.is_a?(String) || column.is_a?(Symbol)
|
|
122
|
+
|
|
123
|
+
@sql << ", " if @sep
|
|
124
|
+
@sep = true
|
|
125
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
126
|
+
@sql << Sanitizer.keyword_wrap(column.to_s)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Appends JSON key-value pairs for columns: 'col', `table`.`col`, ...
|
|
131
|
+
def build_columns_json(params)
|
|
132
|
+
columns = params["columns"]
|
|
133
|
+
return unless columns.is_a?(Array)
|
|
134
|
+
|
|
135
|
+
columns.each do |column|
|
|
136
|
+
next unless column.is_a?(String) || column.is_a?(Symbol)
|
|
137
|
+
|
|
138
|
+
@sql << ", " if @sep
|
|
139
|
+
@sep = true
|
|
140
|
+
col = column.to_s
|
|
141
|
+
@sql << Sanitizer.keyword_wrap(col, "'")
|
|
142
|
+
@sql << ", "
|
|
143
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
144
|
+
@sql << Sanitizer.keyword_wrap(col)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Appends nested child arrays (subquery → JSON_ARRAYAGG).
|
|
149
|
+
# Uses WhereRelation::PARENT because child table references parent.
|
|
150
|
+
def build_columns_array(params)
|
|
151
|
+
children = params["children"]
|
|
152
|
+
return unless children.is_a?(Hash)
|
|
153
|
+
|
|
154
|
+
relation = WhereRelation.parent(@table)
|
|
155
|
+
|
|
156
|
+
children.each do |key, value|
|
|
157
|
+
next unless value.is_a?(Hash)
|
|
158
|
+
|
|
159
|
+
@sql << ", " if @sep
|
|
160
|
+
@sep = true
|
|
161
|
+
tbl = key.to_s
|
|
162
|
+
@sql << Sanitizer.keyword_wrap(tbl, "'")
|
|
163
|
+
@sql << ", ("
|
|
164
|
+
SelectModel.new(@sql, tbl, relation).build_query_options(value)
|
|
165
|
+
@sql << ")"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Appends nested parent objects (subquery → JSON_OBJECT, single row).
|
|
170
|
+
# Uses WhereRelation::CHILD because parent table is referenced from child.
|
|
171
|
+
def build_columns_object(params)
|
|
172
|
+
parents = params["parents"]
|
|
173
|
+
return unless parents.is_a?(Hash)
|
|
174
|
+
|
|
175
|
+
relation = WhereRelation.child(@table)
|
|
176
|
+
|
|
177
|
+
parents.each do |key, value|
|
|
178
|
+
next unless value.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
@sql << ", " if @sep
|
|
181
|
+
@sep = true
|
|
182
|
+
tbl = key.to_s
|
|
183
|
+
@sql << Sanitizer.keyword_wrap(tbl, "'")
|
|
184
|
+
@sql << ", ("
|
|
185
|
+
SelectModel.new(@sql, tbl, relation).build_query_object(value)
|
|
186
|
+
@sql << ")"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_order(params)
|
|
191
|
+
order = params["order"]
|
|
192
|
+
return unless order.is_a?(Hash) && !order.empty?
|
|
193
|
+
|
|
194
|
+
@sql << " ORDER BY "
|
|
195
|
+
glue = false
|
|
196
|
+
|
|
197
|
+
order.each do |key, value|
|
|
198
|
+
@sql << ", " if glue
|
|
199
|
+
glue = true
|
|
200
|
+
|
|
201
|
+
column = key.to_s
|
|
202
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
203
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
204
|
+
|
|
205
|
+
case value.to_s.downcase
|
|
206
|
+
when "asc" then @sql << " ASC"
|
|
207
|
+
when "desc" then @sql << " DESC"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_limit(params)
|
|
213
|
+
limit = params["limit"]
|
|
214
|
+
@sql << " LIMIT #{limit}" if limit.is_a?(Integer)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_offset(params)
|
|
218
|
+
offset = params["offset"]
|
|
219
|
+
@sql << " OFFSET #{offset}" if offset.is_a?(Integer)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds a top-level SELECT statement from a Hash of table → params.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# sql = Json2sql::SelectRunner.build(
|
|
6
|
+
# "users" => {
|
|
7
|
+
# "columns" => ["id", "name", "email"],
|
|
8
|
+
# "and" => { "active" => 1, "role" => { "in" => [1, 2] } },
|
|
9
|
+
# "order" => { "created_at" => "desc" },
|
|
10
|
+
# "limit" => 20,
|
|
11
|
+
# "offset" => 0,
|
|
12
|
+
# "options" => ["total"]
|
|
13
|
+
# }
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# Output wraps every table in JSON_OBJECT so the client receives a single
|
|
17
|
+
# JSON document:
|
|
18
|
+
# SELECT JSON_OBJECT('users', (...));
|
|
19
|
+
class SelectRunner
|
|
20
|
+
def self.build(input)
|
|
21
|
+
input = Json2sql.normalize(input)
|
|
22
|
+
sql = +""
|
|
23
|
+
separator = false
|
|
24
|
+
relation = WhereRelation.none("")
|
|
25
|
+
|
|
26
|
+
sql << "SELECT JSON_OBJECT("
|
|
27
|
+
|
|
28
|
+
input.each do |table, value|
|
|
29
|
+
next unless value.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
sql << ", " if separator
|
|
32
|
+
separator = true
|
|
33
|
+
|
|
34
|
+
sql << Sanitizer.keyword_wrap(table.to_s, "'")
|
|
35
|
+
sql << ", ("
|
|
36
|
+
SelectModel.new(sql, table.to_s, relation).build_query_options(value)
|
|
37
|
+
sql << ")"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sql << ");\n"
|
|
41
|
+
sql
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds an UPDATE statement for a single table.
|
|
3
|
+
#
|
|
4
|
+
# Input Hash:
|
|
5
|
+
# "columns" => { "col" => value, ... } – columns to SET
|
|
6
|
+
# "and" => { ... } – WHERE conditions
|
|
7
|
+
# "or" => { ... } – WHERE conditions (OR)
|
|
8
|
+
#
|
|
9
|
+
# Value types follow the same rules as InsertModel.
|
|
10
|
+
class UpdateModel
|
|
11
|
+
def initialize(sql, table, relation)
|
|
12
|
+
@sql = sql
|
|
13
|
+
@table = table.to_s
|
|
14
|
+
@relation = relation
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build(params)
|
|
18
|
+
@sql << "UPDATE "
|
|
19
|
+
@sql << Sanitizer.keyword_wrap(@table)
|
|
20
|
+
@sql << " SET "
|
|
21
|
+
build_columns(params)
|
|
22
|
+
WhereModel.new(@sql, @table, @relation).build(params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_columns(params)
|
|
28
|
+
columns = params["columns"]
|
|
29
|
+
return unless columns.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
separator = false
|
|
32
|
+
columns.each do |key, value|
|
|
33
|
+
@sql << ", " if separator
|
|
34
|
+
separator = true
|
|
35
|
+
|
|
36
|
+
column = key.to_s
|
|
37
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
38
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
39
|
+
@sql << " = "
|
|
40
|
+
|
|
41
|
+
case value
|
|
42
|
+
when Integer, Float then @sql << value.to_s
|
|
43
|
+
when String then @sql << Sanitizer.value_wrap(value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds one or more UPDATE statements from a Hash of table → params.
|
|
3
|
+
#
|
|
4
|
+
# Usage (single row):
|
|
5
|
+
# sql = Json2sql::UpdateRunner.build(
|
|
6
|
+
# "users" => {
|
|
7
|
+
# "columns" => { "name" => "Maria", "updated_at" => "2026-04-12" },
|
|
8
|
+
# "and" => { "id" => 42 }
|
|
9
|
+
# }
|
|
10
|
+
# )
|
|
11
|
+
#
|
|
12
|
+
# Usage (multiple rows — value is an Array):
|
|
13
|
+
# sql = Json2sql::UpdateRunner.build(
|
|
14
|
+
# "settings" => [
|
|
15
|
+
# { "columns" => { "value" => "dark" }, "and" => { "key" => "theme" } },
|
|
16
|
+
# { "columns" => { "value" => "en" }, "and" => { "key" => "lang" } }
|
|
17
|
+
# ]
|
|
18
|
+
# )
|
|
19
|
+
class UpdateRunner
|
|
20
|
+
def self.build(input)
|
|
21
|
+
input = Json2sql.normalize(input)
|
|
22
|
+
sql = +""
|
|
23
|
+
relation = WhereRelation.none("")
|
|
24
|
+
|
|
25
|
+
input.each do |table, value|
|
|
26
|
+
tbl = table.to_s
|
|
27
|
+
|
|
28
|
+
case value
|
|
29
|
+
when Hash
|
|
30
|
+
UpdateModel.new(sql, tbl, relation).build(value)
|
|
31
|
+
sql << ";\n"
|
|
32
|
+
when Array
|
|
33
|
+
value.each do |item|
|
|
34
|
+
next unless item.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
UpdateModel.new(sql, tbl, relation).build(item)
|
|
37
|
+
sql << ";\n"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sql
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
# Builds a SQL WHERE clause from a Hash describing the conditions.
|
|
3
|
+
#
|
|
4
|
+
# Input structure mirrors the JSON format used in the C++ backend:
|
|
5
|
+
#
|
|
6
|
+
# {
|
|
7
|
+
# "and" => {
|
|
8
|
+
# "name" => "john", # implicit LIKE '%john%'
|
|
9
|
+
# "age" => 30, # implicit equality
|
|
10
|
+
# "status" => { "in" => [1,2] },
|
|
11
|
+
# "score" => { ">=" => 4.5 },
|
|
12
|
+
# "col" => { "null" => true }, # IS NULL / IS NOT NULL
|
|
13
|
+
# "ref" => { "=" => "$.table.col" } # column reference
|
|
14
|
+
# },
|
|
15
|
+
# "or" => { ... }
|
|
16
|
+
# }
|
|
17
|
+
#
|
|
18
|
+
# Supported operators: = < > <= >= != <>
|
|
19
|
+
# in !in like !like
|
|
20
|
+
# String pseudo-actions: contains (LIKE %v%), first (LIKE v%), last (LIKE %v)
|
|
21
|
+
class WhereModel
|
|
22
|
+
def initialize(sql, table, relation)
|
|
23
|
+
@sql = sql
|
|
24
|
+
@table = table.to_s
|
|
25
|
+
@relation = relation
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build(params)
|
|
29
|
+
has_relation = @relation.kind != WhereRelation::NONE
|
|
30
|
+
has_where_and = params["and"].is_a?(Hash)
|
|
31
|
+
has_where_or = params["or"].is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
return unless has_relation || has_where_and || has_where_or
|
|
34
|
+
|
|
35
|
+
@sql << " WHERE "
|
|
36
|
+
|
|
37
|
+
if has_relation
|
|
38
|
+
@relation.build_table_relation(@sql, @table)
|
|
39
|
+
@sql << " AND " if has_where_and || has_where_or
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if has_where_and
|
|
43
|
+
build_column_group(params["and"], " AND ")
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if has_where_or
|
|
48
|
+
build_column_group(params["or"], " OR ")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# -------------------------------------------------------------------------
|
|
55
|
+
# Group level
|
|
56
|
+
# -------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def build_column_group(params, scope)
|
|
59
|
+
@sql << "("
|
|
60
|
+
glue = false
|
|
61
|
+
|
|
62
|
+
params.each do |key, value|
|
|
63
|
+
@sql << scope if glue
|
|
64
|
+
glue = true
|
|
65
|
+
|
|
66
|
+
build_column_types(value, scope, key.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@sql << ")"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Dispatch by Ruby type of the value.
|
|
73
|
+
def build_column_types(params, scope, column)
|
|
74
|
+
case params
|
|
75
|
+
when TrueClass, FalseClass
|
|
76
|
+
build_action_types(params, column, "=")
|
|
77
|
+
when Integer
|
|
78
|
+
build_action_types(params, column, "=")
|
|
79
|
+
when String
|
|
80
|
+
build_action_types(params, column, "contains")
|
|
81
|
+
when Hash
|
|
82
|
+
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)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# -------------------------------------------------------------------------
|
|
93
|
+
# Action level
|
|
94
|
+
# -------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def build_action_group(params, scope, column)
|
|
97
|
+
glue = false
|
|
98
|
+
|
|
99
|
+
params.each do |key, value|
|
|
100
|
+
@sql << scope if glue
|
|
101
|
+
glue = true
|
|
102
|
+
|
|
103
|
+
build_action_types(value, column, key.to_s)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_action_types(params, column, action)
|
|
108
|
+
if action == "and"
|
|
109
|
+
build_column_types(params, " AND ", column)
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if action == "or"
|
|
114
|
+
build_column_types(params, " OR ", column)
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
build_action_values(params, column, action)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# -------------------------------------------------------------------------
|
|
122
|
+
# Value level — emit the actual SQL comparison
|
|
123
|
+
# -------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
def build_action_values(params, column, action) # rubocop:disable Metrics/MethodLength
|
|
126
|
+
case params
|
|
127
|
+
when TrueClass, FalseClass
|
|
128
|
+
# Only "null" → IS NULL / IS NOT NULL. Boolean equality is not emitted
|
|
129
|
+
# (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
|
|
136
|
+
|
|
137
|
+
when Integer
|
|
138
|
+
action_name = get_action(action)
|
|
139
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
140
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
141
|
+
@sql << " #{action_name} #{params}"
|
|
142
|
+
|
|
143
|
+
when Float
|
|
144
|
+
action_name = get_action(action)
|
|
145
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
146
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
147
|
+
@sql << " #{action_name} #{params}"
|
|
148
|
+
|
|
149
|
+
when String
|
|
150
|
+
build_action_string(params, column, action)
|
|
151
|
+
|
|
152
|
+
when Array
|
|
153
|
+
action_name = get_action(action)
|
|
154
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
155
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
156
|
+
@sql << " #{action_name} ("
|
|
157
|
+
build_array(params)
|
|
158
|
+
@sql << ")"
|
|
159
|
+
|
|
160
|
+
when Hash
|
|
161
|
+
action_name = get_action(action)
|
|
162
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
163
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
164
|
+
@sql << " #{action_name} ("
|
|
165
|
+
build_object(params)
|
|
166
|
+
@sql << ")"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def build_action_string(params, column, action)
|
|
171
|
+
action_name = get_action(action)
|
|
172
|
+
|
|
173
|
+
case action_name
|
|
174
|
+
when "last"
|
|
175
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
176
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
177
|
+
@sql << " LIKE '%" << Sanitizer.value(params) << "'"
|
|
178
|
+
|
|
179
|
+
when "first"
|
|
180
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
181
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
182
|
+
@sql << " LIKE '" << Sanitizer.value(params) << "%'"
|
|
183
|
+
|
|
184
|
+
when "contains"
|
|
185
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
186
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
187
|
+
@sql << " LIKE '%" << Sanitizer.value(params) << "%'"
|
|
188
|
+
|
|
189
|
+
else
|
|
190
|
+
if params.start_with?("$.")
|
|
191
|
+
@sql << Sanitizer.keyword_wrap(@table) << "."
|
|
192
|
+
@sql << Sanitizer.keyword_wrap(column)
|
|
193
|
+
@sql << " #{action_name} "
|
|
194
|
+
@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)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# -------------------------------------------------------------------------
|
|
205
|
+
# IN-list and subquery helpers
|
|
206
|
+
# -------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def build_array(array)
|
|
209
|
+
if array.empty?
|
|
210
|
+
@sql << "NULL"
|
|
211
|
+
return
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
glue = false
|
|
215
|
+
array.each do |item|
|
|
216
|
+
@sql << ", " if glue
|
|
217
|
+
glue = true
|
|
218
|
+
|
|
219
|
+
case item
|
|
220
|
+
when Integer, Float then @sql << item.to_s
|
|
221
|
+
when String then @sql << Sanitizer.value_wrap(item)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Builds a UNION of sub-SELECTs (used when action value is a Hash of tables).
|
|
227
|
+
def build_object(object)
|
|
228
|
+
if object.empty?
|
|
229
|
+
@sql << "NULL"
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
glue = false
|
|
234
|
+
relation = WhereRelation.none(@table)
|
|
235
|
+
|
|
236
|
+
object.each do |key, value|
|
|
237
|
+
@sql << " UNION " if glue
|
|
238
|
+
glue = true
|
|
239
|
+
|
|
240
|
+
tbl = key.to_s
|
|
241
|
+
@sql << "("
|
|
242
|
+
SelectModel.new(@sql, tbl, relation).build_query_default(value)
|
|
243
|
+
@sql << ")"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# -------------------------------------------------------------------------
|
|
248
|
+
# Operator mapping
|
|
249
|
+
# -------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def get_action(action)
|
|
252
|
+
case action
|
|
253
|
+
when "=", "<", ">", "<=", ">=", "!=", "<>" then action
|
|
254
|
+
when "in" then "IN"
|
|
255
|
+
when "!in" then "NOT IN"
|
|
256
|
+
when "like" then "LIKE"
|
|
257
|
+
when "!like" then "NOT LIKE"
|
|
258
|
+
else action
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Json2sql
|
|
2
|
+
class WhereRelation
|
|
3
|
+
NONE = :none
|
|
4
|
+
CHILD = :child
|
|
5
|
+
PARENT = :parent
|
|
6
|
+
|
|
7
|
+
attr_reader :table, :kind
|
|
8
|
+
|
|
9
|
+
def initialize(table, kind)
|
|
10
|
+
@table = table.to_s
|
|
11
|
+
@kind = kind
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Factory: no relationship (top-level query).
|
|
15
|
+
def self.none(table)
|
|
16
|
+
new(table, NONE)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Factory: foreign key is on the child table pointing to the parent.
|
|
20
|
+
# Produces: `parent`.`child_id` = `current`.`id`
|
|
21
|
+
def self.child(table)
|
|
22
|
+
new(table, CHILD)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Factory: foreign key is on the current/parent table pointing to the child.
|
|
26
|
+
# Produces: `current`.`parent_id` = `parent`.`id`
|
|
27
|
+
def self.parent(table)
|
|
28
|
+
new(table, PARENT)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Appends the JOIN condition for this relationship into sql.
|
|
32
|
+
# +current+ is the name of the table being queried.
|
|
33
|
+
def build_table_relation(sql, current)
|
|
34
|
+
current = current.to_s
|
|
35
|
+
|
|
36
|
+
if kind == CHILD
|
|
37
|
+
sql << Sanitizer.keyword_wrap(table)
|
|
38
|
+
sql << "."
|
|
39
|
+
sql << build_table_id(current)
|
|
40
|
+
sql << " = "
|
|
41
|
+
sql << Sanitizer.keyword_wrap(current)
|
|
42
|
+
sql << ".`id`"
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if kind == PARENT
|
|
47
|
+
sql << Sanitizer.keyword_wrap(current)
|
|
48
|
+
sql << "."
|
|
49
|
+
sql << build_table_id(table)
|
|
50
|
+
sql << " = "
|
|
51
|
+
sql << Sanitizer.keyword_wrap(table)
|
|
52
|
+
sql << ".`id`"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Converts a (possibly plural) table name to its foreign-key column
|
|
57
|
+
# name wrapped in backticks.
|
|
58
|
+
# "users" → "`user_id`"
|
|
59
|
+
# "categories" → "`category_id`"
|
|
60
|
+
# "admins" → "`admin_id`"
|
|
61
|
+
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`"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/json2sql.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative "json2sql/version"
|
|
2
|
+
require_relative "json2sql/sanitizer"
|
|
3
|
+
require_relative "json2sql/where_relation"
|
|
4
|
+
require_relative "json2sql/where_model"
|
|
5
|
+
require_relative "json2sql/select_model"
|
|
6
|
+
require_relative "json2sql/select_runner"
|
|
7
|
+
require_relative "json2sql/insert_model"
|
|
8
|
+
require_relative "json2sql/insert_runner"
|
|
9
|
+
require_relative "json2sql/update_model"
|
|
10
|
+
require_relative "json2sql/update_runner"
|
|
11
|
+
require_relative "json2sql/delete_model"
|
|
12
|
+
require_relative "json2sql/delete_runner"
|
|
13
|
+
|
|
14
|
+
# Json2sql — SQL builder that generates MySQL/MariaDB query strings from
|
|
15
|
+
# plain Ruby Hashes (or parsed JSON).
|
|
16
|
+
#
|
|
17
|
+
# All Hash keys may be either Strings or Symbols; they are normalized to
|
|
18
|
+
# Strings internally before processing.
|
|
19
|
+
#
|
|
20
|
+
# Entry points:
|
|
21
|
+
# Json2sql::SelectRunner.build(hash) → String
|
|
22
|
+
# Json2sql::InsertRunner.build(hash) → String
|
|
23
|
+
# Json2sql::UpdateRunner.build(hash) → String
|
|
24
|
+
# Json2sql::DeleteRunner.build(hash) → String
|
|
25
|
+
module Json2sql
|
|
26
|
+
# Deep-converts all Hash keys to Strings and recurses into nested Hashes
|
|
27
|
+
# and Arrays. Leaves all other values (Integers, Strings, etc.) unchanged.
|
|
28
|
+
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
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: json2sql
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tiago da Silva
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
description: Pure-Ruby SQL builder. No runtime dependencies. Supports SELECT (with
|
|
42
|
+
JSON aggregation and nesting), INSERT, UPDATE, and DELETE for MySQL/MariaDB.
|
|
43
|
+
email:
|
|
44
|
+
- tyagoy@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE.txt
|
|
50
|
+
- lib/json2sql.rb
|
|
51
|
+
- lib/json2sql/delete_model.rb
|
|
52
|
+
- lib/json2sql/delete_runner.rb
|
|
53
|
+
- lib/json2sql/insert_model.rb
|
|
54
|
+
- lib/json2sql/insert_runner.rb
|
|
55
|
+
- lib/json2sql/sanitizer.rb
|
|
56
|
+
- lib/json2sql/select_model.rb
|
|
57
|
+
- lib/json2sql/select_runner.rb
|
|
58
|
+
- lib/json2sql/update_model.rb
|
|
59
|
+
- lib/json2sql/update_runner.rb
|
|
60
|
+
- lib/json2sql/version.rb
|
|
61
|
+
- lib/json2sql/where_model.rb
|
|
62
|
+
- lib/json2sql/where_relation.rb
|
|
63
|
+
homepage: https://github.com/tyagoy/json2sql
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
homepage_uri: https://github.com/tyagoy/json2sql
|
|
68
|
+
source_code_uri: https://github.com/tyagoy/json2sql
|
|
69
|
+
post_install_message:
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '2.7'
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 3.4.10
|
|
85
|
+
signing_key:
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Translates Ruby Hashes (or parsed JSON) into MySQL/MariaDB query strings.
|
|
88
|
+
test_files: []
|