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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17336c46db1b966512f67b1c4dae714d612460ae61c1f24d75d0fa79153422df
4
- data.tar.gz: 8b510daed8f21c7733e0b841f0c11f60c8634abde062e3a81dc844b8ed4cb682
3
+ metadata.gz: 70f787b04560e3b297b4a392eb72c0e85be57e349a0533366056fe485f339401
4
+ data.tar.gz: ad8a2e347c97e12b78a264e67d0e12649e838127a0079d8ff96bf5ac51764b40
5
5
  SHA512:
6
- metadata.gz: 1cc8dd6b2e36c5f5e976027d1f25442cf3cbee76b8bf5bf05b827ba2693d2aed952273491b1abd6404f53482b6da1c0e88e5de5cc9d25ac3af56b65f649f0aa7
7
- data.tar.gz: 3e4a135613d5c22963030754e1bc21d8ac3d3aaf210abecdd2d8eeb47893f96c3243702c29d138321a122c28ed571670e32c0f703462cf1a1ca41e6b925f6da5
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
@@ -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
- IO.popen(PG_FORMAT_BIN, "r+", err: "/dev/null") do |f|
13
- f.write(sql)
14
- f.close_write
15
- f.read
16
- end
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
@@ -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
- attr_reader :key, :quotable
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 name
15
- @key.sub(/_[^_]+$/, '')
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
- table
23
- when /(?:^|(.*)_)columns?$/i
24
- columns
25
- when /(?:^|(.*)_)(table_name?s?)$/i
26
- table_name
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.to_s
89
+ quotable
33
90
  when /(?:^|(.*)_)(raw|sql)$/i
34
- quotable.to_s
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
- private def value(ary)
45
- column_names = @qsql.column_names
46
- if ary.is_a?(Hash) and column_names.present?
47
- ary = @qsql.column_names.map do |column_name|
48
- if ary.key? column_name&.to_sym
49
- ary[column_name.to_sym]
50
- elsif column_name[/^(created|updated)_at$/]
51
- :current_timestamp
52
- else
53
- :default
54
- end
55
- end
56
- end
57
- "(" + ary.map do |i|
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.join(",") + ")"
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 = @qsql.quotes[:"#{name}_columns"].dup
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 #{ident_name name} (#{ident_name column_names})"
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 = Arel.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
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
- "(#{ident_name column_names}) VALUES #{values.join(",")}"
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, column = nil)
161
- name, column = name.to_s.split(".") if column.nil?
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
- return _quote(item.to_json) if json?
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
- ident_name(item)
269
+ _ident(item)
209
270
  end
210
271
 
211
- def json_build_object(h)
212
- compact = h.delete(nil) == false
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 ident_name(item = @quotable)
219
- case item
220
- when Array
221
- item.map do |item|
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 table(item = @quotable)
252
- @qsql.table_name ||= if item.respond_to?(:table_name)
253
- item = item.table_name
254
- elsif item.class.respond_to?(:table_name)
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 table_name(item = @quotable)
261
- case item
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
@@ -1,35 +1,40 @@
1
- module QuoteSql::Test
2
- def self.all
1
+ class QuoteSql::Test
2
+
3
+ def all
3
4
  @success = []
4
5
  @fail = []
5
- methods(false).grep(/^test_/).each do |name|
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 self.run(name, all)
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
- rv = [name, @test.original, @test.quotes.inspect, "✅ #{expected}"]
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 = [name, @test.inspect, sql, "❌ #{expected}"]
23
+ rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
23
24
  @fail << rv if @fail
24
25
  end
25
- STDOUT.puts rv unless @fail or @success
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 self.expected(v = nil)
33
+ def expected(v = nil)
29
34
  @expected ||= v
30
35
  end
31
36
 
