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.
@@ -1,35 +1,216 @@
1
- module QuoteSql::Test
2
- def self.all
1
+ class QuoteSql::Test
2
+ private
3
+ def test_columns
4
+ expected <<~SQL
5
+ SELECT x, "a", "b", "c", "d"
6
+ SQL
7
+ "SELECT x, %x_columns ".quote_sql(x_columns: %i[a b c d])
8
+ end
9
+
10
+ def test_columns_and_table_name_simple
11
+ expected <<~SQL
12
+ 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"
13
+ SQL
14
+ QuoteSql.new("SELECT %columns FROM %table").quote(
15
+ columns: [:a, "b", "gaga.c", { d: :e, f: "gaga.d", g: Arel.sql("1 + 2") }, Arel.sql("whatever AS raw")],
16
+ table: "my_table"
17
+ )
18
+ end
19
+
20
+ def test_columns_and_table_name_complex
21
+ expected <<~SQL
22
+ SELECT "table1"."a","table1"."c" as "b" FROM "table1","table2"
23
+ SQL
24
+ QuoteSql.new("SELECT %columns FROM %table").quote(
25
+ columns: [:a, b: :c],
26
+ table: ["table1", "table2"]
27
+ )
28
+ end
29
+
30
+ def test_recursive_injects
31
+ expected %(SELECT TRUE FROM "table1")
32
+ QuoteSql.new("SELECT %raw FROM %table").quote(
33
+ raw: "%recurse1_raw",
34
+ recurse1_raw: "%recurse2",
35
+ recurse2: true,
36
+ table: "table1"
37
+ )
38
+ end
39
+
40
+ def test_values
41
+ expected <<~SQL
42
+ SELECT 'a text', 123, '{"abc":"don''t"}'::jsonb FROM "my_table"
43
+ SQL
44
+ QuoteSql.new("SELECT %text, %{number}, %hash FROM %table").quote(
45
+ text: "a text",
46
+ number: 123,
47
+ hash: { abc: "don't" },
48
+ table: "my_table"
49
+ )
50
+ end
51
+
52
+ def test_binds
53
+ expected <<~SQL
54
+ SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
55
+ SQL
56
+ QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
57
+ table: "my_table"
58
+ )
59
+ end
60
+
61
+ def test_from_values_array
62
+ expected <<~SQL
63
+ SELECT * FROM (VALUES ('a',1,TRUE,NULL)) AS "x" ("column1","column2","column3","column4")
64
+ SQL
65
+ "SELECT * FROM %x_values".quote_sql(x_values: [['a', 1, true, nil]])
66
+ end
67
+
68
+ def test_from_values_hash_no_columns
69
+ expected <<~SQL
70
+ SELECT * FROM (VALUES ('a', 1, true, NULL), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "y" ("a", "b", "c", "d")
71
+ SQL
72
+ "SELECT * FROM %y_values".quote_sql(y_values: [
73
+ { a: 'a', b: 1, c: true, d: nil },
74
+ { d: nil, a: 'a', c: true, b: 1 },
75
+ { d: 2, b: 1 }
76
+ ])
77
+ end
78
+
79
+ def test_from_values_hash_with_columns
80
+ expected <<~SQL
81
+ SELECT * FROM (VALUES (NULL, true, 1, 'a')) AS "x" ("d","c","b","a")
82
+ SQL
83
+ "SELECT * FROM %x_values".quote_sql(x_columns: %i[d c b a], x_values: [{ a: 'a', b: 1, c: true, d: nil }])
84
+ end
85
+
86
+ def test_from_values_hash_with_type_columns
87
+ expected <<~SQL
88
+ SELECT *
89
+ FROM (VALUES
90
+ ('a'::TEXT, 1::INTEGER, true::BOOLEAN, NULL::FLOAT),
91
+ ('a', 1, true, NULL),
92
+ (NULL, 1, NULL, 2)
93
+ ) AS "x" ("a", "b", "c", "d")
94
+ SQL
95
+ "SELECT * FROM %x_values".quote_sql(
96
+ x_columns: {
97
+ a: "text",
98
+ b: "integer",
99
+ c: "boolean",
100
+ d: "float"
101
+ },
102
+ x_values: [
103
+ { a: 'a', b: 1, c: true, d: nil },
104
+ { d: nil, a: 'a', c: true, b: 1 },
105
+ { d: 2, b: 1 }
106
+ ])
107
+ end
108
+
109
+ def test_insert_values_array
110
+ expected <<~SQL
111
+ INSERT INTO x VALUES ('a', 1, true, NULL)
112
+ SQL
113
+ "INSERT INTO x %values".quote_sql(values: [['a', 1, true, nil]])
114
+ end
115
+
116
+ def test_insert_values_hash
117
+ expected <<~SQL
118
+ INSERT INTO x ("a", "b", "c", "d") VALUES ('a', 1, true, NULL)
119
+ SQL
120
+ "INSERT INTO x %values".quote_sql(values: [{ a: 'a', b: 1, c: true, d: nil }])
121
+ end
122
+
123
+ def test_from_json
124
+ expected <<~SQL
125
+ SELECT * FROM json_to_recordset('[{"a":1,"b":"foo"},{"a":"2"}]') as "x" ("a" int, "b" text)
126
+ SQL
127
+ "SELECT * FROM %x_json".quote_sql(x_casts: {a: "int", b: "text"}, x_json: [{ a: 1, b: 'foo'}, {a: '2', c: 'bar'}])
128
+ end
129
+
130
+ def test_json_insert
131
+ expected <<~SQL
132
+ INSERT INTO users (name, color) SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "x"("name" text,"color" text)
133
+ SQL
134
+ 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"}
135
+ "INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: {name: "text", color: "text"}, x_json:)
136
+ end
137
+
138
+ def test_from_json_bind
139
+ expected <<~SQL
140
+ Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
141
+ SQL
142
+ QuoteSQL("Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
143
+ end
144
+
145
+ def test_insert_json_bind
146
+ expected <<~SQL
147
+ INSERT INTO table ("a","b","c") Select * From json_to_recordset($1) AS "x"("a" int,"b" text,"c" boolean)
148
+ SQL
149
+ QuoteSQL("INSERT INTO table (%x_columns) Select * From %x_json", x_json: 1, x_casts: {a: "int", b: "text", c: "boolean"})
150
+ end
151
+
152
+ # def test_q3
153
+ # expected Arel.sql(<<-SQL)
154
+ # INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
155
+ # VALUES (NULL,TRUE,'A','[5,5]','{"a":1}'),
156
+ # (1,FALSE,'B','[]','{"a":2}'),
157
+ # (2,NULL,'c','[1,2,3]','{"a":3}')
158
+ # ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
159
+ # SQL
160
+ #
161
+ # QuoteSql.new(<<-SQL).
162
+ # INSERT INTO %table (%columns) VALUES %values
163
+ # ON CONFLICT (responses_task_id_index_unique) DO NOTHING;
164
+ # SQL
165
+ # quote(
166
+ # table: Response,
167
+ # values: [
168
+ # [nil, true, "A", [5, 5], { a: 1 }],
169
+ # [1, false, "B", [], { a: 2 }],
170
+ # [2, nil, "c", [1, 2, 3], { a: 3 }]
171
+ # ]
172
+ # )
173
+ # end
174
+
175
+
176
+ public
177
+
178
+ def all
3
179
  @success = []
4
180
  @fail = []
5
- methods(false).grep(/^test_/).each do |name|
6
- run(name, true)
7
- end
181
+ private_methods(false).grep(/^test_/).each { run(_1, true) }
8
182
  @success.each { STDOUT.puts(*_1, nil) }
9
183
  @fail.each { STDOUT.puts(*_1, nil) }
10
184
  puts
11
185
  end
12
186
 
13
- def self.run(name, all)
187
+ def run(name, all = false)
14
188
  name = name.to_s.sub(/^test_/, "")
189
+ rv = ["🧪 #{name}"]
15
190
  @expected = nil
16
191
  @test = send("test_#{name}")
17
-
18
192
  if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
19
- rv = [name, @test.original, @test.quotes.inspect, "✅ #{expected}"]
193
+ tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
194
+ columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
195
+ rv += [
196
+ "QuoteSql.new(\"#{@test.original}\").quote(#{{**tables, **columns, **@test.quotes }.inspect}).to_sql", "🎯 #{expected}", "✅ #{sql}"]
20
197
  @success << rv if @success
21
198
  else
22
- rv = [name, @test.inspect, sql, "❌ #{expected}"]
199
+ rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
23
200
  @fail << rv if @fail
24
201
  end
25
- STDOUT.puts rv unless @fail or @success
202
+ rescue => exc
203
+ rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}", exc.message]
204
+ @fail << rv if @fail
205
+ ensure
206
+ STDOUT.puts(*rv) unless @fail or @success
26
207
  end
27
208
 
28
- def self.expected(v = nil)
209
+ def expected(v = nil)
29
210
  @expected ||= v
30
211
  end
31
212
 
32
- def self.sql
213
+ def sql
33
214
  @test.to_sql
34
215
  end
35
216
 
@@ -47,134 +228,4 @@ module QuoteSql::Test
47
228
  end
48
229
  end
49
230
 
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
58
-
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
66
-
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
76
-
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
90
-
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
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
- ])
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
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
179
- end
180
231
  end
@@ -0,0 +1,3 @@
1
+ class QuoteSql
2
+ VERSION = "0.0.5"
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
@@ -178,14 +82,14 @@ time(stamp)?(_\\(\d+\\))?(_with(out)?_time_zone)?
178
82
  @sql
179
83
  end
180
84
 
181
- def result(binds = [], prepare: false, async: false)
85
+ def result(*binds, prepare: false, async: false)
182
86
  sql = to_sql
183
- if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max + 1 != binds.length
87
+ if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max != binds.length
184
88
  raise ArgumentError, "Wrong number of binds"
185
89
  end
186
90
  _exec(sql, binds, prepare: false, async: false)
187
91
  rescue => exc
188
- STDERR.puts exc.sql
92
+ STDERR.puts exc.inspect, self.inspect
189
93
  raise exc
190
94
  end
191
95
 
@@ -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