quote-sql 0.0.6 → 0.0.8
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 +10 -12
- data/lib/quote_sql/connector/active_record_base.rb +3 -4
- data/lib/quote_sql/formater.rb +3 -3
- data/lib/quote_sql/quoter.rb +136 -133
- data/lib/quote_sql/test.rb +275 -39
- data/lib/quote_sql/version.rb +1 -1
- data/lib/quote_sql.rb +84 -55
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1ae8afe082e0867130f3933c76e867ac3f16f53b816db61cd9a9cabdfeb3298
|
4
|
+
data.tar.gz: d5c1638521b04e8e62ff2dab8a27a27165f27f972ea4bc6e5f6e55e4680a8cb6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 886abf29ad27600528bd42dd88e4c13da41a1c7076b7bc9a69592d07f8d1653962eb9daaab68eb897238f5b4069658496919a254d01a53b5656cbff9c02bc958
|
7
|
+
data.tar.gz: '085c3fd76781bf95c658151398970fdc6de805767d495d4c23cc9e8cbd444cd61335b73162b99cc16d71eb1f8d268d8c7824244c5e2d6eafe47fe1bd20aaed0e'
|
data/README.md
CHANGED
@@ -26,7 +26,7 @@ Best Martin
|
|
26
26
|
`QuoteSql.new("SELECT %field").quote(field: "abc").to_sql`
|
27
27
|
=> SELECT 'abc'
|
28
28
|
|
29
|
-
`QuoteSql.new("SELECT %
|
29
|
+
`QuoteSql.new("SELECT %field::TEXT").quote(field: 9).to_sql`
|
30
30
|
=> SELECT 9::TEXT
|
31
31
|
|
32
32
|
### Rails models
|
@@ -54,15 +54,14 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
|
|
54
54
|
{lastname: "Schultz", firstname: "herbert"}
|
55
55
|
], constraint: :id).to_sql`
|
56
56
|
=> INSERT INTO "users" ("id", "firstname", "lastname", "created_at")
|
57
|
-
VALUES
|
57
|
+
VALUES
|
58
|
+
(1, 'Albert', 'Müller', DEFAULT),
|
59
|
+
(DEFAULT, 'herbert', 'Schultz', DEFAULT)
|
58
60
|
ON CONFLICT ("id") DO NOTHING
|
59
61
|
|
60
62
|
### Columns from a list
|
61
|
-
`QuoteSql.new("SELECT %columns").quote(columns: [:a, "b.c", d:
|
62
|
-
=> SELECT "a","b"."c",
|
63
|
-
|
64
|
-
`QuoteSql.new("SELECT %columns").quote(columns: [:a, "b.c", d: {e: field, nil: false}]).to_sql`
|
65
|
-
=> SELECT "a","b"."c",jsonb_strip_nulls(jsonb_build_object('e', 1)) AS d
|
63
|
+
`QuoteSql.new("SELECT %columns FROM %table").quote(table: "foo", columns: [:a, "b", "foo.c", {d: :e}]).to_sql`
|
64
|
+
=> SELECT "foo"."a","b"."foo"."c", "foo"."e" AS d
|
66
65
|
|
67
66
|
## Executing
|
68
67
|
### Getting the results
|
@@ -72,6 +71,7 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
|
|
72
71
|
### Binds
|
73
72
|
You can use binds ($1, $2, ...) in the SQL and add arguments to the result call
|
74
73
|
`QuoteSql.new('SELECT $1 AS a').result(1)`
|
74
|
+
=> [{:a=>1}]
|
75
75
|
|
76
76
|
#### using JSON
|
77
77
|
|
@@ -83,7 +83,7 @@ You can use binds ($1, $2, ...) in the SQL and add arguments to the result call
|
|
83
83
|
Insert fom json
|
84
84
|
|
85
85
|
v = {a: 1, b: "foo", c: true}
|
86
|
-
QuoteSql.new("INSERT INTO table (%
|
86
|
+
QuoteSql.new("INSERT INTO table (%columns) SELECT * FROM %json").quote({:json=>1}).result(v.to_json)
|
87
87
|
|
88
88
|
|
89
89
|
|
@@ -111,10 +111,8 @@ All can be preceded by additional letters and underscore e.g. `%foo_bar_column`
|
|
111
111
|
A database typecast is added to fields ending with double underscore and a valid db data type
|
112
112
|
with optional array dimension
|
113
113
|
|
114
|
-
- `%
|
115
|
-
- `%
|
116
|
-
- `%array__text1` => adds a `::TEXT[]` (TO BE IMPLEMENTED)
|
117
|
-
- `%array__text2` => adds a `::TEXT[][]` (TO BE IMPLEMENTED)
|
114
|
+
- `%field::jsonb` => treats the field as jsonb when casted
|
115
|
+
- `%array::text[]` => treats an array like a text array, default is JSONB
|
118
116
|
|
119
117
|
### Quoting
|
120
118
|
- Any value of the standard mixins are quoted with these exceptions
|
@@ -19,12 +19,11 @@ class QuoteSql
|
|
19
19
|
self.class.conn
|
20
20
|
end
|
21
21
|
|
22
|
-
def _exec_query(sql, binds = [],
|
23
|
-
conn.exec_query(sql, "SQL", binds,
|
22
|
+
def _exec_query(sql, binds = [], **options)
|
23
|
+
conn.exec_query(sql, "SQL", binds, **options)
|
24
24
|
end
|
25
25
|
|
26
|
-
def _exec(sql, binds = [],
|
27
|
-
options = { prepare:, async: }
|
26
|
+
def _exec(sql, binds = [], **options)
|
28
27
|
result = _exec_query(sql, binds, **options)
|
29
28
|
columns = result.columns.map(&:to_sym)
|
30
29
|
result.cast_values.map do |row|
|
data/lib/quote_sql/formater.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'niceql'
|
1
2
|
class QuoteSql
|
2
3
|
module Formater
|
3
4
|
PG_FORMAT_BIN = `which pg_format`.chomp.presence
|
@@ -9,15 +10,14 @@ class QuoteSql
|
|
9
10
|
|
10
11
|
def to_formatted_sql
|
11
12
|
sql = respond_to?(:to_sql) ? to_sql : to_s
|
12
|
-
Niceql::Prettifier.prettify_sql(sql)
|
13
|
+
Niceql::Prettifier.prettify_sql(sql.gsub(/(?<=[^%])%(?=\S)/, "%%"))
|
13
14
|
|
14
15
|
# IO.popen(PG_FORMAT_BIN, "r+", err: "/dev/null") do |f|
|
15
16
|
# f.write(sql)
|
16
17
|
# f.close_write
|
17
18
|
# f.read
|
18
19
|
# end
|
19
|
-
|
20
|
-
sql
|
20
|
+
|
21
21
|
end
|
22
22
|
|
23
23
|
alias to_sqf to_formatted_sql
|
data/lib/quote_sql/quoter.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
class QuoteSql
|
2
2
|
class Quoter
|
3
|
-
def initialize(qsql, key, quotable)
|
3
|
+
def initialize(qsql, key, cast, quotable)
|
4
4
|
@qsql = qsql
|
5
|
-
@key, @quotable = key, quotable
|
5
|
+
@key, @cast, @quotable = key, cast, quotable
|
6
6
|
@name = key.sub(/_[^_]+$/, '') if key["_"]
|
7
7
|
end
|
8
8
|
|
9
|
-
attr_reader :key, :quotable, :name
|
9
|
+
attr_reader :key, :quotable, :name, :cast
|
10
10
|
|
11
11
|
def quotes
|
12
12
|
@qsql.quotes
|
@@ -34,19 +34,19 @@ class QuoteSql
|
|
34
34
|
@qsql.casts(name || self.name)
|
35
35
|
end
|
36
36
|
|
37
|
-
def ident_columns(name =
|
38
|
-
item = columns(name
|
37
|
+
def ident_columns(name = self.name)
|
38
|
+
item = columns(name)
|
39
39
|
unless item
|
40
|
-
unless item = casts(name
|
41
|
-
if (table = self.table(name
|
40
|
+
unless item = casts(name)&.keys&.map(&:to_s)
|
41
|
+
if (table = self.table(name))&.respond_to? :column_names
|
42
42
|
item = table.column_names
|
43
43
|
else
|
44
|
-
raise
|
44
|
+
raise ArgumentError, "No columns, casts or table given for #{name}" unless table&.respond_to? :column_names
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
if item.is_a?(Array)
|
49
|
-
if item.all? { _1.respond_to?(:name) }
|
49
|
+
if item.all? { not _1.is_a?(Symbol) and not _1.is_a?(String) and _1.respond_to?(:name) }
|
50
50
|
item = item.map(&:name)
|
51
51
|
end
|
52
52
|
end
|
@@ -90,13 +90,13 @@ class QuoteSql
|
|
90
90
|
when /(?:^|(.*)_)(ident)$/i
|
91
91
|
_ident
|
92
92
|
when /(?:^|(.*)_)constraints?$/i
|
93
|
-
quotable
|
93
|
+
quotable.to_s
|
94
94
|
when /(?:^|(.*)_)(raw|sql)$/i
|
95
|
-
quotable
|
96
|
-
when
|
97
|
-
|
95
|
+
quotable.to_s
|
96
|
+
when /(?:^|(.*)_)json$/i
|
97
|
+
json_recordset
|
98
98
|
when /^(.+)_values$/i
|
99
|
-
|
99
|
+
values
|
100
100
|
when /values$/i
|
101
101
|
insert_values
|
102
102
|
else
|
@@ -106,31 +106,7 @@ class QuoteSql
|
|
106
106
|
|
107
107
|
###############
|
108
108
|
|
109
|
-
private def
|
110
|
-
# case values.class.to_s
|
111
|
-
# when "QuoteSql::Raw", "Arel::Nodes::SqlLiteral" then rv = values
|
112
|
-
# when "Array"
|
113
|
-
# when "Hash"
|
114
|
-
# columns = self.columns(name)&.flat_map { _1.is_a?(Hash) ? _1.values : _1 }
|
115
|
-
# if columns.nil?
|
116
|
-
# values = values.values
|
117
|
-
# elsif columns.all? { _1.is_a? Symbol }
|
118
|
-
# raise ArgumentError, "Columns just Symbols"
|
119
|
-
# else
|
120
|
-
# values = columns.map do |column|
|
121
|
-
# if values.key?(column&.to_sym) or !defaults
|
122
|
-
# values[column.to_sym]
|
123
|
-
# elsif column[/^(created|updated)_at$/]
|
124
|
-
# :current_timestamp
|
125
|
-
# else
|
126
|
-
# :default
|
127
|
-
# end
|
128
|
-
# end
|
129
|
-
# end
|
130
|
-
# else
|
131
|
-
# raise ArgumentError, "value just Array, Hash, QuoteSql::Raw, Arel::Nodes::SqlLiteral"
|
132
|
-
# end
|
133
|
-
|
109
|
+
private def _value(values)
|
134
110
|
rv ||= values.map do |i|
|
135
111
|
case i
|
136
112
|
when :default, :current_timestamp
|
@@ -143,128 +119,155 @@ class QuoteSql
|
|
143
119
|
Raw.sql "(#{rv.join(",")})"
|
144
120
|
end
|
145
121
|
|
146
|
-
def data_json(item = @quotable)
|
122
|
+
# def data_json(item = @quotable)
|
123
|
+
# casts = self.casts(name)
|
124
|
+
# columns = self.columns(name) || casts&.keys
|
125
|
+
# column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
|
126
|
+
# if item.is_a? Integer
|
127
|
+
# rv = "$#{item}"
|
128
|
+
# else
|
129
|
+
# item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
|
130
|
+
# rv = "'#{item.to_json.gsub(/'/, "''")}'"
|
131
|
+
# end
|
132
|
+
# Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
|
133
|
+
# end
|
134
|
+
|
135
|
+
def json_recordset(rows = @quotable)
|
136
|
+
case rows
|
137
|
+
when Array, Integer
|
138
|
+
when Hash
|
139
|
+
rows = [rows]
|
140
|
+
else
|
141
|
+
raise ArgumentError, "just Array<Hash> or Hash (for a single value)"
|
142
|
+
end
|
147
143
|
casts = self.casts(name)
|
148
|
-
columns = self.columns(name) || casts&.keys
|
149
|
-
|
150
|
-
|
151
|
-
rv = "$#{item}"
|
144
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
145
|
+
if rows.is_a? Integer
|
146
|
+
rv = "$#{rows}"
|
152
147
|
else
|
153
|
-
|
154
|
-
|
148
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
149
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
150
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
151
|
+
rv = "'#{rows.map{ _1.slice(*columns)}.to_json.gsub(/'/, "''")}'"
|
152
|
+
end
|
153
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
154
|
+
column_cast = columns.map do |column|
|
155
|
+
"#{QuoteSql.quote_column_name column} #{casts&.dig(column, :sql_type) || "TEXT"}"
|
155
156
|
end
|
156
|
-
Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name
|
157
|
+
Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name(name || "json")}(#{column_cast.join(',')})"
|
157
158
|
end
|
158
159
|
|
159
|
-
def
|
160
|
-
|
161
|
-
|
162
|
-
if column_names.is_a? Hash
|
163
|
-
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
164
|
-
column_names = column_names.keys
|
160
|
+
def values(rows = @quotable)
|
161
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
162
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
165
163
|
end
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if item.all? { _1.is_a?(Array) }
|
171
|
-
length, overflow = item.map { _1.length }.uniq
|
172
|
-
raise ArgumentError, "all values need to have the same length" if overflow
|
173
|
-
column_names ||= (1..length).map { "column#{_1}" }
|
174
|
-
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
|
175
|
-
values = item.map { value(_1) }
|
164
|
+
case rows
|
165
|
+
when Array
|
166
|
+
when Hash
|
167
|
+
rows = [rows]
|
176
168
|
else
|
177
|
-
raise ArgumentError, "
|
169
|
+
raise ArgumentError, "just raw or Array<Hash, Integer> or Hash (for a single value)"
|
178
170
|
end
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
171
|
+
casts = self.casts(name)
|
172
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
173
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
174
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
175
|
+
values = rows.each_with_index.map do |row, i|
|
176
|
+
row.transform_keys(&:to_sym)
|
177
|
+
if i == 0 and casts.present?
|
178
|
+
columns.map{ "#{_quote(row[_1])}::#{casts&.dig(_1, :sql_type) || "TEXT"}" }
|
179
|
+
else
|
180
|
+
columns.map{ _quote(row[_1]) }
|
181
|
+
end.then { "(#{_1.join(",")})"}
|
183
182
|
end
|
184
|
-
# values[0] { _1 << types[_1] || ""}
|
185
|
-
Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
|
186
|
-
end
|
187
183
|
|
188
|
-
|
189
|
-
|
190
|
-
when Arel::Nodes::SqlLiteral
|
191
|
-
item = Raw.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
|
192
|
-
return item
|
193
|
-
when Array
|
194
|
-
item.compact!
|
195
|
-
column_names = (@qsql.quotes[:columns] || @qsql.quotes[:column_names]).dup
|
196
|
-
types = []
|
197
|
-
if column_names.is_a? Hash
|
198
|
-
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
199
|
-
column_names = column_names.keys
|
200
|
-
elsif column_names.is_a? Array
|
201
|
-
column_names = column_names.map do |column|
|
202
|
-
types << column.respond_to?(:sql_type) ? "::#{column.sql_type}" : nil
|
203
|
-
column.respond_to?(:name) ? column.name : column
|
204
|
-
end
|
205
|
-
end
|
184
|
+
Raw.sql "(VALUES #{values.join(",")}) AS #{QuoteSql.quote_column_name(name || "values")} (#{columns.map{QuoteSql.quote_column_name(_1)}.join(",")})"
|
185
|
+
end
|
206
186
|
|
207
|
-
if item.all? { _1.is_a?(Hash) }
|
208
|
-
column_names ||= item.flat_map { _1.keys.sort }.uniq
|
209
|
-
item.map! { _1.fetch_values(*column_names) {} }
|
210
|
-
end
|
211
187
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
raise ArgumentError, "Either all type Hash or Array"
|
219
|
-
end
|
220
|
-
if column_names.present?
|
221
|
-
Raw.sql "(#{_ident column_names}) VALUES #{values.join(",")}"
|
222
|
-
else
|
223
|
-
Raw.sql "VALUES #{values.join(",")}"
|
224
|
-
end
|
188
|
+
def insert_values(rows = @quotable)
|
189
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
190
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
191
|
+
end
|
192
|
+
case rows
|
193
|
+
when Array
|
225
194
|
when Hash
|
226
|
-
|
195
|
+
rows = [rows]
|
196
|
+
else
|
197
|
+
raise ArgumentError, "just raw or Array<Hash> or Hash (for a single value)"
|
227
198
|
end
|
228
|
-
end
|
229
199
|
|
230
|
-
|
231
|
-
if
|
232
|
-
|
233
|
-
|
200
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
201
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
202
|
+
casts = self.casts(name)
|
203
|
+
columns = (self.columns(name) || casts&.keys || rows.flat_map { _1.keys.sort }.uniq).map(&:to_sym)
|
204
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
205
|
+
columns -= (casts&.select { _2[:virtual] }&.keys || [])
|
206
|
+
values = rows.map { _value(_1.fetch_values(*columns) { :default }) }
|
207
|
+
Raw.sql("(#{columns.map { QuoteSql.quote_column_name _1 }.join(",")}) VALUES #{values.join(",")}")
|
234
208
|
end
|
235
209
|
|
236
|
-
def json?
|
237
|
-
|
210
|
+
def json?(cast = self.cast)
|
211
|
+
cast.to_s[/jsonb?$/i]
|
238
212
|
end
|
239
213
|
|
240
|
-
private def _quote(item = @quotable
|
241
|
-
|
242
|
-
if cast
|
243
|
-
rv << "::#{cast.upcase}"
|
244
|
-
rv << "[]" * rv.depth if rv[/^ARRAY/]
|
245
|
-
end
|
246
|
-
Raw.sql rv
|
214
|
+
private def _quote(item = @quotable)
|
215
|
+
Raw.sql QuoteSql.quote(item)
|
247
216
|
end
|
248
217
|
|
249
218
|
private def _quote_column_name(name)
|
250
219
|
Raw.sql name.scan(/(?:^|")?([^."]+)/).map { QuoteSql.quote_column_name _1 }.join(".")
|
251
220
|
end
|
252
221
|
|
253
|
-
def
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
222
|
+
private def _quote_array(items)
|
223
|
+
rv = items.map do |i|
|
224
|
+
if i.is_a?(Array)
|
225
|
+
_quote_array(i)
|
226
|
+
elsif self.cast[/jsonb?/i]
|
227
|
+
_quote(i.to_json)
|
228
|
+
else
|
229
|
+
quote(i)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
"[#{rv.join(",")}]"
|
233
|
+
end
|
234
|
+
|
235
|
+
def quote_hash(item)
|
236
|
+
item.compact! if item.delete(nil) == false
|
237
|
+
case self.cast
|
238
|
+
when /hstore/i
|
239
|
+
_quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}" }.join(","))
|
240
|
+
when NilClass, ""
|
241
|
+
"#{_quote(item.to_json)}::JSONB"
|
242
|
+
when /jsonb?/i
|
243
|
+
_quote(item.to_json)
|
265
244
|
end
|
266
245
|
end
|
267
246
|
|
247
|
+
def quote(item = @quotable, cast = nil)
|
248
|
+
Raw.sql case item.class.to_s
|
249
|
+
when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
|
250
|
+
item
|
251
|
+
when "Array"
|
252
|
+
if json? or self.cast.blank?
|
253
|
+
rv = _quote(item.to_json)
|
254
|
+
self.cast.present? ? rv : "#{rv}::JSONB"
|
255
|
+
else
|
256
|
+
"ARRAY#{_quote_array(item)}"
|
257
|
+
end
|
258
|
+
when "Hash"
|
259
|
+
quote_hash(item)
|
260
|
+
else
|
261
|
+
if item.respond_to? :to_sql
|
262
|
+
item.to_sql
|
263
|
+
elsif json?
|
264
|
+
_quote(item.to_json)
|
265
|
+
else
|
266
|
+
_quote(item)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
268
271
|
def column_names(item = @quotable)
|
269
272
|
if item.respond_to?(:column_names)
|
270
273
|
item = item.column_names
|
data/lib/quote_sql/test.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
class QuoteSql::Test
|
2
2
|
private
|
3
|
+
|
3
4
|
def test_columns
|
4
5
|
expected <<~SQL
|
5
6
|
SELECT x, "a", "b", "c", "d"
|
@@ -49,22 +50,46 @@ class QuoteSql::Test
|
|
49
50
|
)
|
50
51
|
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
53
|
+
def test_values_hash_active_record
|
54
|
+
table = create_active_record_class("tasks") do |t|
|
55
|
+
t.text :name
|
56
|
+
t.integer :n1, default: 1, null: false
|
57
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
58
|
+
t.timestamps
|
59
|
+
end
|
60
|
+
updated_at = Date.new(2024,1,1)
|
61
|
+
expected <<~SQL
|
62
|
+
INSERT INTO "tasks" ("id", "name", "n1", "created_at", "updated_at") VALUES (DEFAULT, 'Task1', 1, DEFAULT, DEFAULT), (DEFAULT, 'Task2', DEFAULT, DEFAULT, '2024-01-01')
|
63
|
+
SQL
|
64
|
+
values = [
|
65
|
+
{n1: 1, name: "Task1"},
|
66
|
+
{name: "Task2", updated_at: }
|
67
|
+
]
|
68
|
+
QuoteSql.new(<<~SQL).quote(table:, values:)
|
69
|
+
INSERT INTO %table %values
|
70
|
+
SQL
|
71
|
+
end
|
60
72
|
|
61
|
-
def
|
73
|
+
def test_values_hash_active_record_select_columns
|
74
|
+
table = create_active_record_class("tasks") do |t|
|
75
|
+
t.text :name
|
76
|
+
t.integer :n1, default: 1, null: false
|
77
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
78
|
+
t.timestamps
|
79
|
+
end
|
62
80
|
expected <<~SQL
|
63
|
-
|
81
|
+
INSERT INTO "tasks" ("name") VALUES ('Task1'), ('Task2')
|
82
|
+
SQL
|
83
|
+
values = [
|
84
|
+
{n1: 1, name: "Task1"},
|
85
|
+
{name: "Task2", id: "12345" }
|
86
|
+
]
|
87
|
+
QuoteSql.new(<<~SQL).quote(table:, values:, columns: %i[name])
|
88
|
+
INSERT INTO %table %values
|
64
89
|
SQL
|
65
|
-
"SELECT * FROM %x_values".quote_sql(x_values: [['a', 1, true, nil]])
|
66
90
|
end
|
67
91
|
|
92
|
+
|
68
93
|
def test_from_values_hash_no_columns
|
69
94
|
expected <<~SQL
|
70
95
|
SELECT * FROM (VALUES ('a', 1, true, NULL), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "y" ("a", "b", "c", "d")
|
@@ -93,7 +118,7 @@ class QuoteSql::Test
|
|
93
118
|
) AS "x" ("a", "b", "c", "d")
|
94
119
|
SQL
|
95
120
|
"SELECT * FROM %x_values".quote_sql(
|
96
|
-
|
121
|
+
x_casts: {
|
97
122
|
a: "text",
|
98
123
|
b: "integer",
|
99
124
|
c: "boolean",
|
@@ -106,12 +131,6 @@ class QuoteSql::Test
|
|
106
131
|
])
|
107
132
|
end
|
108
133
|
|
109
|
-
def test_insert_values_array
|
110
|
-
expected <<~SQL
|
111
|
-
INSERT INTO x VALUES ('a', 1, true, NULL)
|
112
|
-
SQL
|
113
|
-
"INSERT INTO x %values".quote_sql(values: [['a', 1, true, nil]])
|
114
|
-
end
|
115
134
|
|
116
135
|
def test_insert_values_hash
|
117
136
|
expected <<~SQL
|
@@ -124,29 +143,91 @@ class QuoteSql::Test
|
|
124
143
|
expected <<~SQL
|
125
144
|
SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"},{"a":"2"}]') as "x" ("a" int, "b" text)
|
126
145
|
SQL
|
127
|
-
"SELECT * FROM %x_json".quote_sql(x_casts: {a: "int", b: "text"}, x_json: [{ a: 1, b: 'foo'}, {a: '2', c: 'bar'}])
|
146
|
+
"SELECT * FROM %x_json".quote_sql(x_casts: { a: "int", b: "text" }, x_json: [{ a: 1, b: 'foo' }, { a: '2', c: 'bar' }])
|
128
147
|
end
|
129
148
|
|
130
149
|
def test_json_insert
|
131
150
|
expected <<~SQL
|
132
|
-
|
151
|
+
INSERT INTO users ("name", "color") SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "json"("name" text,"color" text)
|
133
152
|
SQL
|
134
|
-
|
135
|
-
"INSERT INTO users (
|
153
|
+
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" }
|
154
|
+
"INSERT INTO users (%columns) SELECT * from %json".quote_sql(columns: %i[name color], json:)
|
136
155
|
end
|
137
156
|
|
138
157
|
def test_from_json_bind
|
139
158
|
expected <<~SQL
|
140
|
-
|
159
|
+
Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
|
141
160
|
SQL
|
142
|
-
QuoteSQL("Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
|
161
|
+
QuoteSQL("Select * From %x_json", x_json: 1, x_casts: { a: "int", b: "text", c: "boolean" })
|
143
162
|
end
|
144
163
|
|
145
164
|
def test_insert_json_bind
|
146
165
|
expected <<~SQL
|
147
|
-
|
166
|
+
INSERT INTO table ("a","b","c") Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
|
167
|
+
SQL
|
168
|
+
QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: { a: "int", b: "text", c: "boolean" })
|
169
|
+
end
|
170
|
+
|
171
|
+
def test_cast_values
|
172
|
+
expected <<~SQL
|
173
|
+
SELECT
|
174
|
+
'abc'::TEXT,
|
175
|
+
'"abc"'::JSON,
|
176
|
+
'["cde",null,"fgh"]'::JSONB,
|
177
|
+
ARRAY['cde', NULL, 'fgh']::TEXT[],
|
178
|
+
ARRAY['"cde"', 'null', '"fgh"']::JSON[],
|
179
|
+
'{"foo":"bar","go":1,"strip_null":null}'::JSONB not_compact,
|
180
|
+
'{"foo":"bar","go":1}'::JSON compact,
|
181
|
+
'foo=>bar,go=>1,strip_null=>NULL'::HSTORE,
|
182
|
+
ARRAY[[1,2,3],[1,2,3]]::INT[][]
|
183
|
+
SQL
|
184
|
+
array1 = array2 = array3 = ["cde", nil, "fgh"]
|
185
|
+
array4 = [[1, 2, 3], [1, 2, 3]]
|
186
|
+
hash = { foo: "bar", "go": 1, strip_null: nil }
|
187
|
+
QuoteSQL(<<~SQL, field1: 'abc', array1:, array2:, array3:, array4:, hash:, not_compact: hash, compact: hash.merge(nil => false))
|
188
|
+
SELECT
|
189
|
+
%field1::TEXT,
|
190
|
+
%field1::JSON,
|
191
|
+
%array1,
|
192
|
+
%array2::TEXT[],
|
193
|
+
%array3::JSON[],
|
194
|
+
%not_compact not_compact,
|
195
|
+
%compact::JSON compact,
|
196
|
+
%hash::HSTORE,
|
197
|
+
%array4::INT[][]
|
198
|
+
SQL
|
199
|
+
end
|
200
|
+
|
201
|
+
def test_columns_with_tables
|
202
|
+
expected <<~SQL
|
203
|
+
SELECT "profiles"."a", "profiles"."b",
|
204
|
+
"relationships"."a", "relationships"."b",
|
205
|
+
relationship_timestamp("relationships".*)
|
206
|
+
SQL
|
207
|
+
|
208
|
+
profile_table = "profiles"
|
209
|
+
relationship_table = "relationships"
|
210
|
+
relationship_columns = profile_columns = %i[a b]
|
211
|
+
|
212
|
+
<<~SQL.quote_sql(profile_columns:, profile_table:, relationship_columns:, relationship_table:)
|
213
|
+
SELECT %profile_columns, %relationship_columns,
|
214
|
+
relationship_timestamp(%relationship_table.*)
|
215
|
+
SQL
|
216
|
+
end
|
217
|
+
|
218
|
+
def test_active_record
|
219
|
+
table = create_active_record_class("users") do |t|
|
220
|
+
t.text :first_name
|
221
|
+
t.integer :n1, default: 1, null: false
|
222
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
223
|
+
t.timestamps default: "CURRENT_TIMESTAMP", null: false
|
224
|
+
end
|
225
|
+
expected <<~SQL
|
226
|
+
SELECT "id", "first_name", "n1", "v1", "created_at", "updated_at" FROM "users"
|
227
|
+
SQL
|
228
|
+
<<~SQL.quote_sql(table:)
|
229
|
+
SELECT %columns FROM %table
|
148
230
|
SQL
|
149
|
-
QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
|
150
231
|
end
|
151
232
|
|
152
233
|
# def test_q3
|
@@ -172,7 +253,6 @@ class QuoteSql::Test
|
|
172
253
|
# )
|
173
254
|
# end
|
174
255
|
|
175
|
-
|
176
256
|
public
|
177
257
|
|
178
258
|
def all
|
@@ -187,16 +267,20 @@ class QuoteSql::Test
|
|
187
267
|
def run(name, all = false)
|
188
268
|
name = name.to_s.sub(/^test_/, "")
|
189
269
|
rv = ["🧪 #{name}"]
|
270
|
+
puts(*rv)
|
190
271
|
@expected = nil
|
191
272
|
@test = send("test_#{name}")
|
192
273
|
if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
|
193
|
-
tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
|
194
|
-
columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
|
195
|
-
|
196
|
-
|
274
|
+
# tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
|
275
|
+
# columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
|
276
|
+
#"QuoteSql.new(\"#{@test.original}\").quote(#{{ **tables, **columns, **@test.quotes }.inspect}).to_sql",
|
277
|
+
rv += ["🎯 #{expected}", "✅ #{sql}"]
|
278
|
+
|
197
279
|
@success << rv if @success
|
198
280
|
else
|
199
281
|
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
|
282
|
+
rv << "🎯 " + expected&.gsub(/\s+/, "")&.downcase&.strip
|
283
|
+
rv << "❌ " + sql.gsub(/\s+/, "")&.downcase&.strip
|
200
284
|
@fail << rv if @fail
|
201
285
|
end
|
202
286
|
rescue => exc
|
@@ -214,18 +298,170 @@ class QuoteSql::Test
|
|
214
298
|
@test.to_sql
|
215
299
|
end
|
216
300
|
|
217
|
-
class
|
218
|
-
|
219
|
-
|
301
|
+
class PseudoActiveRecordKlass
|
302
|
+
class Column
|
303
|
+
def initialize(name, type, **options)
|
304
|
+
@name = name.to_s
|
305
|
+
@type = type
|
306
|
+
@null = options[:null]
|
307
|
+
type = options[:type] if @type == :virtual
|
308
|
+
@sql_type = DATATYPES[/^#{type}$/]
|
309
|
+
unless @type == :virtual or options[:default].nil?
|
310
|
+
@default = options[:default]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
attr_reader :name, :type, :sql_type, :null, :default, :default_function
|
315
|
+
|
316
|
+
def default?
|
317
|
+
! (@default || @default_function).nil?
|
318
|
+
end
|
220
319
|
end
|
320
|
+
class Columns
|
321
|
+
def initialize(&block)
|
322
|
+
@rv = []
|
323
|
+
block.call(self)
|
324
|
+
end
|
221
325
|
|
222
|
-
|
223
|
-
|
326
|
+
def to_a
|
327
|
+
@rv
|
328
|
+
end
|
329
|
+
|
330
|
+
def timestamps(**options)
|
331
|
+
@rv << Column.new( :created_at, :timestamp, null: false, **options)
|
332
|
+
@rv << Column.new( :updated_at, :timestamp, null: false, **options)
|
333
|
+
end
|
334
|
+
|
335
|
+
def method_missing(type, name, *args, **options)
|
336
|
+
@rv << Column.new(name, type, *args, **options)
|
337
|
+
end
|
224
338
|
end
|
225
339
|
|
226
|
-
def
|
227
|
-
|
340
|
+
def initialize(table_name, id: :uuid, &block)
|
341
|
+
@table_name = table_name
|
342
|
+
@columns = Columns.new(&block).to_a
|
343
|
+
unless id.nil? or id == false
|
344
|
+
@columns.unshift(Column.new("id", *[id], null: false, default: "gen_random_uuid()"))
|
345
|
+
end
|
228
346
|
end
|
347
|
+
|
348
|
+
attr_reader :table_name, :columns
|
349
|
+
|
350
|
+
def column_names
|
351
|
+
@columns.map { _1.name }
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def create_active_record_class(table_name, **options, &block)
|
356
|
+
PseudoActiveRecordKlass.new(table_name, **options, &block)
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
def datatype
|
361
|
+
errors = {}
|
362
|
+
success = []
|
363
|
+
spaces = ->(*) { " " * (rand(4) + 1) }
|
364
|
+
|
365
|
+
DATATYPES.each_line(chomp: true) do |line|
|
366
|
+
|
367
|
+
l = line.gsub(/\s+/, &spaces).gsub(/(?<=\()\d+|\d+(?=\))/) { "#{spaces.call}#{rand(10) + 1}#{spaces.call}" }.gsub(/\(/) { "#{spaces.call}(" }
|
368
|
+
|
369
|
+
m = "jgj hsgjhsgfjh ag %field::#{l} asldfalskjdfl".match(QuoteSql::CASTS)
|
370
|
+
if m.present? and l == m[1]
|
371
|
+
success << line
|
372
|
+
else
|
373
|
+
errors[line] = m&.to_a
|
374
|
+
end
|
375
|
+
line = line + "[]" * (rand(3) + 1)
|
376
|
+
m = "jgj hsgjhsgfjh ag %field::#{line} asldfalskjdfl".match(QuoteSql::CASTS)
|
377
|
+
if m.present? and line == m[1] + m[2]
|
378
|
+
success << line
|
379
|
+
else
|
380
|
+
errors[line] = m&.to_a
|
381
|
+
end
|
382
|
+
end
|
383
|
+
puts success.sort.inspect
|
384
|
+
ap errors
|
229
385
|
end
|
230
386
|
|
231
|
-
|
387
|
+
DATATYPES = <<-DATATYPES
|
388
|
+
bigint
|
389
|
+
int8
|
390
|
+
bigserial
|
391
|
+
serial8
|
392
|
+
bit
|
393
|
+
bit (1)
|
394
|
+
bit varying
|
395
|
+
varbit
|
396
|
+
bit varying (2)
|
397
|
+
varbit (2)
|
398
|
+
boolean
|
399
|
+
bool
|
400
|
+
box
|
401
|
+
bytea
|
402
|
+
character
|
403
|
+
char
|
404
|
+
character (1)
|
405
|
+
char (1)
|
406
|
+
character varying
|
407
|
+
varchar
|
408
|
+
character varying (1)
|
409
|
+
varchar (1)
|
410
|
+
cidr
|
411
|
+
circle
|
412
|
+
date
|
413
|
+
double precision
|
414
|
+
float8
|
415
|
+
inet
|
416
|
+
integer
|
417
|
+
int
|
418
|
+
int4
|
419
|
+
interval
|
420
|
+
interval (1)
|
421
|
+
json
|
422
|
+
jsonb
|
423
|
+
line
|
424
|
+
lseg
|
425
|
+
macaddr
|
426
|
+
macaddr8
|
427
|
+
money
|
428
|
+
numeric
|
429
|
+
numeric(10,3)
|
430
|
+
decimal
|
431
|
+
decimal(10,3)
|
432
|
+
path
|
433
|
+
pg_lsn
|
434
|
+
pg_snapshot
|
435
|
+
point
|
436
|
+
polygon
|
437
|
+
real
|
438
|
+
float4
|
439
|
+
smallint
|
440
|
+
int2
|
441
|
+
smallserial
|
442
|
+
serial
|
443
|
+
serial2
|
444
|
+
serial4
|
445
|
+
text
|
446
|
+
time
|
447
|
+
time(1)
|
448
|
+
time without time zone
|
449
|
+
time(1) without time zone
|
450
|
+
time with time zone
|
451
|
+
time(2) with time zone
|
452
|
+
timetz
|
453
|
+
timestamp
|
454
|
+
timestamp(1)
|
455
|
+
timestamp without time zone
|
456
|
+
timestamp(1) without time zone
|
457
|
+
timestamp with time zone
|
458
|
+
timestamp(1) with time zone
|
459
|
+
timestamptz
|
460
|
+
tsquery
|
461
|
+
tsvector
|
462
|
+
txid_snapshot
|
463
|
+
uuid
|
464
|
+
xml
|
465
|
+
DATATYPES
|
466
|
+
|
467
|
+
end
|
data/lib/quote_sql/version.rb
CHANGED
data/lib/quote_sql.rb
CHANGED
@@ -4,24 +4,28 @@ Dir.glob(__FILE__.sub(/\.rb$/, "/*.rb")).each { require(_1) unless _1[/(deprecat
|
|
4
4
|
class QuoteSql
|
5
5
|
|
6
6
|
DATA_TYPES_RE = %w(
|
7
|
-
(
|
8
|
-
|
7
|
+
(?>character\\s+varying|bit\\s+varying|character|varbit|varchar|char|bit|interval)(?>\\s*\\(\\s*\\d+\\s*\\))?
|
8
|
+
(?>numeric|decimal)(?>\\s*\\(\\s*\\d+\\s*,\\s*\\d+\\s*\\))?
|
9
|
+
timestamptz timetz
|
10
|
+
time(?>stamp)?(?>\\s*\\(\\s*\\d+\\s*\\))?(?>\\s+with(?>out)?\\s+time\\s+zone)?
|
11
|
+
integer
|
12
|
+
(?>small|big)(?>int|serial)
|
13
|
+
bool(?>ean)? box bytea cidr circle date
|
9
14
|
(?:date|int[48]|num|ts(?:tz)?)(?:multi)?range
|
10
15
|
macaddr8?
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
16
|
+
ts(?>query|vector)
|
17
|
+
float[48]
|
18
|
+
(?:int|serial)[248]?
|
19
|
+
double\\s+precision
|
20
|
+
jsonb json
|
21
|
+
inet
|
22
|
+
line lseg money path
|
23
|
+
pg_lsn pg_snapshot txid_snapshot
|
24
|
+
point polygon real text
|
25
|
+
uuid xml hstore
|
22
26
|
).join("|")
|
23
27
|
|
24
|
-
CASTS = Regexp.new("
|
28
|
+
CASTS = Regexp.new("::(#{DATA_TYPES_RE})((?:\\s*\\[\\s*\\d?\\s*\\])*)", "i")
|
25
29
|
|
26
30
|
def self.conn
|
27
31
|
raise ArgumentError, "You need to define a database connection function"
|
@@ -46,20 +50,49 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
46
50
|
attr_reader :sql, :quotes, :original, :binds, :tables, :columns
|
47
51
|
|
48
52
|
def table(name = nil)
|
49
|
-
@tables[name&.to_sym]
|
53
|
+
table = @tables[name&.to_sym]
|
54
|
+
table.is_a?(Class) ? table : table.dup
|
50
55
|
end
|
51
56
|
|
57
|
+
def table=(value)
|
58
|
+
name, table = value
|
59
|
+
name = name&.to_sym
|
60
|
+
@tables[name] = table
|
61
|
+
if table.respond_to?(:columns)
|
62
|
+
@casts[name] = table.columns.to_h do |c|
|
63
|
+
[c.name.to_sym, { sql_type: c.sql_type, default: (c.default || c.default_function rescue nil).present?, virtual: c.type == :virtual }]
|
64
|
+
end if @casts[name].blank?
|
65
|
+
elsif table.respond_to?(:column_names)
|
66
|
+
@casts[name] = table.column_names.to_h { [_1.to_sym, nil] } if @casts[name].blank?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
alias tables= table=
|
71
|
+
|
72
|
+
def column=(value)
|
73
|
+
name, column = value
|
74
|
+
name = name&.to_sym
|
75
|
+
@columns[name] = column
|
76
|
+
end
|
77
|
+
|
78
|
+
alias columns= column=
|
79
|
+
|
80
|
+
def cast=(value)
|
81
|
+
name, cast = value
|
82
|
+
name = name&.to_sym
|
83
|
+
raise ArgumentError unless cast.is_a?(Hash)
|
84
|
+
(@casts[name] ||= {}).update(cast.transform_values { _1.is_a?(Hash) ? _1 : { sql_type: _1 } })
|
85
|
+
end
|
86
|
+
|
87
|
+
alias casts= cast=
|
88
|
+
|
52
89
|
def columns(name = nil)
|
53
|
-
|
90
|
+
name = name&.to_sym
|
91
|
+
@columns[name] || @casts[name]&.keys&.map(&:to_s)
|
54
92
|
end
|
55
93
|
|
56
94
|
def casts(name = nil)
|
57
|
-
|
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] }
|
61
|
-
end
|
62
|
-
rv
|
95
|
+
@casts[name&.to_sym]
|
63
96
|
end
|
64
97
|
|
65
98
|
# Add quotes keys are symbolized
|
@@ -69,7 +102,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
69
102
|
_, name, type = quote.to_s.match(re)&.to_a
|
70
103
|
value = quotes.delete quote
|
71
104
|
value = Raw.sql(value) if value.class.to_s == "Arel::Nodes::SqlLiteral"
|
72
|
-
|
105
|
+
send(:"#{type}=", [name, value])
|
106
|
+
# instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
|
73
107
|
end
|
74
108
|
@quotes.update quotes.transform_keys(&:to_sym)
|
75
109
|
self
|
@@ -87,7 +121,7 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
87
121
|
if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max != binds.length
|
88
122
|
raise ArgumentError, "Wrong number of binds"
|
89
123
|
end
|
90
|
-
_exec(sql, binds, prepare: false
|
124
|
+
_exec(sql, binds, prepare: false)
|
91
125
|
rescue => exc
|
92
126
|
STDERR.puts exc.inspect, self.inspect
|
93
127
|
raise exc
|
@@ -127,7 +161,7 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
127
161
|
if @binds.length != record.length
|
128
162
|
next RuntimeError.new("binds are not equal arguments, #{record.inspect}")
|
129
163
|
end
|
130
|
-
_exec(sql, record, prepare: false
|
164
|
+
_exec(sql, record, prepare: false)
|
131
165
|
end
|
132
166
|
end
|
133
167
|
|
@@ -152,11 +186,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
152
186
|
def key_matches
|
153
187
|
@sql.scan(MIXIN_RE).map do |full, *key|
|
154
188
|
key = key.compact[0]
|
155
|
-
if m = key.match(/^(.+)#{CASTS}/i)
|
156
|
-
_, key, cast = m.to_a
|
157
|
-
end
|
158
189
|
has_quote = @quotes.key?(key.to_sym) || key.match?(/(table|columns)$/)
|
159
|
-
[full, key,
|
190
|
+
[full, key, has_quote]
|
160
191
|
end
|
161
192
|
end
|
162
193
|
|
@@ -166,27 +197,28 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
166
197
|
loop do
|
167
198
|
s = StringScanner.new(@sql)
|
168
199
|
sql = ""
|
169
|
-
key_matches.each do |key_match, key,
|
170
|
-
s.scan_until(/(.*?)#{key_match}(
|
171
|
-
matched, pre,
|
172
|
-
if m = key.match(/^bind(\d+)?/im)
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
200
|
+
key_matches.each do |key_match, key, has_quote|
|
201
|
+
s.scan_until(/(.*?)#{key_match}(#{CASTS}?)/im)
|
202
|
+
matched, pre, cast = s.matched, s[1], s[2]
|
203
|
+
# if m = key.match(/^bind(\d+)?/im)
|
204
|
+
# if m[1].present?
|
205
|
+
# bind_num = m[1].to_i
|
206
|
+
# @binds[bind_num - 1] ||= cast
|
207
|
+
# raise "bind #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
|
208
|
+
# else
|
209
|
+
# @binds << cast
|
210
|
+
# bind_num = @binds.length
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
|
214
|
+
# els
|
215
|
+
if has_quote
|
216
|
+
quoted = quoter(key, cast)
|
185
217
|
unresolved.delete key
|
186
218
|
if (i = quoted.scan MIXIN_RE).present?
|
187
219
|
unresolved += i.map(&:last)
|
188
220
|
end
|
189
|
-
matched = "#{pre}#{quoted}#{
|
221
|
+
matched = "#{pre}#{quoted}#{cast}"
|
190
222
|
end
|
191
223
|
rescue TypeError
|
192
224
|
ensure
|
@@ -200,8 +232,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
200
232
|
self
|
201
233
|
end
|
202
234
|
|
203
|
-
def quoter(key)
|
204
|
-
quoter = @resolved[key.to_sym] = Quoter.new(self, key, @quotes[key.to_sym])
|
235
|
+
def quoter(key, cast)
|
236
|
+
quoter = @resolved[key.to_sym] = Quoter.new(self, key, cast, @quotes[key.to_sym])
|
205
237
|
quoter.to_sql
|
206
238
|
rescue TypeError => exc
|
207
239
|
@resolved[key.to_sym] = exc
|
@@ -224,8 +256,11 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
|
|
224
256
|
|
225
257
|
def self.test(which = :all)
|
226
258
|
require __dir__ + "/quote_sql/test.rb"
|
227
|
-
|
259
|
+
case which
|
260
|
+
when :all
|
228
261
|
Test.new.all
|
262
|
+
when :datatype
|
263
|
+
Test.new.datatype
|
229
264
|
else
|
230
265
|
Test.new.run(which)
|
231
266
|
end
|
@@ -239,9 +274,3 @@ end
|
|
239
274
|
|
240
275
|
QuoteSql.include QuoteSql::Formater
|
241
276
|
|
242
|
-
class Array
|
243
|
-
def depth
|
244
|
-
select { _1.is_a?(Array) }.map { _1.depth.to_i + 1 }.max || 1
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.8
|
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-
|
11
|
+
date: 2024-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: niceql
|