quote-sql 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/lib/quote_sql/error.rb +47 -0
- data/lib/quote_sql/formater.rb +7 -5
- data/lib/quote_sql/quoter.rb +165 -124
- data/lib/quote_sql/test.rb +163 -128
- data/lib/quote_sql/version.rb +3 -0
- data/lib/quote_sql.rb +55 -134
- metadata +20 -5
- data/lib/quote_sql/deprecated.rb +0 -162
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70f787b04560e3b297b4a392eb72c0e85be57e349a0533366056fe485f339401
|
4
|
+
data.tar.gz: ad8a2e347c97e12b78a264e67d0e12649e838127a0079d8ff96bf5ac51764b40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d2a6819ad0d7fc752a70a04efb48c7583759a1b6cd07eb93c0bacd5deec9845b4abf578ae902c94aaf68ba4cf41720b1f05aea8dc6950b468c8cc8f42c34c42
|
7
|
+
data.tar.gz: e84a311154ef05f71b8af81c17cac1b4923f067fb48556f501aa075ab8fca1c112d494c8b0b58f1c8350d7d7467c10ba99f74591755beb206415dbe78756c7ff
|
data/README.md
CHANGED
@@ -127,7 +127,11 @@ with optional array dimension
|
|
127
127
|
## Debug and dump
|
128
128
|
If you have pg_format installed you can get the resulting query inspected:
|
129
129
|
`QuoteSql.new("select %abc").quote(abc: 1).dsql`
|
130
|
-
|
130
|
+
|
131
|
+
# Test
|
132
|
+
Minimal tests you can run by
|
133
|
+
`QuoteSql.test.all`
|
134
|
+
You can find them in /lib/quote_sql/test.rb
|
131
135
|
|
132
136
|
## Installing
|
133
137
|
`gem install quote-sql`
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class QuoteSql
|
2
|
+
class Error < ::RuntimeError
|
3
|
+
def initialize(quote_sql, errors)
|
4
|
+
@object = quote_sql
|
5
|
+
@errors = errors
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :object, :errors
|
9
|
+
|
10
|
+
def original
|
11
|
+
@object.original.dsql
|
12
|
+
end
|
13
|
+
|
14
|
+
def sql
|
15
|
+
@object.sql.dsql
|
16
|
+
end
|
17
|
+
|
18
|
+
def tables
|
19
|
+
@object.tables.inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
def columns
|
23
|
+
@object.columns.inspect
|
24
|
+
end
|
25
|
+
|
26
|
+
def quotes
|
27
|
+
@object.quotes.inspect
|
28
|
+
end
|
29
|
+
|
30
|
+
def message
|
31
|
+
|
32
|
+
errors = @object.errors.map do |quote, error|
|
33
|
+
error => {exc:, backtrace:, **transformations}
|
34
|
+
|
35
|
+
"#{quote}: #{exc.class} #{exc.message} #{transformations.inspect}\n#{backtrace.join("\n")}"
|
36
|
+
end
|
37
|
+
<<~ERROR
|
38
|
+
Original: #{original}
|
39
|
+
Tables:
|
40
|
+
Quotes: #{quotes}
|
41
|
+
Processed: #{sql}
|
42
|
+
#{errors.join("\n\n")}
|
43
|
+
#{'*' * 40}
|
44
|
+
ERROR
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/quote_sql/formater.rb
CHANGED
@@ -9,11 +9,13 @@ class QuoteSql
|
|
9
9
|
|
10
10
|
def to_formatted_sql
|
11
11
|
sql = respond_to?(:to_sql) ? to_sql : to_s
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
Niceql::Prettifier.prettify_sql(sql)
|
13
|
+
|
14
|
+
# IO.popen(PG_FORMAT_BIN, "r+", err: "/dev/null") do |f|
|
15
|
+
# f.write(sql)
|
16
|
+
# f.close_write
|
17
|
+
# f.read
|
18
|
+
# end
|
17
19
|
rescue
|
18
20
|
sql
|
19
21
|
end
|
data/lib/quote_sql/quoter.rb
CHANGED
@@ -3,35 +3,94 @@ class QuoteSql
|
|
3
3
|
def initialize(qsql, key, quotable)
|
4
4
|
@qsql = qsql
|
5
5
|
@key, @quotable = key, quotable
|
6
|
+
@name = key.sub(/_[^_]+$/, '') if key["_"]
|
6
7
|
end
|
7
8
|
|
9
|
+
attr_reader :key, :quotable, :name
|
10
|
+
|
8
11
|
def quotes
|
9
12
|
@qsql.quotes
|
10
13
|
end
|
11
14
|
|
12
|
-
|
15
|
+
def table(name = nil)
|
16
|
+
@qsql.table(name || self.name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def ident_table(i = nil)
|
20
|
+
Raw.sql(Array(self.table(name)).compact[0..i].map do |table|
|
21
|
+
if table.respond_to? :table_name
|
22
|
+
QuoteSql.quote_column_name table.table_name
|
23
|
+
elsif table.present?
|
24
|
+
QuoteSql.quote_column_name table
|
25
|
+
end
|
26
|
+
end.join(","))
|
27
|
+
end
|
28
|
+
|
29
|
+
def columns(name = nil)
|
30
|
+
@qsql.columns(name || self.name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def casts(name = nil)
|
34
|
+
@qsql.casts(name || self.name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def ident_columns(name = nil)
|
38
|
+
item = columns(name || self.name)
|
39
|
+
unless item
|
40
|
+
table = self.table(name || self.name)
|
41
|
+
raise ArgumntError, "No columns or table given" unless table&.respond_to? :column_names
|
42
|
+
item = table.column_names
|
43
|
+
end
|
44
|
+
if item.is_a?(Array)
|
45
|
+
if item.all? { _1.respond_to?(:name) }
|
46
|
+
item = item.map(&:name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
_ident(item)
|
50
|
+
end
|
13
51
|
|
14
|
-
def
|
15
|
-
|
52
|
+
def _quote_ident(item)
|
53
|
+
Raw.sql case item.class.to_s
|
54
|
+
when "QuoteSql::Raw", "Arel::Nodes::SqlLiteral" then item
|
55
|
+
when "Hash" then json_hash_ident(item)
|
56
|
+
when "Array" then json_array_ident(item)
|
57
|
+
when "Proc" then item.call(self)
|
58
|
+
when "Integer" then "$#{item}"
|
59
|
+
when "Symbol" then [ident_table(0).presence, _quote_ident(item.to_s)].compact.join(".")
|
60
|
+
when "String" then item.scan(/(?:^|")?([^."]+)/).flatten.map { QuoteSql.quote_column_name _1 }.join(".")
|
61
|
+
else raise ArgumentError, "just Hash, Array, Arel::Nodes::SqlLiteral, QuoteSql::Raw, String, Symbol, Proc, Integer, or responding to #to_sql"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def _ident(item = @quotable)
|
66
|
+
return Raw.sql(item) if item.respond_to?(:to_sql)
|
67
|
+
rv = case item.class.to_s
|
68
|
+
when "Array"
|
69
|
+
item.map { _1.is_a?(Hash) ? _ident(_1) : _quote_ident(_1) }.join(",")
|
70
|
+
when "Hash"
|
71
|
+
item.map { "#{_quote_ident(_2)} AS \"#{_1}\"" }.join(",")
|
72
|
+
else
|
73
|
+
_quote_ident(item)
|
74
|
+
# _quote_column_name(item)
|
75
|
+
end
|
76
|
+
Raw.sql rv
|
16
77
|
end
|
17
78
|
|
18
79
|
def to_sql
|
19
80
|
return @quotable.call(self) if @quotable.is_a? Proc
|
20
81
|
case key.to_s
|
21
82
|
when /(?:^|(.*)_)table$/i
|
22
|
-
|
23
|
-
when /(?:^|(.*)_)columns
|
24
|
-
|
25
|
-
when /(?:^|(.*)_)(
|
26
|
-
|
27
|
-
when /(?:^|(.*)_)(column_name?s?)$/i
|
28
|
-
ident_name
|
29
|
-
when /(?:^|(.*)_)(ident|args)$/i
|
30
|
-
ident_name
|
83
|
+
ident_table
|
84
|
+
when /(?:^|(.*)_)columns$/i
|
85
|
+
ident_columns
|
86
|
+
when /(?:^|(.*)_)(ident)$/i
|
87
|
+
_ident
|
31
88
|
when /(?:^|(.*)_)constraints?$/i
|
32
|
-
quotable
|
89
|
+
quotable
|
33
90
|
when /(?:^|(.*)_)(raw|sql)$/i
|
34
|
-
quotable
|
91
|
+
quotable
|
92
|
+
when /^(.+)_json$/i
|
93
|
+
data_json
|
35
94
|
when /^(.+)_values$/i
|
36
95
|
data_values
|
37
96
|
when /values$/i
|
@@ -41,20 +100,34 @@ class QuoteSql
|
|
41
100
|
end
|
42
101
|
end
|
43
102
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
103
|
+
###############
|
104
|
+
|
105
|
+
private def value(values)
|
106
|
+
# case values.class.to_s
|
107
|
+
# when "QuoteSql::Raw", "Arel::Nodes::SqlLiteral" then rv = values
|
108
|
+
# when "Array"
|
109
|
+
# when "Hash"
|
110
|
+
# columns = self.columns(name)&.flat_map { _1.is_a?(Hash) ? _1.values : _1 }
|
111
|
+
# if columns.nil?
|
112
|
+
# values = values.values
|
113
|
+
# elsif columns.all? { _1.is_a? Symbol }
|
114
|
+
# raise ArgumentError, "Columns just Symbols"
|
115
|
+
# else
|
116
|
+
# values = columns.map do |column|
|
117
|
+
# if values.key?(column&.to_sym) or !defaults
|
118
|
+
# values[column.to_sym]
|
119
|
+
# elsif column[/^(created|updated)_at$/]
|
120
|
+
# :current_timestamp
|
121
|
+
# else
|
122
|
+
# :default
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
# else
|
127
|
+
# raise ArgumentError, "value just Array, Hash, QuoteSql::Raw, Arel::Nodes::SqlLiteral"
|
128
|
+
# end
|
129
|
+
|
130
|
+
rv ||= values.map do |i|
|
58
131
|
case i
|
59
132
|
when :default, :current_timestamp
|
60
133
|
next i.to_s.upcase
|
@@ -62,13 +135,22 @@ class QuoteSql
|
|
62
135
|
i = i.to_json
|
63
136
|
end
|
64
137
|
_quote(i)
|
65
|
-
end
|
138
|
+
end
|
139
|
+
Raw.sql "(#{rv.join(",")})"
|
140
|
+
end
|
66
141
|
|
142
|
+
def data_json(item = @quotable)
|
143
|
+
casts = self.casts(name)
|
144
|
+
columns = self.columns(name) || casts&.keys
|
145
|
+
column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
|
146
|
+
item = [item].flatten.compact.as_json.map{_1.slice(*columns.map(&:to_s))}
|
147
|
+
Raw.sql "json_to_recordset('#{item.to_json.gsub(/'/,"''")}') AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
|
67
148
|
end
|
68
149
|
|
150
|
+
|
69
151
|
def data_values(item = @quotable)
|
70
152
|
item = Array(item).compact
|
71
|
-
column_names =
|
153
|
+
column_names = columns(name)
|
72
154
|
if column_names.is_a? Hash
|
73
155
|
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
74
156
|
column_names = column_names.keys
|
@@ -80,7 +162,7 @@ class QuoteSql
|
|
80
162
|
if item.all? { _1.is_a?(Array) }
|
81
163
|
length, overflow = item.map { _1.length }.uniq
|
82
164
|
raise ArgumentError, "all values need to have the same length" if overflow
|
83
|
-
column_names ||= (1..length).map{"column#{_1}"}
|
165
|
+
column_names ||= (1..length).map { "column#{_1}" }
|
84
166
|
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
|
85
167
|
values = item.map { value(_1) }
|
86
168
|
else
|
@@ -88,18 +170,17 @@ class QuoteSql
|
|
88
170
|
end
|
89
171
|
if types.present?
|
90
172
|
value = values[0][1..-2].split(/\s*,\s*/)
|
91
|
-
types.each_with_index { value[_2] << _1 || ""}
|
173
|
+
types.each_with_index { value[_2] << _1 || "" }
|
92
174
|
values[0] = "(" + value.join(",") + ")"
|
93
175
|
end
|
94
176
|
# values[0] { _1 << types[_1] || ""}
|
95
|
-
"(VALUES #{values.join(",")}) AS #{
|
177
|
+
Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
|
96
178
|
end
|
97
179
|
|
98
|
-
|
99
180
|
def insert_values(item = @quotable)
|
100
181
|
case item
|
101
182
|
when Arel::Nodes::SqlLiteral
|
102
|
-
item =
|
183
|
+
item = Raw.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
|
103
184
|
return item
|
104
185
|
when Array
|
105
186
|
item.compact!
|
@@ -129,9 +210,9 @@ class QuoteSql
|
|
129
210
|
raise ArgumentError, "Either all type Hash or Array"
|
130
211
|
end
|
131
212
|
if column_names.present?
|
132
|
-
"(#{
|
213
|
+
Raw.sql "(#{_ident column_names}) VALUES #{values.join(",")}"
|
133
214
|
else
|
134
|
-
"VALUES #{values.join(",")}"
|
215
|
+
Raw.sql "VALUES #{values.join(",")}"
|
135
216
|
end
|
136
217
|
when Hash
|
137
218
|
value([item])
|
@@ -151,51 +232,31 @@ class QuoteSql
|
|
151
232
|
private def _quote(item = @quotable, cast = self.cast)
|
152
233
|
rv = QuoteSql.quote(item)
|
153
234
|
if cast
|
154
|
-
rv << "::#{cast}"
|
235
|
+
rv << "::#{cast.upcase}"
|
155
236
|
rv << "[]" * rv.depth if rv[/^ARRAY/]
|
156
237
|
end
|
157
|
-
rv
|
238
|
+
Raw.sql rv
|
158
239
|
end
|
159
240
|
|
160
|
-
private def _quote_column_name(name
|
161
|
-
name
|
162
|
-
rv = QuoteSql.quote_column_name(name)
|
163
|
-
return rv unless column.present?
|
164
|
-
rv + "." + QuoteSql.quote_column_name(column)
|
241
|
+
private def _quote_column_name(name)
|
242
|
+
Raw.sql name.scan(/(?:^|")?([^."]+)/).map { QuoteSql.quote_column_name _1 }.join(".")
|
165
243
|
end
|
166
244
|
|
167
245
|
def quote(item = @quotable)
|
168
|
-
case item
|
169
|
-
when Arel::Nodes::SqlLiteral
|
170
|
-
return item
|
171
|
-
when Array
|
246
|
+
case item.class.to_s
|
247
|
+
when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
|
248
|
+
return Raw.sql(item)
|
249
|
+
when "Array"
|
172
250
|
return _quote(item.to_json) if json?
|
173
251
|
_quote(item)
|
174
|
-
when Hash
|
175
|
-
|
176
|
-
item.map do |as, item|
|
177
|
-
"#{_quote(item)} AS #{as}"
|
178
|
-
end.join(",")
|
252
|
+
when "Hash"
|
253
|
+
_quote(item.to_json, :jsonb)
|
179
254
|
else
|
180
|
-
return item.to_sql if item.respond_to? :to_sql
|
255
|
+
return Raw.sql item.to_sql if item.respond_to? :to_sql
|
181
256
|
_quote(item)
|
182
257
|
end
|
183
258
|
end
|
184
259
|
|
185
|
-
def columns(item = @quotable)
|
186
|
-
if item.respond_to?(:column_names)
|
187
|
-
item = item.column_names
|
188
|
-
elsif item.class.respond_to?(:column_names)
|
189
|
-
item = item.class.column_names
|
190
|
-
elsif item.is_a?(Array)
|
191
|
-
if item.all?{ _1.respond_to?(:name) }
|
192
|
-
item = item.map(&:name)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
@qsql.column_names ||= item
|
196
|
-
ident_name(item)
|
197
|
-
end
|
198
|
-
|
199
260
|
def column_names(item = @quotable)
|
200
261
|
if item.respond_to?(:column_names)
|
201
262
|
item = item.column_names
|
@@ -205,70 +266,50 @@ class QuoteSql
|
|
205
266
|
item = item.map(&:name)
|
206
267
|
end
|
207
268
|
@qsql.column_names ||= item
|
208
|
-
|
269
|
+
_ident(item)
|
209
270
|
end
|
210
271
|
|
211
|
-
def
|
212
|
-
|
213
|
-
rv = "jsonb_build_object(" + h.map { "'#{_1}',#{_2}" }.join(",") + ")"
|
214
|
-
return rv unless compact
|
215
|
-
"jsonb_strip_nulls(#{rv})"
|
272
|
+
def json_array_values(h)
|
273
|
+
Raw.sql "'#{h.to_json.gsub(/'/, "''")}'::JSONB"
|
216
274
|
end
|
217
275
|
|
218
|
-
def
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
case item
|
223
|
-
when Hash
|
224
|
-
ident_name(item)
|
225
|
-
when String, Symbol
|
226
|
-
_quote_column_name(item)
|
227
|
-
when Proc
|
228
|
-
item.call(self)
|
229
|
-
end
|
230
|
-
end.join(",")
|
231
|
-
when Hash
|
232
|
-
item.map do |k,v|
|
233
|
-
case v
|
234
|
-
when Symbol
|
235
|
-
_quote_column_name(k, v)
|
236
|
-
when String
|
237
|
-
"#{v} AS #{k}"
|
238
|
-
when Proc
|
239
|
-
item.call(self)
|
240
|
-
when Hash
|
241
|
-
"#{json_build_object(v)} AS #{k}"
|
242
|
-
else
|
243
|
-
raise ArgumentError
|
244
|
-
end
|
245
|
-
end.join(",")
|
246
|
-
else
|
247
|
-
_quote_column_name(item)
|
248
|
-
end
|
276
|
+
def json_hash_values(h)
|
277
|
+
compact = h.delete(nil) == false
|
278
|
+
rv = json_array_values(h)
|
279
|
+
Raw.sql(compact ? "jsonb_strip_nulls(#{rv})" : rv)
|
249
280
|
end
|
250
281
|
|
251
|
-
def
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
item = item.class.table_name
|
256
|
-
end
|
257
|
-
table_name(item || @qsql.table_name)
|
282
|
+
def json_hash_ident(h)
|
283
|
+
compact = h.delete(nil) == false
|
284
|
+
rv = "jsonb_build_object(" + h.map { "'#{_1.to_s.gsub(/'/, "''")}', #{_ident(_2)}" }.join(",") + ")"
|
285
|
+
Raw.sql(compact ? "jsonb_strip_nulls(#{rv})" : rv)
|
258
286
|
end
|
259
287
|
|
260
|
-
def
|
261
|
-
|
262
|
-
when Array
|
263
|
-
item.map do |item|
|
264
|
-
item.is_a?(Hash) ? table_name(item) : _quote_column_name(item)
|
265
|
-
end.join(",")
|
266
|
-
when Hash
|
267
|
-
raise NotImplementedError, "table name is a Hash"
|
268
|
-
# perhaps as ...
|
269
|
-
else
|
270
|
-
_quote_column_name(item)
|
271
|
-
end
|
288
|
+
def json_array_ident(h)
|
289
|
+
Raw.sql "jsonb_build_array(#{h.map { _ident(_2) }.join(",")})"
|
272
290
|
end
|
291
|
+
|
292
|
+
# def table(item = @quotable)
|
293
|
+
# @qsql.table_name ||= if item.respond_to?(:table_name)
|
294
|
+
# item = item.table_name
|
295
|
+
# elsif item.class.respond_to?(:table_name)
|
296
|
+
# item = item.class.table_name
|
297
|
+
# end
|
298
|
+
# table_name(item || @qsql.table_name)
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# def table_name(item = @quotable)
|
302
|
+
# case item
|
303
|
+
# when Array
|
304
|
+
# item.map do |item|
|
305
|
+
# item.is_a?(Hash) ? table_name(item) : _quote_column_name(item)
|
306
|
+
# end.join(",")
|
307
|
+
# when Hash
|
308
|
+
# raise NotImplementedError, "table name is a Hash"
|
309
|
+
# # perhaps as ...
|
310
|
+
# else
|
311
|
+
# _quote_column_name(item)
|
312
|
+
# end
|
313
|
+
# end
|
273
314
|
end
|
274
315
|
end
|
data/lib/quote_sql/test.rb
CHANGED
@@ -1,35 +1,40 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
class QuoteSql::Test
|
2
|
+
|
3
|
+
def all
|
3
4
|
@success = []
|
4
5
|
@fail = []
|
5
|
-
|
6
|
-
run(name, true)
|
7
|
-
end
|
6
|
+
private_methods(false).grep(/^test_/).each { run(_1, true) }
|
8
7
|
@success.each { STDOUT.puts(*_1, nil) }
|
9
8
|
@fail.each { STDOUT.puts(*_1, nil) }
|
10
9
|
puts
|
11
10
|
end
|
12
11
|
|
13
|
-
def
|
12
|
+
def run(name, all = false)
|
14
13
|
name = name.to_s.sub(/^test_/, "")
|
14
|
+
rv = ["🧪 #{name}"]
|
15
15
|
@expected = nil
|
16
16
|
@test = send("test_#{name}")
|
17
|
-
|
18
17
|
if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
|
19
|
-
|
18
|
+
tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
|
19
|
+
columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
|
20
|
+
rv += [@test.original, { **tables, **columns, **@test.quotes }.inspect, "🎯 #{expected}", "✅ #{sql}"]
|
20
21
|
@success << rv if @success
|
21
22
|
else
|
22
|
-
rv
|
23
|
+
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
|
23
24
|
@fail << rv if @fail
|
24
25
|
end
|
25
|
-
|
26
|
+
rescue => exc
|
27
|
+
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}", exc.message]
|
28
|
+
@fail << rv if @fail
|
29
|
+
ensure
|
30
|
+
STDOUT.puts(*rv) unless @fail or @success
|
26
31
|
end
|
27
32
|
|
28
|
-
def
|
33
|
+
def expected(v = nil)
|
29
34
|
@expected ||= v
|
30
35
|
end
|
31
36
|
|
32
|
-
def
|
37
|
+
def sql
|
33
38
|
@test.to_sql
|
34
39
|
end
|
35
40
|
|
@@ -47,134 +52,164 @@ module QuoteSql::Test
|
|
47
52
|
end
|
48
53
|
end
|
49
54
|
|
50
|
-
|
51
|
-
def test_columns_and_table_name_simple
|
52
|
-
expected %(SELECT "a","b"."c" FROM "my_table")
|
53
|
-
QuoteSql.new("SELECT %columns FROM %table_name").quote(
|
54
|
-
columns: [:a, b: :c],
|
55
|
-
table_name: "my_table"
|
56
|
-
)
|
57
|
-
end
|
55
|
+
private
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
57
|
+
def test_columns
|
58
|
+
expected <<~SQL
|
59
|
+
SELECT x, "a", "b", "c", "d"
|
60
|
+
SQL
|
61
|
+
"SELECT x, %x_columns ".quote_sql(x_columns: %i[a b c d])
|
62
|
+
end
|
66
63
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
64
|
+
def test_columns_and_table_name_simple
|
65
|
+
expected <<~SQL
|
66
|
+
SELECT "my_table"."a", "b", "gaga"."c", "my_table"."e" AS "d", "gaga"."d" AS "f", 1 + 2 AS "g", whatever AS raw FROM "my_table"
|
67
|
+
SQL
|
68
|
+
QuoteSql.new("SELECT %columns FROM %table").quote(
|
69
|
+
columns: [:a, "b", "gaga.c", { d: :e, f: "gaga.d", g: Arel.sql("1 + 2") }, Arel.sql("whatever AS raw")],
|
70
|
+
table: "my_table"
|
71
|
+
)
|
72
|
+
end
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
},
|
87
|
-
table_name: "my_table"
|
88
|
-
)
|
89
|
-
end
|
74
|
+
def test_columns_and_table_name_complex
|
75
|
+
expected <<~SQL
|
76
|
+
SELECT "table1"."a","table1"."c" as "b" FROM "table1","table2"
|
77
|
+
SQL
|
78
|
+
QuoteSql.new("SELECT %columns FROM %table").quote(
|
79
|
+
columns: [:a, b: :c],
|
80
|
+
table: ["table1", "table2"]
|
81
|
+
)
|
82
|
+
end
|
90
83
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
84
|
+
def test_recursive_injects
|
85
|
+
expected %(SELECT TRUE FROM "table1")
|
86
|
+
QuoteSql.new("SELECT %raw FROM %table").quote(
|
87
|
+
raw: "%recurse1_raw",
|
88
|
+
recurse1_raw: "%recurse2",
|
89
|
+
recurse2: true,
|
90
|
+
table: "table1"
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_values
|
95
|
+
expected <<~SQL
|
96
|
+
SELECT 'a text', 123, '{"abc":"don''t"}'::jsonb FROM "my_table"
|
97
|
+
SQL
|
98
|
+
QuoteSql.new("SELECT %text, %{number}, %hash FROM %table").quote(
|
99
|
+
text: "a text",
|
100
|
+
number: 123,
|
101
|
+
hash: { abc: "don't" },
|
102
|
+
table: "my_table"
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_binds
|
107
|
+
expected <<~SQL
|
108
|
+
SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
|
109
|
+
SQL
|
110
|
+
QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
|
111
|
+
table: "my_table"
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_from_values_array
|
116
|
+
expected <<~SQL
|
117
|
+
SELECT * FROM (VALUES ('a',1,TRUE,NULL)) AS "x" ("column1","column2","column3","column4")
|
118
|
+
SQL
|
119
|
+
"SELECT * FROM %x_values".quote_sql(x_values: [['a', 1, true, nil]])
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_from_values_hash_no_columns
|
123
|
+
expected <<~SQL
|
124
|
+
SELECT * FROM (VALUES ('a', 1, true, NULL), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "y" ("a", "b", "c", "d")
|
125
|
+
SQL
|
126
|
+
"SELECT * FROM %y_values".quote_sql(y_values: [
|
127
|
+
{ a: 'a', b: 1, c: true, d: nil },
|
128
|
+
{ d: nil, a: 'a', c: true, b: 1 },
|
129
|
+
{ d: 2, b: 1 }
|
130
|
+
])
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_from_values_hash_with_columns
|
134
|
+
expected <<~SQL
|
135
|
+
SELECT * FROM (VALUES (NULL, true, 1, 'a')) AS "x" ("d","c","b","a")
|
136
|
+
SQL
|
137
|
+
"SELECT * FROM %x_values".quote_sql(x_columns: %i[d c b a], x_values: [{ a: 'a', b: 1, c: true, d: nil }])
|
138
|
+
end
|
106
139
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
140
|
+
def test_from_values_hash_with_type_columns
|
141
|
+
expected <<~SQL
|
142
|
+
SELECT *
|
143
|
+
FROM (VALUES
|
144
|
+
('a'::TEXT, 1::INTEGER, true::BOOLEAN, NULL::FLOAT),
|
145
|
+
('a', 1, true, NULL),
|
146
|
+
(NULL, 1, NULL, 2)
|
147
|
+
) AS "x" ("a", "b", "c", "d")
|
148
|
+
SQL
|
149
|
+
"SELECT * FROM %x_values".quote_sql(
|
150
|
+
x_columns: {
|
151
|
+
a: "text",
|
152
|
+
b: "integer",
|
153
|
+
c: "boolean",
|
154
|
+
d: "float"
|
155
|
+
},
|
156
|
+
x_values: [
|
112
157
|
{ a: 'a', b: 1, c: true, d: nil },
|
113
158
|
{ d: nil, a: 'a', c: true, b: 1 },
|
114
159
|
{ d: 2, b: 1 }
|
115
160
|
])
|
116
|
-
|
117
|
-
|
118
|
-
def test_from_values_hash_with_columns
|
119
|
-
expected <<~SQL
|
120
|
-
SELECT * FROM (VALUES (NULL, true, 1, 'a')) AS "x" ("d","c","b","a")
|
121
|
-
SQL
|
122
|
-
"SELECT * FROM %x_values".quote_sql(x_columns: %i[d c b a], x_values: [{ a: 'a', b: 1, c: true, d: nil }])
|
123
|
-
end
|
161
|
+
end
|
124
162
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
a: "text",
|
132
|
-
b: "integer",
|
133
|
-
c: "boolean",
|
134
|
-
d: "float"
|
135
|
-
},
|
136
|
-
x_values: [
|
137
|
-
{ a: 'a', b: 1, c: true, d: nil },
|
138
|
-
{ d: nil, a: 'a', c: true, b: 1 },
|
139
|
-
{ d: 2, b: 1 }
|
140
|
-
])
|
141
|
-
end
|
163
|
+
def test_insert_values_array
|
164
|
+
expected <<~SQL
|
165
|
+
INSERT INTO x VALUES ('a', 1, true, NULL)
|
166
|
+
SQL
|
167
|
+
"INSERT INTO x %values".quote_sql(values: [['a', 1, true, nil]])
|
168
|
+
end
|
142
169
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
170
|
+
def test_insert_values_hash
|
171
|
+
expected <<~SQL
|
172
|
+
INSERT INTO x ("a", "b", "c", "d") VALUES ('a', 1, true, NULL)
|
173
|
+
SQL
|
174
|
+
"INSERT INTO x %values".quote_sql(values: [{ a: 'a', b: 1, c: true, d: nil }])
|
175
|
+
end
|
149
176
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
177
|
+
def test_from_json
|
178
|
+
expected <<~SQL
|
179
|
+
SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"},{"a":"2"}]') as "x" ("a" int, "b" text)
|
180
|
+
SQL
|
181
|
+
"SELECT * FROM %x_json".quote_sql(x_casts: {a: "int", b: "text"}, x_json: [{ a: 1, b: 'foo'}, {a: '2', c: 'bar'}])
|
182
|
+
end
|
156
183
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
#
|
162
|
-
|
163
|
-
# ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
|
164
|
-
# SQL
|
165
|
-
#
|
166
|
-
# QuoteSql.new(<<-SQL).
|
167
|
-
# INSERT INTO %table (%columns) VALUES %values
|
168
|
-
# ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
|
169
|
-
# SQL
|
170
|
-
# quote(
|
171
|
-
# table: Response,
|
172
|
-
# values: [
|
173
|
-
# [nil, true, "A", [5, 5], { a: 1 }],
|
174
|
-
# [1, false, "B", [], { a: 2 }],
|
175
|
-
# [2, nil, "c", [1, 2, 3], { a: 3 }]
|
176
|
-
# ]
|
177
|
-
# )
|
178
|
-
# end
|
184
|
+
def test_json_insert
|
185
|
+
expected <<~SQL
|
186
|
+
INSERT INTO users (name, color) SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "x"("name" text,"color" text)
|
187
|
+
SQL
|
188
|
+
x_json = {"first_name"=>nil, "last_name"=>nil, "stripe_id"=>nil, "credits"=>nil, "avatar"=>nil, "name"=>"auge", "color"=>"#611333", "founder"=>nil, "language"=>nil, "country"=>nil, "data"=>{}, "created_at"=>"2020-11-19T09:30:18.670Z", "updated_at"=>"2020-11-19T09:40:00.063Z"}
|
189
|
+
"INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: {name: "text", color: "text"}, x_json:)
|
179
190
|
end
|
191
|
+
|
192
|
+
# def test_q3
|
193
|
+
# expected Arel.sql(<<-SQL)
|
194
|
+
# INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
|
195
|
+
# VALUES (NULL,TRUE,'A','[5,5]','{"a":1}'),
|
196
|
+
# (1,FALSE,'B','[]','{"a":2}'),
|
197
|
+
# (2,NULL,'c','[1,2,3]','{"a":3}')
|
198
|
+
# ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
|
199
|
+
# SQL
|
200
|
+
#
|
201
|
+
# QuoteSql.new(<<-SQL).
|
202
|
+
# INSERT INTO %table (%columns) VALUES %values
|
203
|
+
# ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
|
204
|
+
# SQL
|
205
|
+
# quote(
|
206
|
+
# table: Response,
|
207
|
+
# values: [
|
208
|
+
# [nil, true, "A", [5, 5], { a: 1 }],
|
209
|
+
# [1, false, "B", [], { a: 2 }],
|
210
|
+
# [2, nil, "c", [1, 2, 3], { a: 3 }]
|
211
|
+
# ]
|
212
|
+
# )
|
213
|
+
# end
|
214
|
+
|
180
215
|
end
|
data/lib/quote_sql.rb
CHANGED
@@ -1,90 +1,8 @@
|
|
1
1
|
Dir.glob(__FILE__.sub(/\.rb$/, "/*.rb")).each { require(_1) unless _1[/(deprecated|test)\.rb$/] }
|
2
2
|
|
3
3
|
# Tool to build and run SQL queries easier
|
4
|
-
#
|
5
|
-
# QuoteSql.new("SELECT %field").quote(field: "abc").to_sql
|
6
|
-
# => SELECT 'abc'
|
7
|
-
#
|
8
|
-
# QuoteSql.new("SELECT %field__text").quote(field__text: 9).to_sql
|
9
|
-
# => SELECT 9::TEXT
|
10
|
-
#
|
11
|
-
# QuoteSql.new("SELECT %columns FROM %table_name").quote(table: User).to_sql
|
12
|
-
# => SELECT "id",firstname","lastname",... FROM "users"
|
13
|
-
#
|
14
|
-
# QuoteSql.new("SELECT a,b,%raw FROM table").quote(raw: "jsonb_build_object('a', 1)").to_sql
|
15
|
-
# => SELECT "a,b,jsonb_build_object('a', 1) FROM table
|
16
|
-
#
|
17
|
-
# QuoteSql.new("SELECT %column_names FROM (%any_name) a").
|
18
|
-
# quote(any_name: User.select("%column_names").where(id: 3), column_names: [:firstname, :lastname]).to_sql
|
19
|
-
# => SELECT firstname, lastname FROM (SELECT firstname, lastname FROM users where id = 3)
|
20
|
-
#
|
21
|
-
# QuoteSql.new("INSERT INTO %table (%columns) VALUES %values ON CONFLICT (%constraint) DO NOTHING").
|
22
|
-
# quote(table: User, values: [
|
23
|
-
# {firstname: "Albert", id: 1, lastname: "Müller"},
|
24
|
-
# {lastname: "Schultz", firstname: "herbert"}
|
25
|
-
# ], constraint: :id).to_sql
|
26
|
-
# => INSERT INTO "users" ("id", "firstname", "lastname", "created_at")
|
27
|
-
# VALUES (1, 'Albert', 'Müller', CURRENT_TIMESTAMP), (DEFAULT, 'herbert', 'Schultz', CURRENT_TIMESTAMP)
|
28
|
-
# ON CONFLICT ("id") DO NOTHING
|
29
|
-
#
|
30
|
-
# QuoteSql.new("SELECT %columns").quote(columns: [:a, :"b.c", c: "jsonb_build_object('d', 1)"]).to_sql
|
31
|
-
# => SELECT "a","b"."c",jsonb_build_object('d', 1) AS c
|
32
|
-
#
|
33
|
-
# Substitution
|
34
|
-
# In the SQL matches of %foo or %{foo} or %foo_4_bar or %{foo_4_bar} the *"mixins"*
|
35
|
-
# are substituted with quoted values
|
36
|
-
# the values are looked up from the options given in the quotes method
|
37
|
-
# the mixins can be recursive, Caution! You need to take care, you can create infintive loops!
|
38
|
-
#
|
39
|
-
# Special mixins are
|
40
|
-
# - %table | %table_name | %table_names
|
41
|
-
# - %column | %columns | %column_names
|
42
|
-
# - %ident | %constraint | %constraints quoting for database columns
|
43
|
-
# - %raw | %sql inserting raw SQL
|
44
|
-
# - %value | %values creates value section for e.g. insert
|
45
|
-
# - In the right order
|
46
|
-
# - Single value => (2)
|
47
|
-
# - +Array+ => (column, column, column) n.b. has to be the correct order
|
48
|
-
# - +Array+ of +Array+ => (...),(...),(...),...
|
49
|
-
# - if the columns option is given (or implicitely by setting table)
|
50
|
-
# - +Hash+ values are ordered according to the columns option, missing values are replaced by DEFAULT
|
51
|
-
# - +Array+ of +Hash+ multiple record insert
|
52
|
-
# - %bind is replaced with the current bind sequence.
|
53
|
-
# Without appended number the first %bind => $1, the second => $2 etc.
|
54
|
-
# - %bind\\d+ => $+Integer+ e.g. %bind7 => $7
|
55
|
-
# - %bind__text => $1 and it is registered as text - this is used in prepared statements TODO
|
56
|
-
# - %key_bind__text => $1 and it is registered as text when using +Hash+ in the execute
|
57
|
-
# $1 will be mapped to the key's value in the +Hash+ TODO
|
58
|
-
#
|
59
|
-
# All can be preceded by additional letters and underscore e.g. %foo_bar_column
|
60
|
-
#
|
61
|
-
# A database typecast is added to fields ending with double underscore and a valid db data type
|
62
|
-
# with optional array dimension
|
63
|
-
#
|
64
|
-
# - %field__jsonb => adds a ::JSONB typecast to the field
|
65
|
-
# - %number_to__text => adds a ::TEXT typecast to the field
|
66
|
-
# - %array__text1 => adds a ::TEXT[] TODO
|
67
|
-
# - %array__text2 => adds a ::TEXT[][] TODO
|
68
|
-
#
|
69
|
-
# Quoting
|
70
|
-
# - Any value of the standard mixins are quoted with these exceptions
|
71
|
-
# - +Array+ are quoted as DB Arrays unless the type cast e.g. __jsonb is given
|
72
|
-
# - +Hash+ are quoted as jsonb
|
73
|
-
# - When the value responds to :to_sql or is a +Arel::Nodes::SqlLiteral+ its added as raw SQL
|
74
|
-
# - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
|
75
|
-
#
|
76
|
-
# Special quoting columns
|
77
|
-
# - +String+ or +Symbol+ without a dot e.g. :firstname => "firstname"
|
78
|
-
# - +String+ or +Symbol+ containing a dot e.g. "users.firstname" or => "users"."firstname"
|
79
|
-
# - +Array+
|
80
|
-
# - +String+ and +Symbols+ see above
|
81
|
-
# - +Hash+ see below
|
82
|
-
# - +Hash+ or within the +Array+
|
83
|
-
# - +Symbol+ value will become the column name e.g. {table: :column} => "table"."column"
|
84
|
-
# - +String+ value will become the expression, the key the AS {result: "SUM(*)"} => SUM(*) AS result
|
85
|
-
# - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
|
86
|
-
#
|
87
4
|
class QuoteSql
|
5
|
+
|
88
6
|
DATA_TYPES_RE = %w(
|
89
7
|
(?:small|big)(?:int|serial)
|
90
8
|
bit bool(?:ean)? box bytea cidr circle date
|
@@ -119,56 +37,42 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
119
37
|
@quotes = {}
|
120
38
|
@resolved = {}
|
121
39
|
@binds = []
|
40
|
+
|
41
|
+
@tables = {}
|
42
|
+
@columns = {}
|
43
|
+
@casts = {}
|
122
44
|
end
|
123
45
|
|
124
|
-
attr_reader :sql, :quotes, :original, :binds
|
125
|
-
attr_writer :table_name, :column_names
|
46
|
+
attr_reader :sql, :quotes, :original, :binds, :tables, :columns
|
126
47
|
|
127
|
-
def
|
128
|
-
|
129
|
-
return unless table = @quote&.dig(:table)
|
130
|
-
@table_name = table.respond_to?(:table_name) ? table.table_name : table.to_s
|
48
|
+
def table(name = nil)
|
49
|
+
@tables[name&.to_sym].dup
|
131
50
|
end
|
132
51
|
|
133
|
-
def
|
134
|
-
|
135
|
-
return unless columns = @quote&.dig(:columns)
|
136
|
-
@column_names = if columns[0].is_a? String
|
137
|
-
columns
|
138
|
-
else
|
139
|
-
columns.map(&:name)
|
140
|
-
end.map(&:to_s)
|
52
|
+
def columns(name = nil)
|
53
|
+
@columns[name&.to_sym].dup
|
141
54
|
end
|
142
55
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
56
|
+
def casts(name = nil)
|
57
|
+
unless rv = @casts[name&.to_sym]
|
58
|
+
table = table(name) or return
|
59
|
+
return unless table.respond_to? :columns
|
60
|
+
rv = table.columns.to_h { [_1.name.to_sym, _1.sql_type] }
|
148
61
|
end
|
149
|
-
|
150
|
-
self
|
62
|
+
rv
|
151
63
|
end
|
152
64
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
def sql
|
162
|
-
@object.original.inspect
|
163
|
-
end
|
164
|
-
|
165
|
-
# def inspect
|
166
|
-
# super + errors.flat_map { [_1.inspect, _1.backtrace] }
|
167
|
-
# end
|
168
|
-
|
169
|
-
def message
|
170
|
-
super + %Q@<QuoteSql #{sql} #{@object.errors.inspect}>@
|
65
|
+
# Add quotes keys are symbolized
|
66
|
+
def quote(quotes = {})
|
67
|
+
re = /(?:^|(.*)_)(table|columns|casts)$/i
|
68
|
+
quotes.keys.grep(re).each do |quote|
|
69
|
+
_, name, type = quote.to_s.match(re)&.to_a
|
70
|
+
value = quotes.delete quote
|
71
|
+
value = Raw.sql(value) if value.class.to_s == "Arel::Nodes::SqlLiteral"
|
72
|
+
instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
|
171
73
|
end
|
74
|
+
@quotes.update quotes.transform_keys(&:to_sym)
|
75
|
+
self
|
172
76
|
end
|
173
77
|
|
174
78
|
def to_sql
|
@@ -199,7 +103,6 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
199
103
|
@prepare_name = name
|
200
104
|
end
|
201
105
|
|
202
|
-
|
203
106
|
# Executes a prepared statement
|
204
107
|
# Processes in batches records
|
205
108
|
# returns the array of the results depending on RETURNING is in the query
|
@@ -236,7 +139,7 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
236
139
|
@quotes.to_h do |k, v|
|
237
140
|
r = @resolved[k]
|
238
141
|
next [nil, nil] if r.nil? or not r.is_a?(Exception)
|
239
|
-
[k, {@quotes[k].inspect => v.inspect, exc: r, backtrace: r.backtrace}]
|
142
|
+
[k, { @quotes[k].inspect => v.inspect, exc: r, backtrace: r.backtrace }]
|
240
143
|
end.compact
|
241
144
|
end
|
242
145
|
|
@@ -249,7 +152,11 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
249
152
|
def key_matches
|
250
153
|
@sql.scan(MIXIN_RE).map do |full, *key|
|
251
154
|
key = key.compact[0]
|
252
|
-
|
155
|
+
if m = key.match(/^(.+)#{CASTS}/i)
|
156
|
+
_, key, cast = m.to_a
|
157
|
+
end
|
158
|
+
has_quote = @quotes.key?(key.to_sym) || key.match?(/(table|columns)$/)
|
159
|
+
[full, key, cast, has_quote]
|
253
160
|
end
|
254
161
|
end
|
255
162
|
|
@@ -259,22 +166,20 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
259
166
|
loop do
|
260
167
|
s = StringScanner.new(@sql)
|
261
168
|
sql = ""
|
262
|
-
key_matches.each do |key_match, key, has_quote|
|
169
|
+
key_matches.each do |key_match, key, cast, has_quote|
|
263
170
|
s.scan_until(/(.*?)#{key_match}([a-z0-9_]*)/im)
|
264
171
|
matched, pre, post = s.matched, s[1], s[2]
|
265
|
-
if m = key.match(/^bind(\d+)
|
266
|
-
if m[2].present?
|
267
|
-
cast = m[2].tr("_", " ")
|
268
|
-
end
|
172
|
+
if m = key.match(/^bind(\d+)?/im)
|
269
173
|
if m[1].present?
|
270
174
|
bind_num = m[1].to_i
|
271
175
|
@binds[bind_num - 1] ||= cast
|
272
|
-
raise "
|
176
|
+
raise "bind #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
|
273
177
|
else
|
274
178
|
@binds << cast
|
275
179
|
bind_num = @binds.length
|
276
180
|
end
|
277
|
-
|
181
|
+
|
182
|
+
matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
|
278
183
|
elsif has_quote
|
279
184
|
quoted = quoter(key)
|
280
185
|
unresolved.delete key
|
@@ -305,9 +210,25 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
305
210
|
|
306
211
|
extend Quoting
|
307
212
|
|
308
|
-
|
213
|
+
class Raw < String
|
214
|
+
def self.sql(v)
|
215
|
+
if v.class == self
|
216
|
+
v
|
217
|
+
elsif v.respond_to? :to_sql
|
218
|
+
new v.to_sql
|
219
|
+
else
|
220
|
+
new v
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.test(which = :all)
|
309
226
|
require __dir__ + "/quote_sql/test.rb"
|
310
|
-
|
227
|
+
if which == :all
|
228
|
+
Test.new.all
|
229
|
+
else
|
230
|
+
Test.new.run(which)
|
231
|
+
end
|
311
232
|
end
|
312
233
|
end
|
313
234
|
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quote-sql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Kufner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-02-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: niceql
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
description: 'QuoteSql helps you creating SQL queries and proper quoting especially
|
14
28
|
with advanced queries.
|
15
29
|
|
@@ -23,12 +37,13 @@ files:
|
|
23
37
|
- lib/quote_sql.rb
|
24
38
|
- lib/quote_sql/connector.rb
|
25
39
|
- lib/quote_sql/connector/active_record_base.rb
|
26
|
-
- lib/quote_sql/
|
40
|
+
- lib/quote_sql/error.rb
|
27
41
|
- lib/quote_sql/extension.rb
|
28
42
|
- lib/quote_sql/formater.rb
|
29
43
|
- lib/quote_sql/quoter.rb
|
30
44
|
- lib/quote_sql/quoting.rb
|
31
45
|
- lib/quote_sql/test.rb
|
46
|
+
- lib/quote_sql/version.rb
|
32
47
|
homepage: https://github.com/martin-kufner/quote-sql
|
33
48
|
licenses:
|
34
49
|
- MIT
|
@@ -48,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
63
|
- !ruby/object:Gem::Version
|
49
64
|
version: '0'
|
50
65
|
requirements: []
|
51
|
-
rubygems_version: 3.
|
66
|
+
rubygems_version: 3.5.6
|
52
67
|
signing_key:
|
53
68
|
specification_version: 4
|
54
69
|
summary: Tool to build and run SQL queries easier
|
data/lib/quote_sql/deprecated.rb
DELETED
@@ -1,162 +0,0 @@
|
|
1
|
-
class QuoteSql
|
2
|
-
module Deprecated
|
3
|
-
private def conn
|
4
|
-
ApplicationRecord.connection
|
5
|
-
end
|
6
|
-
|
7
|
-
private def quote_sql_values(sub, casts)
|
8
|
-
sub.map do |s|
|
9
|
-
casts.map do |k, column|
|
10
|
-
column.transform_keys(&:to_sym) => { sql_type:, default:, array: }
|
11
|
-
value = s.key?(k) ? s[k] : s[k.to_sym]
|
12
|
-
if value.nil?
|
13
|
-
value = default
|
14
|
-
else
|
15
|
-
value = value.to_json if sql_type[/^json/]
|
16
|
-
end
|
17
|
-
"#{conn.quote(value)}::#{sql_type}"
|
18
|
-
end.join(",")
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def quote_sql(**options)
|
23
|
-
loop do
|
24
|
-
# keys = []
|
25
|
-
break unless gsub!(%r{(?<=^|\W)[:$](#{options.keys.join("|")})(?=\W|$)}) do |m|
|
26
|
-
key = m[1..].to_sym
|
27
|
-
# keys << key
|
28
|
-
next m unless options.key? key
|
29
|
-
sub = options[key]
|
30
|
-
case sub
|
31
|
-
when Arel::Nodes::SqlLiteral
|
32
|
-
next sub
|
33
|
-
when NilClass
|
34
|
-
next "NULL"
|
35
|
-
when TrueClass, FalseClass
|
36
|
-
next sub.to_s.upcase
|
37
|
-
when Time
|
38
|
-
sub = sub.strftime("%Y-%m-%d %H:%M:%S.%3N%z")
|
39
|
-
end
|
40
|
-
if sub.respond_to? :to_sql
|
41
|
-
next sub.to_sql
|
42
|
-
end
|
43
|
-
case m
|
44
|
-
when /^:(.+)_(FROM_CLAUSE)$/ # prefix (column,...) AS ( VALUES (data::CAST, ...), ...)
|
45
|
-
name = conn.quote_column_name($1)
|
46
|
-
casts = sub.shift.transform_keys(&:to_s)
|
47
|
-
rv = quote_sql_values(sub, casts)
|
48
|
-
column_names = casts.map { conn.quote_column_name(_2.key?(:as) ? _2[:as] : _1) }
|
49
|
-
next "(VALUES \n(#{rv.join("),\n(")})\n ) #{name} (#{column_names.join(",") })"
|
50
|
-
|
51
|
-
when /^:(.+)_(as_select)$/i # prefix (column,...) AS ( VALUES (data::CAST, ...), ...)
|
52
|
-
name = conn.quote_column_name($1)
|
53
|
-
casts = sub.shift.transform_keys(&:to_s)
|
54
|
-
rv = quote_sql_values(sub, casts)
|
55
|
-
next "SELECT * FROM (VALUES \n(#{rv.join("),\n(")})\n ) #{name} (#{casts.keys.map { conn.quote_column_name(_1) }.join(",") })"
|
56
|
-
when /^:(.+)_(as_values)$/i # prefix (column,...) AS ( VALUES (data::CAST, ...), ...)
|
57
|
-
name = conn.quote_column_name($1)
|
58
|
-
casts = sub.shift.transform_keys(&:to_s)
|
59
|
-
rv = quote_sql_values(sub, casts)
|
60
|
-
next "#{name} (#{casts.keys.map { conn.quote_column_name(_1) }.join(",") }) AS ( VALUES \n(#{rv.join("),\n(")})\n )"
|
61
|
-
when /^:(.+)_(values)$/i
|
62
|
-
casts = sub.shift.transform_keys(&:to_sym)
|
63
|
-
rv = quote_sql_values(sub, casts)
|
64
|
-
next "VALUES \n(#{rv.join("),\n(")})\n"
|
65
|
-
when /_(LIST)$/i
|
66
|
-
next sub.map { conn.quote _1 }.join(",")
|
67
|
-
when /_(args)$/i
|
68
|
-
next sub.join(',')
|
69
|
-
when /_(raw|sql)$/i
|
70
|
-
next sub
|
71
|
-
when /_(ident|column)$/i, /table_name$/, /_?columns?$/, /column_names$/
|
72
|
-
if sub.is_a? Array
|
73
|
-
next sub.map do
|
74
|
-
_1[/^"[^"]+"\."[^"]+"$/] ? _1 : conn.quote_column_name(_1)
|
75
|
-
end.join(',')
|
76
|
-
else
|
77
|
-
next conn.quote_column_name(sub)
|
78
|
-
end
|
79
|
-
when /(?<=_)jsonb?$/i
|
80
|
-
next conn.quote(sub.to_json) + "::#{$MATCH}"
|
81
|
-
when /(?<=_)(uuid|int|text)$/i
|
82
|
-
cast = "::#{$MATCH}"
|
83
|
-
end
|
84
|
-
case sub
|
85
|
-
when Regexp
|
86
|
-
sub.to_postgres
|
87
|
-
when Array
|
88
|
-
dims = 1 # todo more dimensional Arrays
|
89
|
-
dive = ->(ary) do
|
90
|
-
ary.map { |s| conn.quote s }.join(',')
|
91
|
-
end
|
92
|
-
sub = "[#{dive.call sub}]"
|
93
|
-
cast += "[]" * dims if cast.present?
|
94
|
-
"ARRAY#{sub}#{cast}"
|
95
|
-
else
|
96
|
-
"#{conn.quote(sub)}#{cast}"
|
97
|
-
end
|
98
|
-
end
|
99
|
-
# break if options.except!(*keys).blank?
|
100
|
-
end
|
101
|
-
Arel.sql self
|
102
|
-
end
|
103
|
-
|
104
|
-
def exec
|
105
|
-
result = conn.exec_query(self)
|
106
|
-
columns = result.columns.map(&:to_sym)
|
107
|
-
result.cast_values.map do |row|
|
108
|
-
row = [row] unless row.is_a? Array
|
109
|
-
[columns, row].transpose.to_h
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def quote_exec(**)
|
114
|
-
quote_sql(**).exec
|
115
|
-
end
|
116
|
-
|
117
|
-
module Dsql
|
118
|
-
|
119
|
-
def dsql
|
120
|
-
IO.popen(PG_FORMAT_BIN, "r+", err: "/dev/null") do |f|
|
121
|
-
f.write self
|
122
|
-
f.close_write
|
123
|
-
puts f.read
|
124
|
-
end
|
125
|
-
self
|
126
|
-
rescue
|
127
|
-
self
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
include Dsql
|
132
|
-
|
133
|
-
module String
|
134
|
-
def self.included(other)
|
135
|
-
other.include Dsql
|
136
|
-
end
|
137
|
-
|
138
|
-
def quote_sql(**)
|
139
|
-
Arel.sql(self).quote_sql(**)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
module Relation
|
144
|
-
def quote_sql(**)
|
145
|
-
Arel.sql(to_sql).quote_sql(**)
|
146
|
-
end
|
147
|
-
|
148
|
-
def dsql
|
149
|
-
to_sql.dsql
|
150
|
-
self
|
151
|
-
end
|
152
|
-
|
153
|
-
def result
|
154
|
-
result = ApplicationRecord.connection.exec_query(to_sql)
|
155
|
-
columns = result.columns.map(&:to_sym)
|
156
|
-
result.cast_values.map do |row|
|
157
|
-
[columns, Array(row)].transpose.to_h
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|