quote-sql 0.0.3 → 0.0.5

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: cb47e85b4c7c4e1f945748295390c9e1bd49b7e0ba5291e0f7310058f6f91ddc
4
+ data.tar.gz: 0f5bde1afc31eb573bb3e24727b530bf5909048284114d45c56c1b24a63e97aa
5
5
  SHA512:
6
- metadata.gz: 1cc8dd6b2e36c5f5e976027d1f25442cf3cbee76b8bf5bf05b827ba2693d2aed952273491b1abd6404f53482b6da1c0e88e5de5cc9d25ac3af56b65f649f0aa7
7
- data.tar.gz: 3e4a135613d5c22963030754e1bc21d8ac3d3aaf210abecdd2d8eeb47893f96c3243702c29d138321a122c28ed571670e32c0f703462cf1a1ca41e6b925f6da5
6
+ metadata.gz: 6596b9bb50ee373030e401dcb9d0067ae31857b6467fdd7b0341e21b3dcbe8fb0e78714ec24f56615618bbf965849307474a4030978e6c98d2d728166e235151
7
+ data.tar.gz: f38366de3e2227835fd11f512a9c9d2a99d130d896a17f6b0a58788dd8ed7ebd13f46682992b762e2d50f09716c3bd5654d525dc4b03f350d3a52cc6b19b322e
data/README.md CHANGED
@@ -9,13 +9,15 @@ I created this library while coding for different projects, and had lots of Here
9
9
 
10
10
  My strategy is to segment SQL Queries in readable junks, which can be individually tested and then combine their sql to the final query.
11
11
 
12
- QuoteSql is used in production, but is still bleeding edge - and there is not a fully sync between doc and code.
13
-
14
12
  If you think QuoteSql is interesting, let's chat!
15
13
  Also if you have problems using it, just drop me a note.
16
14
 
17
15
  Best Martin
18
16
 
17
+ ## Caveats & Notes
18
+ - QuoteSql is used in production, but is still bleeding edge - and there is not a fully sync between doc and code.
19
+ - Just for my examples and in the docs, I'm using for Yajl for JSON parsing, and changed in my environments the standard parse output to *symbolized keys*.
20
+
19
21
  ## Examples
20
22
  ### Simple quoting
21
23
  `QuoteSql.new("SELECT %field").quote(field: "abc").to_sql`
@@ -119,6 +121,12 @@ with optional array dimension
119
121
  - +String+ value will become the expression, the key the AS {result: "SUM(*)"} => SUM(*) AS result
120
122
  - +Proc+ are executed with the +QuoteSQL::Quoter+ object as parameter and added as raw SQL
121
123
 
124
+ ## Executing
125
+ ### Getting the results
126
+ ### Binds
127
+ `v = {a: 1, b: "foo", c: true};QuoteSQL(%q{Select * From %x_json}, x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"}).result(v.to_json)`
128
+ => Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean) => [{a: 1, b: "foo", c: true}]
129
+
122
130
  ## Shortcuts and functions
123
131
  - `QuoteSQL("select %abc", abc: 1)` == `QuoteSql.new("select %abc").quote(abc: 1)`
124
132
  - when you have in your initializer `String.include QuoteSql::Extension` you can do e.g. `"select %abc".quote_sql(abc: 1)`
@@ -127,7 +135,11 @@ with optional array dimension
127
135
  ## Debug and dump
128
136
  If you have pg_format installed you can get the resulting query inspected:
129
137
  `QuoteSql.new("select %abc").quote(abc: 1).dsql`
130
-
138
+
139
+ # Test
140
+ Minimal tests you can run by
141
+ `QuoteSql.test.all`
142
+ You can find them in /lib/quote_sql/test.rb
131
143
 
132
144
  ## Installing
