quote-sql 0.0.2 → 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 +227 -125
- data/lib/quote_sql/test.rb +179 -75
- data/lib/quote_sql/version.rb +3 -0
- data/lib/quote_sql.rb +57 -125
- 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,48 +3,131 @@ 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
|
|
8
|
-
attr_reader :key, :quotable
|
9
|
+
attr_reader :key, :quotable, :name
|
10
|
+
|
11
|
+
def quotes
|
12
|
+
@qsql.quotes
|
13
|
+
end
|
14
|
+
|
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
|
51
|
+
|
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
|
77
|
+
end
|
9
78
|
|
10
79
|
def to_sql
|
11
80
|
return @quotable.call(self) if @quotable.is_a? Proc
|
12
81
|
case key.to_s
|
13
82
|
when /(?:^|(.*)_)table$/i
|
14
|
-
|
15
|
-
when /(?:^|(.*)_)columns
|
16
|
-
|
17
|
-
when /(?:^|(.*)_)(
|
18
|
-
|
19
|
-
when /(?:^|(.*)_)(column_name?s?)$/i
|
20
|
-
ident_name
|
21
|
-
when /(?:^|(.*)_)(ident|args)$/i
|
22
|
-
ident_name
|
83
|
+
ident_table
|
84
|
+
when /(?:^|(.*)_)columns$/i
|
85
|
+
ident_columns
|
86
|
+
when /(?:^|(.*)_)(ident)$/i
|
87
|
+
_ident
|
23
88
|
when /(?:^|(.*)_)constraints?$/i
|
24
|
-
quotable
|
89
|
+
quotable
|
25
90
|
when /(?:^|(.*)_)(raw|sql)$/i
|
26
|
-
quotable
|
27
|
-
when
|
28
|
-
|
91
|
+
quotable
|
92
|
+
when /^(.+)_json$/i
|
93
|
+
data_json
|
94
|
+
when /^(.+)_values$/i
|
95
|
+
data_values
|
96
|
+
when /values$/i
|
97
|
+
insert_values
|
29
98
|
else
|
30
99
|
quote
|
31
100
|
end
|
32
101
|
end
|
33
102
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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|
|
48
131
|
case i
|
49
132
|
when :default, :current_timestamp
|
50
133
|
next i.to_s.upcase
|
@@ -52,28 +135,87 @@ class QuoteSql
|
|
52
135
|
i = i.to_json
|
53
136
|
end
|
54
137
|
_quote(i)
|
55
|
-
end
|
138
|
+
end
|
139
|
+
Raw.sql "(#{rv.join(",")})"
|
140
|
+
end
|
56
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(',')})"
|
57
148
|
end
|
58
149
|
|
59
|
-
|
150
|
+
|
151
|
+
def data_values(item = @quotable)
|
152
|
+
item = Array(item).compact
|
153
|
+
column_names = columns(name)
|
154
|
+
if column_names.is_a? Hash
|
155
|
+
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
156
|
+
column_names = column_names.keys
|
157
|
+
end
|
158
|
+
if item.all? { _1.is_a?(Hash) }
|
159
|
+
column_names ||= item.flat_map { _1.keys.sort }.uniq
|
160
|
+
item.map! { _1.fetch_values(*column_names) {} }
|
161
|
+
end
|
162
|
+
if item.all? { _1.is_a?(Array) }
|
163
|
+
length, overflow = item.map { _1.length }.uniq
|
164
|
+
raise ArgumentError, "all values need to have the same length" if overflow
|
165
|
+
column_names ||= (1..length).map { "column#{_1}" }
|
166
|
+
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
|
167
|
+
values = item.map { value(_1) }
|
168
|
+
else
|
169
|
+
raise ArgumentError, "Either all type Hash or Array"
|
170
|
+
end
|
171
|
+
if types.present?
|
172
|
+
value = values[0][1..-2].split(/\s*,\s*/)
|
173
|
+
types.each_with_index { value[_2] << _1 || "" }
|
174
|
+
values[0] = "(" + value.join(",") + ")"
|
175
|
+
end
|
176
|
+
# values[0] { _1 << types[_1] || ""}
|
177
|
+
Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
|
178
|
+
end
|
179
|
+
|
180
|
+
def insert_values(item = @quotable)
|
60
181
|
case item
|
61
182
|
when Arel::Nodes::SqlLiteral
|
62
|
-
item =
|
183
|
+
item = Raw.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
|
63
184
|
return item
|
64
185
|
when Array
|
186
|
+
item.compact!
|
187
|
+
column_names = (@qsql.quotes[:columns] || @qsql.quotes[:column_names]).dup
|
188
|
+
types = []
|
189
|
+
if column_names.is_a? Hash
|
190
|
+
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
191
|
+
column_names = column_names.keys
|
192
|
+
elsif column_names.is_a? Array
|
193
|
+
column_names = column_names.map do |column|
|
194
|
+
types << column.respond_to?(:sql_type) ? "::#{column.sql_type}" : nil
|
195
|
+
column.respond_to?(:name) ? column.name : column
|
196
|
+
end
|
197
|
+
end
|
65
198
|
|
66
|
-
|
67
|
-
|
68
|
-
item.
|
199
|
+
if item.all? { _1.is_a?(Hash) }
|
200
|
+
column_names ||= item.flat_map { _1.keys.sort }.uniq
|
201
|
+
item.map! { _1.fetch_values(*column_names) {} }
|
202
|
+
end
|
203
|
+
|
204
|
+
if item.all? { _1.is_a?(Array) }
|
205
|
+
length, overflow = item.map { _1.length }.uniq
|
206
|
+
raise ArgumentError, "all values need to have the same length" if overflow
|
207
|
+
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names and column_names.length != length
|
208
|
+
values = item.map { value(_1) }
|
69
209
|
else
|
70
|
-
|
210
|
+
raise ArgumentError, "Either all type Hash or Array"
|
211
|
+
end
|
212
|
+
if column_names.present?
|
213
|
+
Raw.sql "(#{_ident column_names}) VALUES #{values.join(",")}"
|
214
|
+
else
|
215
|
+
Raw.sql "VALUES #{values.join(",")}"
|
71
216
|
end
|
72
217
|
when Hash
|
73
218
|
value([item])
|
74
|
-
else
|
75
|
-
return item.to_sql if item.respond_to? :to_sql
|
76
|
-
"(" + _quote(item) + ")"
|
77
219
|
end
|
78
220
|
end
|
79
221
|
|
@@ -90,51 +232,31 @@ class QuoteSql
|
|
90
232
|
private def _quote(item = @quotable, cast = self.cast)
|
91
233
|
rv = QuoteSql.quote(item)
|
92
234
|
if cast
|
93
|
-
rv << "::#{cast}"
|
235
|
+
rv << "::#{cast.upcase}"
|
94
236
|
rv << "[]" * rv.depth if rv[/^ARRAY/]
|
95
237
|
end
|
96
|
-
rv
|
238
|
+
Raw.sql rv
|
97
239
|
end
|
98
240
|
|
99
|
-
private def _quote_column_name(name
|
100
|
-
name
|
101
|
-
rv = QuoteSql.quote_column_name(name)
|
102
|
-
return rv unless column.present?
|
103
|
-
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(".")
|
104
243
|
end
|
105
244
|
|
106
245
|
def quote(item = @quotable)
|
107
|
-
case item
|
108
|
-
when Arel::Nodes::SqlLiteral
|
109
|
-
return item
|
110
|
-
when Array
|
246
|
+
case item.class.to_s
|
247
|
+
when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
|
248
|
+
return Raw.sql(item)
|
249
|
+
when "Array"
|
111
250
|
return _quote(item.to_json) if json?
|
112
251
|
_quote(item)
|
113
|
-
when Hash
|
114
|
-
|
115
|
-
item.map do |as, item|
|
116
|
-
"#{_quote(item)} AS #{as}"
|
117
|
-
end.join(",")
|
252
|
+
when "Hash"
|
253
|
+
_quote(item.to_json, :jsonb)
|
118
254
|
else
|
119
|
-
return item.to_sql if item.respond_to? :to_sql
|
255
|
+
return Raw.sql item.to_sql if item.respond_to? :to_sql
|
120
256
|
_quote(item)
|
121
257
|
end
|
122
258
|
end
|
123
259
|
|
124
|
-
def columns(item = @quotable)
|
125
|
-
if item.respond_to?(:column_names)
|
126
|
-
item = item.column_names
|
127
|
-
elsif item.class.respond_to?(:column_names)
|
128
|
-
item = item.class.column_names
|
129
|
-
elsif item.is_a?(Array)
|
130
|
-
if item[0].respond_to?(:name)
|
131
|
-
item = item.map(&:name)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
@qsql.column_names ||= item
|
135
|
-
ident_name(item)
|
136
|
-
end
|
137
|
-
|
138
260
|
def column_names(item = @quotable)
|
139
261
|
if item.respond_to?(:column_names)
|
140
262
|
item = item.column_names
|
@@ -144,70 +266,50 @@ class QuoteSql
|
|
144
266
|
item = item.map(&:name)
|
145
267
|
end
|
146
268
|
@qsql.column_names ||= item
|
147
|
-
|
269
|
+
_ident(item)
|
148
270
|
end
|
149
271
|
|
150
|
-
def
|
151
|
-
|
152
|
-
rv = "jsonb_build_object(" + h.map { "'#{_1}',#{_2}" }.join(",") + ")"
|
153
|
-
return rv unless compact
|
154
|
-
"jsonb_strip_nulls(#{rv})"
|
272
|
+
def json_array_values(h)
|
273
|
+
Raw.sql "'#{h.to_json.gsub(/'/, "''")}'::JSONB"
|
155
274
|
end
|
156
275
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
case item
|
162
|
-
when Hash
|
163
|
-
ident_name(item)
|
164
|
-
when String, Symbol
|
165
|
-
_quote_column_name(item)
|
166
|
-
when Proc
|
167
|
-
item.call(self)
|
168
|
-
end
|
169
|
-
end.join(",")
|
170
|
-
when Hash
|
171
|
-
item.map do |k,v|
|
172
|
-
case v
|
173
|
-
when Symbol
|
174
|
-
_quote_column_name(k, v)
|
175
|
-
when String
|
176
|
-
"#{v} AS #{k}"
|
177
|
-
when Proc
|
178
|
-
item.call(self)
|
179
|
-
when Hash
|
180
|
-
"#{json_build_object(v)} AS #{k}"
|
181
|
-
else
|
182
|
-
raise ArgumentError
|
183
|
-
end
|
184
|
-
end.join(",")
|
185
|
-
else
|
186
|
-
_quote_column_name(item)
|
187
|
-
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)
|
188
280
|
end
|
189
281
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
item = item.class.table_name
|
195
|
-
end
|
196
|
-
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)
|
197
286
|
end
|
198
287
|
|
199
|
-
def
|
200
|
-
|
201
|
-
when Array
|
202
|
-
item.map do |item|
|
203
|
-
item.is_a?(Hash) ? table_name(item) : _quote_column_name(item)
|
204
|
-
end.join(",")
|
205
|
-
when Hash
|
206
|
-
raise NotImplementedError, "table name is a Hash"
|
207
|
-
# perhaps as ...
|
208
|
-
else
|
209
|
-
_quote_column_name(item)
|
210
|
-
end
|
288
|
+
def json_array_ident(h)
|
289
|
+
Raw.sql "jsonb_build_array(#{h.map { _ident(_2) }.join(",")})"
|
211
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
|
212
314
|
end
|
213
315
|
end
|
data/lib/quote_sql/test.rb
CHANGED
@@ -1,28 +1,40 @@
|
|
1
|
-
|
2
|
-
def self.all
|
3
|
-
methods(false).grep(/^test_/).each do |name|
|
4
|
-
run(name)
|
5
|
-
puts
|
6
|
-
end
|
1
|
+
class QuoteSql::Test
|
7
2
|
|
3
|
+
def all
|
4
|
+
@success = []
|
5
|
+
@fail = []
|
6
|
+
private_methods(false).grep(/^test_/).each { run(_1, true) }
|
7
|
+
@success.each { STDOUT.puts(*_1, nil) }
|
8
|
+
@fail.each { STDOUT.puts(*_1, nil) }
|
9
|
+
puts
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
12
|
+
def run(name, all = false)
|
11
13
|
name = name.to_s.sub(/^test_/, "")
|
14
|
+
rv = ["🧪 #{name}"]
|
12
15
|
@expected = nil
|
13
16
|
@test = send("test_#{name}")
|
14
|
-
if sql.gsub(/\s+/, "") == expected&.gsub(/\s+/, "")
|
15
|
-
|
17
|
+
if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
|
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}"]
|
21
|
+
@success << rv if @success
|
16
22
|
else
|
17
|
-
|
23
|
+
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
|
24
|
+
@fail << rv if @fail
|
18
25
|
end
|
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
|
19
31
|
end
|
20
32
|
|
21
|
-
def
|
33
|
+
def expected(v = nil)
|
22
34
|
@expected ||= v
|
23
35
|
end
|
24
36
|
|
25
|
-
def
|
37
|
+
def sql
|
26
38
|
@test.to_sql
|
27
39
|
end
|
28
40
|
|
@@ -39,73 +51,165 @@ module QuoteSql::Test
|
|
39
51
|
"SELECT * FROM #{self.class.table_name}"
|
40
52
|
end
|
41
53
|
end
|
42
|
-
class << self
|
43
|
-
def test_columns_and_table_name_simple
|
44
|
-
expected Arel.sql(%(SELECT "a","b"."c" FROM "my_table"))
|
45
|
-
QuoteSql.new("SELECT %columns FROM %table_name").quote(
|
46
|
-
columns: [:a, b: :c],
|
47
|
-
table_name: "my_table"
|
48
|
-
)
|
49
|
-
end
|
50
54
|
|
51
|
-
|
52
|
-
expected Arel.sql(%(SELECT "a","b"."c" FROM "table1","table2"))
|
53
|
-
QuoteSql.new("SELECT %columns FROM %table_names").quote(
|
54
|
-
columns: [:a, b: :c],
|
55
|
-
table_names: ["table1", "table2"]
|
56
|
-
)
|
57
|
-
end
|
55
|
+
private
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
table_names: "table1"
|
66
|
-
)
|
67
|
-
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
|
68
63
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
)
|
79
|
-
end
|
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
|
80
73
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
87
83
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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]])
|
110
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
|
139
|
+
|
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: [
|
157
|
+
{ a: 'a', b: 1, c: true, d: nil },
|
158
|
+
{ d: nil, a: 'a', c: true, b: 1 },
|
159
|
+
{ d: 2, b: 1 }
|
160
|
+
])
|
161
|
+
end
|
162
|
+
|
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
|
169
|
+
|
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
|
176
|
+
|
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
|
183
|
+
|
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:)
|
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
|
+
|
111
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,50 +37,47 @@ 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
|
-
|
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
|
160
73
|
end
|
74
|
+
@quotes.update quotes.transform_keys(&:to_sym)
|
75
|
+
self
|
161
76
|
end
|
162
77
|
|
163
78
|
def to_sql
|
164
79
|
mixin!
|
165
|
-
raise Error.new(self) if errors?
|
80
|
+
raise Error.new(self, errors) if errors?
|
166
81
|
return Arel.sql @sql if defined? Arel
|
167
82
|
@sql
|
168
83
|
end
|
@@ -188,7 +103,6 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
188
103
|
@prepare_name = name
|
189
104
|
end
|
190
105
|
|
191
|
-
|
192
106
|
# Executes a prepared statement
|
193
107
|
# Processes in batches records
|
194
108
|
# returns the array of the results depending on RETURNING is in the query
|
@@ -224,8 +138,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
224
138
|
def errors
|
225
139
|
@quotes.to_h do |k, v|
|
226
140
|
r = @resolved[k]
|
227
|
-
next [nil, nil]
|
228
|
-
[k,
|
141
|
+
next [nil, nil] if r.nil? or not r.is_a?(Exception)
|
142
|
+
[k, { @quotes[k].inspect => v.inspect, exc: r, backtrace: r.backtrace }]
|
229
143
|
end.compact
|
230
144
|
end
|
231
145
|
|
@@ -238,7 +152,11 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
238
152
|
def key_matches
|
239
153
|
@sql.scan(MIXIN_RE).map do |full, *key|
|
240
154
|
key = key.compact[0]
|
241
|
-
|
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]
|
242
160
|
end
|
243
161
|
end
|
244
162
|
|
@@ -248,22 +166,20 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
248
166
|
loop do
|
249
167
|
s = StringScanner.new(@sql)
|
250
168
|
sql = ""
|
251
|
-
key_matches.each do |key_match, key, has_quote|
|
169
|
+
key_matches.each do |key_match, key, cast, has_quote|
|
252
170
|
s.scan_until(/(.*?)#{key_match}([a-z0-9_]*)/im)
|
253
171
|
matched, pre, post = s.matched, s[1], s[2]
|
254
|
-
if m = key.match(/^bind(\d+)
|
255
|
-
if m[2].present?
|
256
|
-
cast = m[2].tr("_", " ")
|
257
|
-
end
|
172
|
+
if m = key.match(/^bind(\d+)?/im)
|
258
173
|
if m[1].present?
|
259
174
|
bind_num = m[1].to_i
|
260
175
|
@binds[bind_num - 1] ||= cast
|
261
|
-
raise "
|
176
|
+
raise "bind #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
|
262
177
|
else
|
263
178
|
@binds << cast
|
264
179
|
bind_num = @binds.length
|
265
180
|
end
|
266
|
-
|
181
|
+
|
182
|
+
matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
|
267
183
|
elsif has_quote
|
268
184
|
quoted = quoter(key)
|
269
185
|
unresolved.delete key
|
@@ -294,9 +210,25 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
294
210
|
|
295
211
|
extend Quoting
|
296
212
|
|
297
|
-
|
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)
|
298
226
|
require __dir__ + "/quote_sql/test.rb"
|
299
|
-
|
227
|
+
if which == :all
|
228
|
+
Test.new.all
|
229
|
+
else
|
230
|
+
Test.new.run(which)
|
231
|
+
end
|
300
232
|
end
|
301
233
|
end
|
302
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
|