quote-sql 0.0.2 → 0.0.3

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: 17336c46db1b966512f67b1c4dae714d612460ae61c1f24d75d0fa79153422df
4
+ data.tar.gz: 8b510daed8f21c7733e0b841f0c11f60c8634abde062e3a81dc844b8ed4cb682
5
5
  SHA512:
6
- metadata.gz: 8e13010de0021284de07a99e33822e54b530d3d5dfbc5986028327c903b5c3ab55ceadcf0042eb7207eea8245f31f57db5811a0f902fe8f68eb9a435c02ff255
7
- data.tar.gz: f178c03540bdea0b38c8278ace489954d9efff1786fa76b804950c11bf60f2ac3162f6ca3ac9f142f08dc62d830e10cd8835786618175a922b7eb183e71c89bc
6
+ metadata.gz: 1cc8dd6b2e36c5f5e976027d1f25442cf3cbee76b8bf5bf05b827ba2693d2aed952273491b1abd6404f53482b6da1c0e88e5de5cc9d25ac3af56b65f649f0aa7
7
+ data.tar.gz: 3e4a135613d5c22963030754e1bc21d8ac3d3aaf210abecdd2d8eeb47893f96c3243702c29d138321a122c28ed571670e32c0f703462cf1a1ca41e6b925f6da5
@@ -5,8 +5,16 @@ class QuoteSql
5
5
  @key, @quotable = key, quotable
6
6
  end
7
7
 
8
+ def quotes
9
+ @qsql.quotes
10
+ end
11
+
8
12
  attr_reader :key, :quotable
9
13
 
14
+ def name
15
+ @key.sub(/_[^_]+$/, '')
16
+ end
17
+
10
18
  def to_sql
11
19
  return @quotable.call(self) if @quotable.is_a? Proc
12
20
  case key.to_s
@@ -24,8 +32,10 @@ class QuoteSql
24
32
  quotable.to_s
25
33
  when /(?:^|(.*)_)(raw|sql)$/i
26
34
  quotable.to_s
27
- when /(?:^|(.*)_)(values?)$/i
28
- values
35
+ when /^(.+)_values$/i
36
+ data_values
37
+ when /values$/i
38
+ insert_values
29
39
  else
30
40
  quote
31
41
  end
@@ -56,24 +66,75 @@ class QuoteSql
56
66
 
57
67
  end
58
68
 
59
- def values(item = @quotable)
69
+ def data_values(item = @quotable)
70
+ item = Array(item).compact
71
+ column_names = @qsql.quotes[:"#{name}_columns"].dup
72
+ if column_names.is_a? Hash
73
+ types = column_names.values.map { "::#{_1.upcase}" if _1 }
74
+ column_names = column_names.keys
75
+ end
76
+ if item.all? { _1.is_a?(Hash) }
77
+ column_names ||= item.flat_map { _1.keys.sort }.uniq
78
+ item.map! { _1.fetch_values(*column_names) {} }
79
+ end
80
+ if item.all? { _1.is_a?(Array) }
81
+ length, overflow = item.map { _1.length }.uniq
82
+ raise ArgumentError, "all values need to have the same length" if overflow
83
+ column_names ||= (1..length).map{"column#{_1}"}
84
+ raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
85
+ values = item.map { value(_1) }
86
+ else
87
+ raise ArgumentError, "Either all type Hash or Array"
88
+ end
89
+ if types.present?
90
+ value = values[0][1..-2].split(/\s*,\s*/)
91
+ types.each_with_index { value[_2] << _1 || ""}
92
+ values[0] = "(" + value.join(",") + ")"
93
+ end
94
+ # values[0] { _1 << types[_1] || ""}
95
+ "(VALUES #{values.join(",")}) AS #{ident_name name} (#{ident_name column_names})"
96
+ end
97
+
98
+
99
+ def insert_values(item = @quotable)
60
100
  case item
61
101
  when Arel::Nodes::SqlLiteral
62
102
  item = Arel.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
63
103
  return item
64
104
  when Array
105
+ item.compact!
106
+ column_names = (@qsql.quotes[:columns] || @qsql.quotes[:column_names]).dup
107
+ types = []
108
+ if column_names.is_a? Hash
109
+ types = column_names.values.map { "::#{_1.upcase}" if _1 }
110
+ column_names = column_names.keys
111
+ elsif column_names.is_a? Array
112
+ column_names = column_names.map do |column|
113
+ types << column.respond_to?(:sql_type) ? "::#{column.sql_type}" : nil
114
+ column.respond_to?(:name) ? column.name : column
115
+ end
116
+ end
117
+
118
+ if item.all? { _1.is_a?(Hash) }
119
+ column_names ||= item.flat_map { _1.keys.sort }.uniq
120
+ item.map! { _1.fetch_values(*column_names) {} }
121
+ end
65
122
 
66
- differences = item.map { _1.is_a?(Array) && _1.length }.uniq
67
- if differences.length == 1
68
- item.compact.map { value(_1) }.join(", ")
123
+ if item.all? { _1.is_a?(Array) }
124
+ length, overflow = item.map { _1.length }.uniq
125
+ raise ArgumentError, "all values need to have the same length" if overflow
126
+ raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names and column_names.length != length
127
+ values = item.map { value(_1) }
128
+ else
129
+ raise ArgumentError, "Either all type Hash or Array"
130
+ end
131
+ if column_names.present?
132
+ "(#{ident_name column_names}) VALUES #{values.join(",")}"
69
133
  else
70
- value([item])
134
+ "VALUES #{values.join(",")}"
71
135
  end
72
136
  when Hash
73
137
  value([item])
74
- else
75
- return item.to_sql if item.respond_to? :to_sql
76
- "(" + _quote(item) + ")"
77
138
  end