133
145
  `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,98 @@ 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
+ unless item = casts(name || self.name)&.keys
41
+ if (table = self.table(name || self.name))&.respond_to? :column_names
42
+ item = table.column_names
43
+ else
44
+ raise ArgumntError, "No columns, casts or table given for #{name}" unless table&.respond_to? :column_names
45
+ end
46
+ end
47
+ end
48
+ if item.is_a?(Array)
49
+ if item.all? { _1.respond_to?(:name) }
50
+ item = item.map(&:name)
51
+ end
52
+ end
53
+ _ident(item)
54
+ end
55
+
56
+ def _quote_ident(item)
57
+ Raw.sql case item.class.to_s
58
+ when "QuoteSql::Raw", "Arel::Nodes::SqlLiteral" then item
59
+ when "Hash" then json_hash_ident(item)
60
+ when "Array" then json_array_ident(item)
61
+ when "Proc" then item.call(self)
62
+ when "Integer" then "$#{item}"
63
+ when "Symbol" then [ident_table(0).presence, _quote_ident(item.to_s)].compact.join(".")
64
+ when "String" then item.scan(/(?:^|")?([^."]+)/).flatten.map { QuoteSql.quote_column_name _1 }.join(".")
65
+ else raise ArgumentError, "just Hash, Array, Arel::Nodes::SqlLiteral, QuoteSql::Raw, String, Symbol, Proc, Integer, or responding to #to_sql"
66
+ end
67
+ end
13
68
 
14
- def name
15
- @key.sub(/_[^_]+$/, '')
69
+ def _ident(item = @quotable)
70
+ return Raw.sql(item) if item.respond_to?(:to_sql)
71
+ rv = case item.class.to_s
72
+ when "Array"
73
+ item.map { _1.is_a?(Hash) ? _ident(_1) : _quote_ident(_1) }.join(",")
74
+ when "Hash"
75
+ item.map { "#{_quote_ident(_2)} AS \"#{_1}\"" }.join(",")
76
+ else
77
+ _quote_ident(item)
78
+ # _quote_column_name(item)
79
+ end
80
+ Raw.sql rv
16
81
  end
17
82
 
18
83
  def to_sql
19
84
  return @quotable.call(self) if @quotable.is_a? Proc
20
85
  case key.to_s
21
86
  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
87
+ ident_table
88
+ when /(?:^|(.*)_)columns$/i
89
+ ident_columns
90
+ when /(?:^|(.*)_)(ident)$/i
91
+ _ident
31
92
  when /(?:^|(.*)_)constraints?$/i
32
- quotable.to_s
93
+ quotable
33
94
  when /(?:^|(.*)_)(raw|sql)$/i
34
- quotable.to_s
95
+ quotable
96
+ when /^(.+)_json$/i
97
+ data_json
35
98
  when /^(.+)_values$/i
36
99
  data_values
37
100
  when /values$/i
@@ -41,20 +104,34 @@ class QuoteSql
41
104
  end
42
105
  end
43
106
 
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|
107
+ ###############
108
+
109
+ private def value(values)
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
+
134
+ rv ||= values.map do |i|
58
135
  case i
59
136
  when :default, :current_timestamp
60
137
  next i.to_s.upcase
@@ -62,13 +139,26 @@ class QuoteSql
62
139
  i = i.to_json
63
140
  end
64
141
  _quote(i)
65
- end.join(",") + ")"
142
+ end
143
+ Raw.sql "(#{rv.join(",")})"
144
+ end
66
145
 
146
+ def data_json(item = @quotable)
147
+ casts = self.casts(name)
148
+ columns = self.columns(name) || casts&.keys
149
+ column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
150
+ if item.is_a? Integer
151
+ rv = "$#{item}"
152
+ else
153
+ item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
154
+ rv = "'#{item.to_json.gsub(/'/, "''")}'"
155
+ end
156
+ Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
67
157
  end
68
158
 
69
159
  def data_values(item = @quotable)
70
160
  item = Array(item).compact
71
- column_names = @qsql.quotes[:"#{name}_columns"].dup
161
+ column_names = columns(name)
72
162
  if column_names.is_a? Hash
73
163
  types = column_names.values.map { "::#{_1.upcase}" if _1 }
74
164
  column_names = column_names.keys
@@ -80,7 +170,7 @@ class QuoteSql
80
170
  if item.all? { _1.is_a?(Array) }
81
171
  length, overflow = item.map { _1.length }.uniq
82
172
  raise ArgumentError, "all values need to have the same length" if overflow
83
- column_names ||= (1..length).map{"column#{_1}"}
173
+ column_names ||= (1..length).map { "column#{_1}" }
84
174
  raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
85
175
  values = item.map { value(_1) }
86
176
  else
@@ -88,18 +178,17 @@ class QuoteSql
88
178
  end
89
179
  if types.present?
90
180
  value = values[0][1..-2].split(/\s*,\s*/)
91
- types.each_with_index { value[_2] << _1 || ""}
181
+ types.each_with_index { value[_2] << _1 || "" }
92
182
  values[0] = "(" + value.join(",") + ")"
93
183
  end
94
184
  # values[0] { _1 << types[_1] || ""}
95
- "(VALUES #{values.join(",")}) AS #{ident_name name} (#{ident_name column_names})"
185
+ Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
96
186
  end
97
187
 
98
-
99
188
  def insert_values(item = @quotable)
100
189
  case item
101
190
  when Arel::Nodes::SqlLiteral
102
- item = Arel.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
191
+ item = Raw.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
103
192
  return item
104
193
  when Array
105
194
  item.compact!
@@ -129,9 +218,9 @@ class QuoteSql
129
218
  raise ArgumentError, "Either all type Hash or Array"
130
219
  end
131
220
  if column_names.present?
132
- "(#{ident_name column_names}) VALUES #{values.join(",")}"
221
+ Raw.sql "(#{_ident column_names}) VALUES #{values.join(",")}"
133
222
  else
134
- "VALUES #{values.join(",")}"
223
+ Raw.sql "VALUES #{values.join(",")}"
135
224
  end
136
225
  when Hash
137
226
  value([item])
@@ -151,51 +240,31 @@ class QuoteSql
151
240
  private def _quote(item = @quotable, cast = self.cast)
152
241
  rv = QuoteSql.quote(item)
153
242
  if cast
154
- rv << "::#{cast}"
243
+ rv << "::#{cast.upcase}"
155
244
  rv << "[]" * rv.depth if rv[/^ARRAY/]
156
245
  end
157
- rv
246
+ Raw.sql rv
158
247
  end
159
248
 
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)
249
+ private def _quote_column_name(name)
250
+ Raw.sql name.scan(/(?:^|")?([^."]+)/).map { QuoteSql.quote_column_name _1 }.join(".")
165
251
  end
166
252
 
167
253
  def quote(item = @quotable)
168
- case item
169
- when Arel::Nodes::SqlLiteral
170
- return item
171
- when Array
254
+ case item.class.to_s
255
+ when "Arel::Nodes::SqlLiteral", "QuoteSql::Raw"
256
+ return Raw.sql(item)
257
+ when "Array"
172
258
  return _quote(item.to_json) if json?
173
259
  _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(",")
260
+ when "Hash"
261
+ _quote(item.to_json, :jsonb)
179
262
  else
180
- return item.to_sql if item.respond_to? :to_sql
263
+ return Raw.sql item.to_sql if item.respond_to? :to_sql
181
264
  _quote(item)
182
265
  end
183
266
  end
184
267
 
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
268
  def column_names(item = @quotable)
200
269
  if item.respond_to?(:column_names)
201
270
  item = item.column_names
@@ -205,70 +274,50 @@ class QuoteSql
205
274
  item = item.map(&:name)
206
275
  end
207
276
  @qsql.column_names ||= item
208
- ident_name(item)
277
+ _ident(item)
209
278
  end
210
279
 
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})"
280
+ def json_array_values(h)
281
+ Raw.sql "'#{h.to_json.gsub(/'/, "''")}'::JSONB"
216
282
  end
217
283
 
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
284
+ def json_hash_values(h)
285
+ compact = h.delete(nil) == false
286
+ rv = json_array_values(h)
287
+ Raw.sql(compact ? "jsonb_strip_nulls(#{rv})" : rv)
249
288
  end
250
289
 
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)
290
+ def json_hash_ident(h)
291
+ compact = h.delete(nil) == false
292
+ rv = "jsonb_build_object(" + h.map { "'#{_1.to_s.gsub(/'/, "''")}', #{_ident(_2)}" }.join(",") + ")"
293
+ Raw.sql(compact ? "jsonb_strip_nulls(#{rv})" : rv)
258
294
  end
259
295
 
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
296
+ def json_array_ident(h)
297
+ Raw.sql "jsonb_build_array(#{h.map { _ident(_2) }.join(",")})"
272
298
  end
299
+
300
+ # def table(item = @quotable)
301
+ # @qsql.table_name ||= if item.respond_to?(:table_name)
302
+ # item = item.table_name
303
+ # elsif item.class.respond_to?(:table_name)
304
+ # item = item.class.table_name
305
+ # end
306
+ # table_name(item || @qsql.table_name)
307
+ # end
308
+ #
309
+ # def table_name(item = @quotable)
310
+ # case item
311
+ # when Array
312
+ # item.map do |item|
313
+ # item.is_a?(Hash) ? table_name(item) : _quote_column_name(item)
314
+ # end.join(",")
315
+ # when Hash
316
+ # raise NotImplementedError, "table name is a Hash"
317
+ # # perhaps as ...
318
+ # else
319
+ # _quote_column_name(item)
320
+ # end
321
+ # end
273
322
  end
274
323
  end