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 +4 -4
- data/README.md +15 -3
- data/lib/quote_sql/error.rb +47 -0
- data/lib/quote_sql/formater.rb +7 -5
- data/lib/quote_sql/quoter.rb +173 -124
- data/lib/quote_sql/test.rb +193 -142
- data/lib/quote_sql/version.rb +3 -0
- data/lib/quote_sql.rb +58 -137
- metadata +20 -5
- data/lib/quote_sql/deprecated.rb +0 -162
data/lib/quote_sql/test.rb
CHANGED
@@ -1,35 +1,216 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
199
|
+
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
|
23
200
|
@fail << rv if @fail
|
24
201
|
end
|
25
|
-
|
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
|
209
|
+
def expected(v = nil)
|
29
210
|
@expected ||= v
|
30
211
|
end
|
31
212
|
|
32
|
-
def
|
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
|
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
|
128
|
-
|
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
|
134
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
150
|
-
self
|
62
|
+
rv
|
151
63
|
end
|
152
64
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
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
|
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.
|
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
|
-
|
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+)
|
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 "
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|