78
139
  end
79
140
 
@@ -127,7 +188,7 @@ class QuoteSql
127
188
  elsif item.class.respond_to?(:column_names)
128
189
  item = item.class.column_names
129
190
  elsif item.is_a?(Array)
130
- if item[0].respond_to?(:name)
191
+ if item.all?{ _1.respond_to?(:name) }
131
192
  item = item.map(&:name)
132
193
  end
133
194
  end
@@ -1,21 +1,28 @@
1
1
  module QuoteSql::Test
2
2
  def self.all
3
+ @success = []
4
+ @fail = []
3
5
  methods(false).grep(/^test_/).each do |name|
4
- run(name)
5
- puts
6
+ run(name, true)
6
7
  end
7
-
8
+ @success.each { STDOUT.puts(*_1, nil) }
9
+ @fail.each { STDOUT.puts(*_1, nil) }
10
+ puts
8
11
  end
9
12
 
10
- def self.run(name)
13
+ def self.run(name, all)
11
14
  name = name.to_s.sub(/^test_/, "")
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
+
18
+ if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
19
+ rv = [name, @test.original, @test.quotes.inspect, "✅ #{expected}"]
20
+ @success << rv if @success
16
21
  else
17
- STDOUT.puts name, @test.inspect, sql, "❌ #{expected}"
22
+ rv = [name, @test.inspect, sql, "❌ #{expected}"]
23
+ @fail << rv if @fail
18
24
  end
25
+ STDOUT.puts rv unless @fail or @success
19
26
  end
20
27
 
21
28
  def self.expected(v = nil)
@@ -39,9 +46,10 @@ module QuoteSql::Test
39
46
  "SELECT * FROM #{self.class.table_name}"
40
47
  end
41
48
  end
49
+
42
50
  class << self
43
51
  def test_columns_and_table_name_simple
44
- expected Arel.sql(%(SELECT "a","b"."c" FROM "my_table"))
52
+ expected %(SELECT "a","b"."c" FROM "my_table")
45
53
  QuoteSql.new("SELECT %columns FROM %table_name").quote(
46
54
  columns: [:a, b: :c],
47
55
  table_name: "my_table"
@@ -49,7 +57,7 @@ module QuoteSql::Test
49
57
  end
50
58
 
51
59
  def test_columns_and_table_name_complex
52
- expected Arel.sql(%(SELECT "a","b"."c" FROM "table1","table2"))
60
+ expected %(SELECT "a","b"."c" FROM "table1","table2")
53
61
  QuoteSql.new("SELECT %columns FROM %table_names").quote(
54
62
  columns: [:a, b: :c],
55
63
  table_names: ["table1", "table2"]
@@ -57,7 +65,7 @@ module QuoteSql::Test
57
65
  end
58
66
 
59
67
  def test_recursive_injects
60
- expected Arel.sql(%(SELECT TRUE FROM "table1"))
68
+ expected %(SELECT TRUE FROM "table1")
61
69
  QuoteSql.new("SELECT %raw FROM %table_names").quote(
62
70
  raw: "%recurse1_raw",
63
71
  recurse1_raw: "%recurse2",
@@ -67,7 +75,9 @@ module QuoteSql::Test
67
75
  end
68
76
 
69
77
  def test_values
70
- expected Arel.sql(%(SELECT 'a text', 123, 'text' AS abc FROM "my_table"))
78
+ expected <<~SQL
79
+ SELECT 'a text', 123, 'text' AS abc FROM "my_table"
80
+ SQL
71
81
  QuoteSql.new("SELECT %text, %{number}, %aliased_with_hash FROM %table_name").quote(
72
82
  text: "a text",
73
83
  number: 123,
@@ -79,33 +89,92 @@ module QuoteSql::Test
79
89
  end
80
90
 
81
91
  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
- )
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
106
+
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: [
112
+ { a: 'a', b: 1, c: true, d: nil },
113
+ { d: nil, a: 'a', c: true, b: 1 },
114
+ { d: 2, b: 1 }
115
+ ])
86
116
  end
87
117
 
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
- )
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 }])
109
123
  end
124
+
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
142
+
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
149
+
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
156
+
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
110
179
  end
111
180
  end
data/lib/quote_sql.rb CHANGED
@@ -151,18 +151,29 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
151
151
  end
152
152
 
153
153
  class Error < ::RuntimeError
154
- def initialize(quote_sql)
154
+ def initialize(quote_sql, errors)
155
155
  @object = quote_sql
156
+ @errors = errors
156
157
  end
157
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
+
158
169
  def message
159
- super + %Q@<QuoteSql #{@object.original.inspect} #{@object.errors.inspect}>@
170
+ super + %Q@<QuoteSql #{sql} #{@object.errors.inspect}>@
160
171
  end
161
172
  end
162
173
 
163
174
  def to_sql
164
175
  mixin!
165
- raise Error.new(self) if errors?
176
+ raise Error.new(self, errors) if errors?
166
177
  return Arel.sql @sql if defined? Arel
167
178
  @sql
168
179
  end
@@ -224,8 +235,8 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
224
235
  def errors
225
236
  @quotes.to_h do |k, v|
226
237
  r = @resolved[k]
227
- next [nil, nil] unless r.nil? or r.is_a?(Exception)
228
- [k, "#{@quotes[k].inspect} => #{v.inspect}"]
238
+ next [nil, nil] if r.nil? or not r.is_a?(Exception)
239
+ [k, {@quotes[k].inspect => v.inspect, exc: r, backtrace: r.backtrace}]
229
240
  end.compact
230
241
  end
231
242
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quote-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
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
11
+ date: 2024-02-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'QuoteSql helps you creating SQL queries and proper quoting especially
14
14
  with advanced queries.