32
- def self.sql
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
- class << self
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
- def test_columns_and_table_name_complex
60
- expected %(SELECT "a","b"."c" FROM "table1","table2")
61
- QuoteSql.new("SELECT %columns FROM %table_names").quote(
62
- columns: [:a, b: :c],
63
- table_names: ["table1", "table2"]
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
- def test_recursive_injects
68
- expected %(SELECT TRUE FROM "table1")
69
- QuoteSql.new("SELECT %raw FROM %table_names").quote(
70
- raw: "%recurse1_raw",
71
- recurse1_raw: "%recurse2",
72
- recurse2: true,
73
- table_names: "table1"
74
- )
75
- 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
76
73
 
77
- def test_values
78
- expected <<~SQL
79
- SELECT 'a text', 123, 'text' AS abc FROM "my_table"
80
- SQL
81
- QuoteSql.new("SELECT %text, %{number}, %aliased_with_hash FROM %table_name").quote(
82
- text: "a text",
83
- number: 123,
84
- aliased_with_hash: {
85
- abc: "text"
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
- def test_binds
92
- expected <<~SQL
93
- SELECT $1, $2, $1 AS get_bind_1_again FROM "my_table"
94
- SQL
95
- QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table_name").quote(
96
- table_name: "my_table"
97
- )
98
- end
99
-
100
- def test_from_values_array
101
- expected <<~SQL
102
- SELECT * FROM (VALUES ('a',1,TRUE,NULL)) AS "x" ("column1","column2","column3","column4")
103
- SQL
104
- "SELECT * FROM %x_values".quote_sql(x_values: [['a', 1, true, nil]])
105
- end
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
- def test_from_values_hash_no_columns
108
- expected <<~SQL
109
- SELECT * FROM (VALUES ('a', 1, true, NULL), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "y" ("a", "b", "c", "d")
110
- SQL
111
- "SELECT * FROM %y_values".quote_sql(y_values: [
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
- end
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
- def test_from_values_hash_with_type_columns
126
- expected <<~SQL
127
- SELECT * FROM (VALUES ('a'::TEXT, 1::INTEGER, true::BOOLEAN, NULL::FLOAT), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "x" ("a", "b", "c", "d")
128
- SQL
129
- "SELECT * FROM %x_values".quote_sql(
130
- x_columns: {
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
- def test_insert_values_array
144
- expected <<~SQL
145
- INSERT INTO x VALUES ('a', 1, true, NULL)
146
- SQL
147
- "INSERT INTO x %values".quote_sql(values: [['a', 1, true, nil]])
148
- end
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
- def test_insert_values_hash
151
- expected <<~SQL
152
- INSERT INTO x ("a", "b", "c", "d") VALUES ('a', 1, true, NULL)
153
- SQL
154
- "INSERT INTO x %values".quote_sql(values: [{ a: 'a', b: 1, c: true, d: nil }])
155
- end
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
- # def test_q3
158
- # expected Arel.sql(<<-SQL)
159
- # INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
160
- # VALUES (NULL,TRUE,'A','[5,5]','{"a":1}'),
161
- # (1,FALSE,'B','[]','{"a":2}'),
162
- # (2,NULL,'c','[1,2,3]','{"a":3}')
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
@@ -0,0 +1,3 @@
1
+ class QuoteSql
2
+ VERSION = "0.0.4"
3
+ 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 table_name
128
- return @table_name if @table_name
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 column_names
134
- return @column_names if @column_names
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
- # Add quotes keys are symbolized
144
- def quote(quotes1 = {}, **quotes2)
145
- quotes = @quotes.merge(quotes1, quotes2).transform_keys(&:to_sym)
146
- if table = quotes.delete(:table)
147
- columns = quotes.delete(:columns) || table.columns
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
- @quotes = { table:, columns:, **quotes }
150
- self
62
+ rv
151
63
  end
152
64
 
153
- class Error < ::RuntimeError
154
- def initialize(quote_sql, errors)
155
- @object = quote_sql
156
- @errors = errors
157
- end
158
-
159
- attr_reader :object, :errors
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
- [full, key, @quotes.key?(key.to_sym)]
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+)?(?:#{CASTS})?$/im)
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 "cast #{bind_num} already set to #{@binds[bind_num - 1]}" unless @binds[bind_num - 1] == cast
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
- matched = "#{pre}$#{bind_num}#{post}"
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
- def self.test
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
- Test
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.3
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-24 00:00:00.000000000 Z
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/deprecated.rb
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.4.19
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
@@ -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