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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e32e2e164efd0cfb007de82abb8870b13d5b432856d21af6f75faed9d2d1107
4
- data.tar.gz: 141ef285fbb563e649d5c4b71faa8b1d9d67066d1c600dd9d85c7dd5ee017edc
3
+ metadata.gz: 70f787b04560e3b297b4a392eb72c0e85be57e349a0533366056fe485f339401
4
+ data.tar.gz: ad8a2e347c97e12b78a264e67d0e12649e838127a0079d8ff96bf5ac51764b40
5
5
  SHA512:
6
- metadata.gz: 8e13010de0021284de07a99e33822e54b530d3d5dfbc5986028327c903b5c3ab55ceadcf0042eb7207eea8245f31f57db5811a0f902fe8f68eb9a435c02ff255
7
- data.tar.gz: f178c03540bdea0b38c8278ace489954d9efff1786fa76b804950c11bf60f2ac3162f6ca3ac9f142f08dc62d830e10cd8835786618175a922b7eb183e71c89bc
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,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
- table
15
- when /(?:^|(.*)_)columns?$/i
16
- columns
17
- when /(?:^|(.*)_)(table_name?s?)$/i
18
- table_name
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.to_s
89
+ quotable
25
90
  when /(?:^|(.*)_)(raw|sql)$/i
26
- quotable.to_s
27
- when /(?:^|(.*)_)(values?)$/i
28
- values
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
- private def value(ary)
35
- column_names = @qsql.column_names
36
- if ary.is_a?(Hash) and column_names.present?
37
- ary = @qsql.column_names.map do |column_name|
38
- if ary.key? column_name&.to_sym
39
- ary[column_name.to_sym]
40
- elsif column_name[/^(created|updated)_at$/]
41
- :current_timestamp
42
- else
43
- :default
44
- end
45
- end
46
- end
47
- "(" + 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|
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.join(",") + ")"
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
- def values(item = @quotable)
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 = Arel.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
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
- differences = item.map { _1.is_a?(Array) && _1.length }.uniq
67
- if differences.length == 1
68
- item.compact.map { value(_1) }.join(", ")
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
- value([item])
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, column = nil)
100
- name, column = name.to_s.split(".") if column.nil?
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
- return _quote(item.to_json) if json?
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
- ident_name(item)
269
+ _ident(item)
148
270
  end
149
271
 
150
- def json_build_object(h)
151
- compact = h.delete(nil) == false
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 ident_name(item = @quotable)
158
- case item
159
- when Array
160
- item.map do |item|
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 table(item = @quotable)
191
- @qsql.table_name ||= if item.respond_to?(:table_name)
192
- item = item.table_name
193
- elsif item.class.respond_to?(:table_name)
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 table_name(item = @quotable)
200
- case item
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
@@ -1,28 +1,40 @@
1
- module QuoteSql::Test
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 self.run(name)
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
- STDOUT.puts name, @test.original, @test.quotes.inspect, "✅ #{expected}"
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
- STDOUT.puts name, @test.inspect, sql, "❌ #{expected}"
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 self.expected(v = nil)
33
+ def expected(v = nil)
22
34
  @expected ||= v
23
35
  end
24
36
 
25
- def self.sql
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
- def test_columns_and_table_name_complex
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
- def test_recursive_injects
60
- expected Arel.sql(%(SELECT TRUE FROM "table1"))
61
- QuoteSql.new("SELECT %raw FROM %table_names").quote(
62
- raw: "%recurse1_raw",
63
- recurse1_raw: "%recurse2",
64
- recurse2: true,
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
- def test_values
70
- expected Arel.sql(%(SELECT 'a text', 123, 'text' AS abc FROM "my_table"))
71
- QuoteSql.new("SELECT %text, %{number}, %aliased_with_hash FROM %table_name").quote(
72
- text: "a text",
73
- number: 123,
74
- aliased_with_hash: {
75
- abc: "text"
76
- },
77
- table_name: "my_table"
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
- def test_binds
82
- expected Arel.sql(%(SELECT $1, $2, $1 AS get_bind_1_again FROM "my_table"))
83
- QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table_name").quote(
84
- table_name: "my_table"
85
- )
86
- 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
87
83
 
88
- def test_q3
89
- expected Arel.sql(<<-SQL)
90
- INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
91
- VALUES (NULL,TRUE,'A','[5,5]','{"a":1}'),
92
- (1,FALSE,'B','[]','{"a":2}'),
93
- (2,NULL,'c','[1,2,3]','{"a":3}')
94
- ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
95
- SQL
96
-
97
- QuoteSql.new(<<-SQL).
98
- INSERT INTO %table (%columns) VALUES %values
99
- ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
100
- SQL
101
- quote(
102
- table: Response,
103
- values: [
104
- [nil, true, "A", [5, 5], { a: 1 }],
105
- [1, false, "B", [], { a: 2 }],
106
- [2, nil, "c", [1, 2, 3], { a: 3 }]
107
- ]
108
- )
109
- 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]])
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
@@ -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,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 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)
155
- @object = quote_sql
156
- end
157
-
158
- def message
159
- super + %Q@<QuoteSql #{@object.original.inspect} #{@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
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] unless r.nil? or r.is_a?(Exception)
228
- [k, "#{@quotes[k].inspect} => #{v.inspect}"]
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
- [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]
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+)?(?:#{CASTS})?$/im)
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 "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
262
177
  else
263
178
  @binds << cast
264
179
  bind_num = @binds.length
265
180
  end
266
- matched = "#{pre}$#{bind_num}#{post}"
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
- 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)
298
226
  require __dir__ + "/quote_sql/test.rb"
299
- Test
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.2
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-23 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