quote-sql 0.0.7 → 0.0.9
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 +4 -2
- data/lib/quote_sql/connector/active_record_base.rb +3 -4
- data/lib/quote_sql/quoter.rb +93 -111
- data/lib/quote_sql/test.rb +137 -40
- data/lib/quote_sql/version.rb +1 -1
- data/lib/quote_sql.rb +45 -20
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37390ee5356d22cf9fe8b2ba9d737e35c5ac6d39a69e8db62bb4b554c15328dc
|
4
|
+
data.tar.gz: cefa08d46fe1b51de03327f0840daa0059fc9da503225b4379c2cd8213fe455e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51fb6f48e65548dd29402fac1b4d4de5d0ebdf3dcec0e591ea10d4144be82e10e24100f535ae62627b88863ab5517b770f53fe13a24febdb2676669dbc37f674
|
7
|
+
data.tar.gz: cd07e668bcbe753cfd180560191d66eedfe1de87f37f33f461f4f05f981c2818e0bae83892182b17b012290fbffe13ca55fb2bac9544387d314b639d91b65a89
|
data/README.md
CHANGED
@@ -54,7 +54,9 @@ Values are be ordered in sequence of columns. Missing value entries are substitu
|
|
54
54
|
{lastname: "Schultz", firstname: "herbert"}
|
55
55
|
], constraint: :id).to_sql`
|
56
56
|
=> INSERT INTO "users" ("id", "firstname", "lastname", "created_at")
|
57
|
-
VALUES
|
57
|
+
VALUES
|
58
|
+
(1, 'Albert', 'Müller', DEFAULT),
|
59
|
+
(DEFAULT, 'herbert', 'Schultz', DEFAULT)
|
58
60
|
ON CONFLICT ("id") DO NOTHING
|
59
61
|
|
60
62
|
### Columns from a list
|
@@ -81,7 +83,7 @@ You can use binds ($1, $2, ...) in the SQL and add arguments to the result call
|
|
81
83
|
Insert fom json
|
82
84
|
|
83
85
|
v = {a: 1, b: "foo", c: true}
|
84
|
-
QuoteSql.new("INSERT INTO table (%
|
86
|
+
QuoteSql.new("INSERT INTO table (%columns) SELECT * FROM %json").quote({:json=>1}).result(v.to_json)
|
85
87
|
|
86
88
|
|
87
89
|
|
@@ -19,12 +19,11 @@ class QuoteSql
|
|
19
19
|
self.class.conn
|
20
20
|
end
|
21
21
|
|
22
|
-
def _exec_query(sql, binds = [],
|
23
|
-
conn.exec_query(sql, "SQL", binds,
|
22
|
+
def _exec_query(sql, binds = [], **options)
|
23
|
+
conn.exec_query(sql, "SQL", binds, **options)
|
24
24
|
end
|
25
25
|
|
26
|
-
def _exec(sql, binds = [],
|
27
|
-
options = { prepare:, async: }
|
26
|
+
def _exec(sql, binds = [], **options)
|
28
27
|
result = _exec_query(sql, binds, **options)
|
29
28
|
columns = result.columns.map(&:to_sym)
|
30
29
|
result.cast_values.map do |row|
|
data/lib/quote_sql/quoter.rb
CHANGED
@@ -34,19 +34,19 @@ class QuoteSql
|
|
34
34
|
@qsql.casts(name || self.name)
|
35
35
|
end
|
36
36
|
|
37
|
-
def ident_columns(name =
|
38
|
-
item = columns(name
|
37
|
+
def ident_columns(name = self.name)
|
38
|
+
item = columns(name)
|
39
39
|
unless item
|
40
|
-
unless item = casts(name
|
41
|
-
if (table = self.table(name
|
40
|
+
unless item = casts(name)&.keys&.map(&:to_s)
|
41
|
+
if (table = self.table(name))&.respond_to? :column_names
|
42
42
|
item = table.column_names
|
43
43
|
else
|
44
|
-
raise
|
44
|
+
raise ArgumentError, "No columns, casts or table given for #{name}" unless table&.respond_to? :column_names
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
48
|
if item.is_a?(Array)
|
49
|
-
if item.all? { _1.respond_to?(:name) }
|
49
|
+
if item.all? { not _1.is_a?(Symbol) and not _1.is_a?(String) and _1.respond_to?(:name) }
|
50
50
|
item = item.map(&:name)
|
51
51
|
end
|
52
52
|
end
|
@@ -83,20 +83,22 @@ class QuoteSql
|
|
83
83
|
def to_sql
|
84
84
|
return @quotable.call(self) if @quotable.is_a? Proc
|
85
85
|
case key.to_s
|
86
|
-
when /(?:^|(
|
86
|
+
when /(?:^|(.+)_)table_name$/i
|
87
87
|
ident_table
|
88
|
-
when /(?:^|(
|
88
|
+
when /(?:^|(.+)_)table$/i
|
89
|
+
ident_table
|
90
|
+
when /(?:^|(.+)_)columns$/i
|
89
91
|
ident_columns
|
90
|
-
when /(?:^|(
|
92
|
+
when /(?:^|(.+)_)(ident|column)$/i
|
91
93
|
_ident
|
92
|
-
when /(?:^|(
|
93
|
-
quotable
|
94
|
-
when /(?:^|(
|
95
|
-
quotable
|
96
|
-
when
|
97
|
-
|
94
|
+
when /(?:^|(.+)_)constraints?$/i
|
95
|
+
quotable.to_s
|
96
|
+
when /(?:^|(.+)_)(raw|sql)$/i
|
97
|
+
quotable.to_s
|
98
|
+
when /(?:^|(.+)_)json$/i
|
99
|
+
json_recordset
|
98
100
|
when /^(.+)_values$/i
|
99
|
-
|
101
|
+
values
|
100
102
|
when /values$/i
|
101
103
|
insert_values
|
102
104
|
else
|
@@ -106,31 +108,7 @@ class QuoteSql
|
|
106
108
|
|
107
109
|
###############
|
108
110
|
|
109
|
-
private def
|
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
|
-
|
111
|
+
private def _value(values)
|
134
112
|
rv ||= values.map do |i|
|
135
113
|
case i
|
136
114
|
when :default, :current_timestamp
|
@@ -143,88 +121,92 @@ class QuoteSql
|
|
143
121
|
Raw.sql "(#{rv.join(",")})"
|
144
122
|
end
|
145
123
|
|
146
|
-
def data_json(item = @quotable)
|
124
|
+
# def data_json(item = @quotable)
|
125
|
+
# casts = self.casts(name)
|
126
|
+
# columns = self.columns(name) || casts&.keys
|
127
|
+
# column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
|
128
|
+
# if item.is_a? Integer
|
129
|
+
# rv = "$#{item}"
|
130
|
+
# else
|
131
|
+
# item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
|
132
|
+
# rv = "'#{item.to_json.gsub(/'/, "''")}'"
|
133
|
+
# end
|
134
|
+
# Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
|
135
|
+
# end
|
136
|
+
|
137
|
+
def json_recordset(rows = @quotable)
|
138
|
+
case rows
|
139
|
+
when Array, Integer
|
140
|
+
when Hash
|
141
|
+
rows = [rows]
|
142
|
+
else
|
143
|
+
raise ArgumentError, "just Array<Hash> or Hash (for a single value)"
|
144
|
+
end
|
147
145
|
casts = self.casts(name)
|
148
|
-
columns = self.columns(name) || casts&.keys
|
149
|
-
|
150
|
-
|
151
|
-
rv = "$#{item}"
|
146
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
147
|
+
if rows.is_a? Integer
|
148
|
+
rv = "$#{rows}"
|
152
149
|
else
|
153
|
-
|
154
|
-
|
150
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
151
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
152
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
153
|
+
rv = "'#{rows.map{ _1.slice(*columns)}.to_json.gsub(/'/, "''")}'"
|
154
|
+
end
|
155
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
156
|
+
column_cast = columns.map do |column|
|
157
|
+
"#{QuoteSql.quote_column_name column} #{casts&.dig(column, :sql_type) || "TEXT"}"
|
155
158
|
end
|
156
|
-
Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name
|
159
|
+
Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name(name || "json")}(#{column_cast.join(',')})"
|
157
160
|
end
|
158
161
|
|
159
|
-
def
|
160
|
-
|
161
|
-
|
162
|
-
if column_names.is_a? Hash
|
163
|
-
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
164
|
-
column_names = column_names.keys
|
162
|
+
def values(rows = @quotable)
|
163
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
164
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
165
165
|
end
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if item.all? { _1.is_a?(Array) }
|
171
|
-
length, overflow = item.map { _1.length }.uniq
|
172
|
-
raise ArgumentError, "all values need to have the same length" if overflow
|
173
|
-
column_names ||= (1..length).map { "column#{_1}" }
|
174
|
-
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
|
175
|
-
values = item.map { value(_1) }
|
166
|
+
case rows
|
167
|
+
when Array
|
168
|
+
when Hash
|
169
|
+
rows = [rows]
|
176
170
|
else
|
177
|
-
raise ArgumentError, "
|
171
|
+
raise ArgumentError, "just raw or Array<Hash, Integer> or Hash (for a single value)"
|
178
172
|
end
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
173
|
+
casts = self.casts(name)
|
174
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
175
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
176
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
177
|
+
values = rows.each_with_index.map do |row, i|
|
178
|
+
row.transform_keys(&:to_sym)
|
179
|
+
if i == 0 and casts.present?
|
180
|
+
columns.map{ "#{_quote(row[_1])}::#{casts&.dig(_1, :sql_type) || "TEXT"}" }
|
181
|
+
else
|
182
|
+
columns.map{ _quote(row[_1]) }
|
183
|
+
end.then { "(#{_1.join(",")})"}
|
183
184
|
end
|
184
|
-
# values[0] { _1 << types[_1] || ""}
|
185
|
-
Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
|
186
|
-
end
|
187
185
|
|
188
|
-
|
189
|
-
|
190
|
-
when Arel::Nodes::SqlLiteral
|
191
|
-
item = Raw.sql("(#{item})") unless item[/^\s*\(/] and item[/\)\s*$/]
|
192
|
-
return item
|
193
|
-
when Array
|
194
|
-
item.compact!
|
195
|
-
column_names = (@qsql.quotes[:columns] || @qsql.quotes[:column_names]).dup
|
196
|
-
types = []
|
197
|
-
if column_names.is_a? Hash
|
198
|
-
types = column_names.values.map { "::#{_1.upcase}" if _1 }
|
199
|
-
column_names = column_names.keys
|
200
|
-
elsif column_names.is_a? Array
|
201
|
-
column_names = column_names.map do |column|
|
202
|
-
types << column.respond_to?(:sql_type) ? "::#{column.sql_type}" : nil
|
203
|
-
column.respond_to?(:name) ? column.name : column
|
204
|
-
end
|
205
|
-
end
|
186
|
+
Raw.sql "(VALUES #{values.join(",")}) AS #{QuoteSql.quote_column_name(name || "values")} (#{columns.map{QuoteSql.quote_column_name(_1)}.join(",")})"
|
187
|
+
end
|
206
188
|
|
207
|
-
if item.all? { _1.is_a?(Hash) }
|
208
|
-
column_names ||= item.flat_map { _1.keys.sort }.uniq
|
209
|
-
item.map! { _1.fetch_values(*column_names) {} }
|
210
|
-
end
|
211
189
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
raise ArgumentError, "Either all type Hash or Array"
|
219
|
-
end
|
220
|
-
if column_names.present?
|
221
|
-
Raw.sql "(#{_ident column_names}) VALUES #{values.join(",")}"
|
222
|
-
else
|
223
|
-
Raw.sql "VALUES #{values.join(",")}"
|
224
|
-
end
|
190
|
+
def insert_values(rows = @quotable)
|
191
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
192
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
193
|
+
end
|
194
|
+
case rows
|
195
|
+
when Array
|
225
196
|
when Hash
|
226
|
-
|
197
|
+
rows = [rows]
|
198
|
+
else
|
199
|
+
raise ArgumentError, "just raw or Array<Hash> or Hash (for a single value)"
|
227
200
|
end
|
201
|
+
|
202
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
203
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
204
|
+
casts = self.casts(name)
|
205
|
+
columns = (self.columns(name) || casts&.keys || rows.flat_map { _1.keys.sort }.uniq).map(&:to_sym)
|
206
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
207
|
+
columns -= (casts&.select { _2[:virtual] }&.keys || [])
|
208
|
+
values = rows.map { _value(_1.fetch_values(*columns) { :default }) }
|
209
|
+
Raw.sql("(#{columns.map { QuoteSql.quote_column_name _1 }.join(",")}) VALUES #{values.join(",")}")
|
228
210
|
end
|
229
211
|
|
230
212
|
def json?(cast = self.cast)
|
@@ -256,8 +238,8 @@ class QuoteSql
|
|
256
238
|
item.compact! if item.delete(nil) == false
|
257
239
|
case self.cast
|
258
240
|
when /hstore/i
|
259
|
-
_quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}"}.join(","))
|
260
|
-
when NilClass,""
|
241
|
+
_quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}" }.join(","))
|
242
|
+
when NilClass, ""
|
261
243
|
"#{_quote(item.to_json)}::JSONB"
|
262
244
|
when /jsonb?/i
|
263
245
|
_quote(item.to_json)
|
data/lib/quote_sql/test.rb
CHANGED
@@ -50,22 +50,46 @@ class QuoteSql::Test
|
|
50
50
|
)
|
51
51
|
end
|
52
52
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
def test_values_hash_active_record
|
54
|
+
table = create_active_record_class("tasks") do |t|
|
55
|
+
t.text :name
|
56
|
+
t.integer :n1, default: 1, null: false
|
57
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
58
|
+
t.timestamps
|
59
|
+
end
|
60
|
+
updated_at = Date.new(2024,1,1)
|
61
|
+
expected <<~SQL
|
62
|
+
INSERT INTO "tasks" ("id", "name", "n1", "created_at", "updated_at") VALUES (DEFAULT, 'Task1', 1, DEFAULT, DEFAULT), (DEFAULT, 'Task2', DEFAULT, DEFAULT, '2024-01-01')
|
63
|
+
SQL
|
64
|
+
values = [
|
65
|
+
{n1: 1, name: "Task1"},
|
66
|
+
{name: "Task2", updated_at: }
|
67
|
+
]
|
68
|
+
QuoteSql.new(<<~SQL).quote(table:, values:)
|
69
|
+
INSERT INTO %table %values
|
70
|
+
SQL
|
71
|
+
end
|
61
72
|
|
62
|
-
def
|
73
|
+
def test_values_hash_active_record_select_columns
|
74
|
+
table = create_active_record_class("tasks") do |t|
|
75
|
+
t.text :name
|
76
|
+
t.integer :n1, default: 1, null: false
|
77
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
78
|
+
t.timestamps
|
79
|
+
end
|
63
80
|
expected <<~SQL
|
64
|
-
|
81
|
+
INSERT INTO "tasks" ("name") VALUES ('Task1'), ('Task2')
|
82
|
+
SQL
|
83
|
+
values = [
|
84
|
+
{n1: 1, name: "Task1"},
|
85
|
+
{name: "Task2", id: "12345" }
|
86
|
+
]
|
87
|
+
QuoteSql.new(<<~SQL).quote(table:, values:, columns: %i[name])
|
88
|
+
INSERT INTO %table %values
|
65
89
|
SQL
|
66
|
-
"SELECT * FROM %x_values".quote_sql(x_values: [['a', 1, true, nil]])
|
67
90
|
end
|
68
91
|
|
92
|
+
|
69
93
|
def test_from_values_hash_no_columns
|
70
94
|
expected <<~SQL
|
71
95
|
SELECT * FROM (VALUES ('a', 1, true, NULL), ('a', 1, true, NULL), (NULL, 1, NULL, 2)) AS "y" ("a", "b", "c", "d")
|
@@ -94,7 +118,7 @@ class QuoteSql::Test
|
|
94
118
|
) AS "x" ("a", "b", "c", "d")
|
95
119
|
SQL
|
96
120
|
"SELECT * FROM %x_values".quote_sql(
|
97
|
-
|
121
|
+
x_casts: {
|
98
122
|
a: "text",
|
99
123
|
b: "integer",
|
100
124
|
c: "boolean",
|
@@ -107,12 +131,6 @@ class QuoteSql::Test
|
|
107
131
|
])
|
108
132
|
end
|
109
133
|
|
110
|
-
def test_insert_values_array
|
111
|
-
expected <<~SQL
|
112
|
-
INSERT INTO x VALUES ('a', 1, true, NULL)
|
113
|
-
SQL
|
114
|
-
"INSERT INTO x %values".quote_sql(values: [['a', 1, true, nil]])
|
115
|
-
end
|
116
134
|
|
117
135
|
def test_insert_values_hash
|
118
136
|
expected <<~SQL
|
@@ -130,10 +148,10 @@ class QuoteSql::Test
|
|
130
148
|
|
131
149
|
def test_json_insert
|
132
150
|
expected <<~SQL
|
133
|
-
INSERT INTO users (name, color) SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "
|
151
|
+
INSERT INTO users ("name", "color") SELECT * from json_to_recordset('[{"name":"auge","color":"#611333"}]') AS "json"("name" text,"color" text)
|
134
152
|
SQL
|
135
|
-
|
136
|
-
"INSERT INTO users (
|
153
|
+
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" }
|
154
|
+
"INSERT INTO users (%columns) SELECT * from %json".quote_sql(columns: %i[name color], json:)
|
137
155
|
end
|
138
156
|
|
139
157
|
def test_from_json_bind
|
@@ -164,9 +182,9 @@ class QuoteSql::Test
|
|
164
182
|
ARRAY[[1,2,3],[1,2,3]]::INT[][]
|
165
183
|
SQL
|
166
184
|
array1 = array2 = array3 = ["cde", nil, "fgh"]
|
167
|
-
array4 = [[1,2,3], [1,2,3]]
|
185
|
+
array4 = [[1, 2, 3], [1, 2, 3]]
|
168
186
|
hash = { foo: "bar", "go": 1, strip_null: nil }
|
169
|
-
QuoteSQL(<<~SQL, field1: 'abc', array1:, array2:, array3:, array4:, hash
|
187
|
+
QuoteSQL(<<~SQL, field1: 'abc', array1:, array2:, array3:, array4:, hash:, not_compact: hash, compact: hash.merge(nil => false))
|
170
188
|
SELECT
|
171
189
|
%field1::TEXT,
|
172
190
|
%field1::JSON,
|
@@ -180,6 +198,38 @@ class QuoteSql::Test
|
|
180
198
|
SQL
|
181
199
|
end
|
182
200
|
|
201
|
+
def test_columns_with_tables
|
202
|
+
expected <<~SQL
|
203
|
+
SELECT "profiles"."a", "profiles"."b",
|
204
|
+
"relationships"."a", "relationships"."b",
|
205
|
+
relationship_timestamp("relationships".*)
|
206
|
+
SQL
|
207
|
+
|
208
|
+
profile_table = "profiles"
|
209
|
+
relationship_table = "relationships"
|
210
|
+
relationship_columns = profile_columns = %i[a b]
|
211
|
+
|
212
|
+
<<~SQL.quote_sql(profile_columns:, profile_table:, relationship_columns:, relationship_table:)
|
213
|
+
SELECT %profile_columns, %relationship_columns,
|
214
|
+
relationship_timestamp(%relationship_table.*)
|
215
|
+
SQL
|
216
|
+
end
|
217
|
+
|
218
|
+
def test_active_record
|
219
|
+
table = create_active_record_class("users") do |t|
|
220
|
+
t.text :first_name
|
221
|
+
t.integer :n1, default: 1, null: false
|
222
|
+
t.virtual :v1, type: :boolean, stored: true, as: "FALSE"
|
223
|
+
t.timestamps default: "CURRENT_TIMESTAMP", null: false
|
224
|
+
end
|
225
|
+
expected <<~SQL
|
226
|
+
SELECT "id", "first_name", "n1", "v1", "created_at", "updated_at" FROM "users"
|
227
|
+
SQL
|
228
|
+
<<~SQL.quote_sql(table:)
|
229
|
+
SELECT %columns FROM %table
|
230
|
+
SQL
|
231
|
+
end
|
232
|
+
|
183
233
|
# def test_q3
|
184
234
|
# expected Arel.sql(<<-SQL)
|
185
235
|
# INSERT INTO "responses" ("id","type","task_id","index","data","parts","value","created_at","updated_at")
|
@@ -217,18 +267,20 @@ class QuoteSql::Test
|
|
217
267
|
def run(name, all = false)
|
218
268
|
name = name.to_s.sub(/^test_/, "")
|
219
269
|
rv = ["🧪 #{name}"]
|
270
|
+
puts(*rv)
|
220
271
|
@expected = nil
|
221
272
|
@test = send("test_#{name}")
|
222
273
|
if sql.gsub(/\s+/, "")&.downcase&.strip == expected&.gsub(/\s+/, "")&.downcase&.strip
|
223
|
-
tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
|
224
|
-
columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
|
225
|
-
|
226
|
-
|
274
|
+
# tables = @test.tables.to_h { [[_1, "table"].compact.join("_"), _2] }
|
275
|
+
# columns = @test.instance_variable_get(:@columns).to_h { [[_1, "columns"].compact.join("_"), _2] }
|
276
|
+
#"QuoteSql.new(\"#{@test.original}\").quote(#{{ **tables, **columns, **@test.quotes }.inspect}).to_sql",
|
277
|
+
rv += ["🎯 #{expected}", "✅ #{sql}"]
|
278
|
+
|
227
279
|
@success << rv if @success
|
228
280
|
else
|
229
281
|
rv += [@test.inspect, "🎯 #{expected}", "❌ #{sql}"]
|
230
|
-
rv <<
|
231
|
-
rv <<
|
282
|
+
rv << "🎯 " + expected&.gsub(/\s+/, "")&.downcase&.strip
|
283
|
+
rv << "❌ " + sql.gsub(/\s+/, "")&.downcase&.strip
|
232
284
|
@fail << rv if @fail
|
233
285
|
end
|
234
286
|
rescue => exc
|
@@ -246,20 +298,65 @@ class QuoteSql::Test
|
|
246
298
|
@test.to_sql
|
247
299
|
end
|
248
300
|
|
249
|
-
class
|
250
|
-
|
251
|
-
|
301
|
+
class PseudoActiveRecordKlass
|
302
|
+
class Column
|
303
|
+
def initialize(name, type, **options)
|
304
|
+
@name = name.to_s
|
305
|
+
@type = type
|
306
|
+
@null = options[:null]
|
307
|
+
type = options[:type] if @type == :virtual
|
308
|
+
@sql_type = DATATYPES[/^#{type}$/]
|
309
|
+
unless @type == :virtual or options[:default].nil?
|
310
|
+
@default = options[:default]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
attr_reader :name, :type, :sql_type, :null, :default, :default_function
|
315
|
+
|
316
|
+
def default?
|
317
|
+
! (@default || @default_function).nil?
|
318
|
+
end
|
252
319
|
end
|
320
|
+
class Columns
|
321
|
+
def initialize(&block)
|
322
|
+
@rv = []
|
323
|
+
block.call(self)
|
324
|
+
end
|
325
|
+
|
326
|
+
def to_a
|
327
|
+
@rv
|
328
|
+
end
|
329
|
+
|
330
|
+
def timestamps(**options)
|
331
|
+
@rv << Column.new( :created_at, :timestamp, null: false, **options)
|
332
|
+
@rv << Column.new( :updated_at, :timestamp, null: false, **options)
|
333
|
+
end
|
253
334
|
|
254
|
-
|
255
|
-
|
335
|
+
def method_missing(type, name, *args, **options)
|
336
|
+
@rv << Column.new(name, type, *args, **options)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def initialize(table_name, id: :uuid, &block)
|
341
|
+
@table_name = table_name
|
342
|
+
@columns = Columns.new(&block).to_a
|
343
|
+
unless id.nil? or id == false
|
344
|
+
@columns.unshift(Column.new("id", *[id], null: false, default: "gen_random_uuid()"))
|
345
|
+
end
|
256
346
|
end
|
257
347
|
|
258
|
-
|
259
|
-
|
348
|
+
attr_reader :table_name, :columns
|
349
|
+
|
350
|
+
def column_names
|
351
|
+
@columns.map { _1.name }
|
260
352
|
end
|
261
353
|
end
|
262
354
|
|
355
|
+
def create_active_record_class(table_name, **options, &block)
|
356
|
+
PseudoActiveRecordKlass.new(table_name, **options, &block)
|
357
|
+
end
|
358
|
+
|
359
|
+
|
263
360
|
def datatype
|
264
361
|
errors = {}
|
265
362
|
success = []
|
@@ -271,11 +368,11 @@ class QuoteSql::Test
|
|
271
368
|
|
272
369
|
m = "jgj hsgjhsgfjh ag %field::#{l} asldfalskjdfl".match(QuoteSql::CASTS)
|
273
370
|
if m.present? and l == m[1]
|
274
|
-
|
275
|
-
|
276
|
-
|
371
|
+
success << line
|
372
|
+
else
|
373
|
+
errors[line] = m&.to_a
|
277
374
|
end
|
278
|
-
line = line + "[]"*(rand(3) + 1)
|
375
|
+
line = line + "[]" * (rand(3) + 1)
|
279
376
|
m = "jgj hsgjhsgfjh ag %field::#{line} asldfalskjdfl".match(QuoteSql::CASTS)
|
280
377
|
if m.present? and line == m[1] + m[2]
|
281
378
|
success << line
|
data/lib/quote_sql/version.rb
CHANGED
data/lib/quote_sql.rb
CHANGED
@@ -3,7 +3,6 @@ Dir.glob(__FILE__.sub(/\.rb$/, "/*.rb")).each { require(_1) unless _1[/(deprecat
|
|
3
3
|
# Tool to build and run SQL queries easier
|
4
4
|
class QuoteSql
|
5
5
|
|
6
|
-
|
7
6
|
DATA_TYPES_RE = %w(
|
8
7
|
(?>character\\s+varying|bit\\s+varying|character|varbit|varchar|char|bit|interval)(?>\\s*\\(\\s*\\d+\\s*\\))?
|
9
8
|
(?>numeric|decimal)(?>\\s*\\(\\s*\\d+\\s*,\\s*\\d+\\s*\\))?
|
@@ -51,32 +50,64 @@ uuid xml hstore
|
|
51
50
|
attr_reader :sql, :quotes, :original, :binds, :tables, :columns
|
52
51
|
|
53
52
|
def table(name = nil)
|
54
|
-
@tables[name&.to_sym]
|
53
|
+
table = @tables[name&.to_sym]
|
54
|
+
table.is_a?(Class) ? table : table.dup
|
55
|
+
end
|
56
|
+
|
57
|
+
def table=(value)
|
58
|
+
name, table = value
|
59
|
+
name = name&.to_sym
|
60
|
+
@tables[name] = table
|
61
|
+
if table.respond_to?(:columns)
|
62
|
+
@casts[name] = table.columns.to_h do |c|
|
63
|
+
[c.name.to_sym, { sql_type: c.sql_type, default: (c.default || c.default_function rescue nil).present?, virtual: c.type == :virtual }]
|
64
|
+
end if @casts[name].blank?
|
65
|
+
elsif table.respond_to?(:column_names)
|
66
|
+
@casts[name] = table.column_names.to_h { [_1.to_sym, nil] } if @casts[name].blank?
|
67
|
+
end
|
55
68
|
end
|
56
69
|
|
70
|
+
alias tables= table=
|
71
|
+
|
72
|
+
def column=(value)
|
73
|
+
name, column = value
|
74
|
+
name = name&.to_sym
|
75
|
+
@columns[name] = column
|
76
|
+
end
|
77
|
+
|
78
|
+
alias columns= column=
|
79
|
+
|
80
|
+
def cast=(value)
|
81
|
+
name, cast = value
|
82
|
+
name = name&.to_sym
|
83
|
+
raise ArgumentError unless cast.is_a?(Hash)
|
84
|
+
(@casts[name] ||= {}).update(cast.transform_values { _1.is_a?(Hash) ? _1 : { sql_type: _1 } })
|
85
|
+
end
|
86
|
+
|
87
|
+
alias casts= cast=
|
88
|
+
|
57
89
|
def columns(name = nil)
|
58
|
-
|
90
|
+
name = name&.to_sym
|
91
|
+
@columns[name] || @casts[name]&.keys&.map(&:to_s)
|
59
92
|
end
|
60
93
|
|
61
94
|
def casts(name = nil)
|
62
|
-
|
63
|
-
table = table(name) or return
|
64
|
-
return unless table.respond_to? :columns
|
65
|
-
rv = table.columns.to_h { [_1.name.to_sym, _1.sql_type] }
|
66
|
-
end
|
67
|
-
rv
|
95
|
+
@casts[name&.to_sym]
|
68
96
|
end
|
69
97
|
|
70
98
|
# Add quotes keys are symbolized
|
71
99
|
def quote(quotes = {})
|
100
|
+
quotes = quotes.transform_keys(&:to_sym)
|
72
101
|
re = /(?:^|(.*)_)(table|columns|casts)$/i
|
102
|
+
(table_name = quotes[:table_name].presence) and (@tables[:table] ||= table_name)
|
73
103
|
quotes.keys.grep(re).each do |quote|
|
74
104
|
_, name, type = quote.to_s.match(re)&.to_a
|
75
105
|
value = quotes.delete quote
|
76
106
|
value = Raw.sql(value) if value.class.to_s == "Arel::Nodes::SqlLiteral"
|
77
|
-
|
107
|
+
send(:"#{type}=", [name, value])
|
108
|
+
# instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
|
78
109
|
end
|
79
|
-
@quotes.update quotes
|
110
|
+
@quotes.update quotes
|
80
111
|
self
|
81
112
|
end
|
82
113
|
|
@@ -92,7 +123,7 @@ uuid xml hstore
|
|
92
123
|
if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max != binds.length
|
93
124
|
raise ArgumentError, "Wrong number of binds"
|
94
125
|
end
|
95
|
-
_exec(sql, binds, prepare: false
|
126
|
+
_exec(sql, binds, prepare: false)
|
96
127
|
rescue => exc
|
97
128
|
STDERR.puts exc.inspect, self.inspect
|
98
129
|
raise exc
|
@@ -132,7 +163,7 @@ uuid xml hstore
|
|
132
163
|
if @binds.length != record.length
|
133
164
|
next RuntimeError.new("binds are not equal arguments, #{record.inspect}")
|
134
165
|
end
|
135
|
-
_exec(sql, record, prepare: false
|
166
|
+
_exec(sql, record, prepare: false)
|
136
167
|
end
|
137
168
|
end
|
138
169
|
|
@@ -183,7 +214,7 @@ uuid xml hstore
|
|
183
214
|
#
|
184
215
|
# matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
|
185
216
|
# els
|
186
|
-
|
217
|
+
if has_quote
|
187
218
|
quoted = quoter(key, cast)
|
188
219
|
unresolved.delete key
|
189
220
|
if (i = quoted.scan MIXIN_RE).present?
|
@@ -245,9 +276,3 @@ end
|
|
245
276
|
|
246
277
|
QuoteSql.include QuoteSql::Formater
|
247
278
|
|
248
|
-
class Array
|
249
|
-
def depth
|
250
|
-
select { _1.is_a?(Array) }.map { _1.depth.to_i + 1 }.max || 1
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
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.
|
4
|
+
version: 0.0.9
|
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-
|
11
|
+
date: 2024-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: niceql
|