quote-sql 0.0.7 → 0.0.8
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 +86 -106
- data/lib/quote_sql/test.rb +137 -40
- data/lib/quote_sql/version.rb +1 -1
- data/lib/quote_sql.rb +42 -19
- 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: b1ae8afe082e0867130f3933c76e867ac3f16f53b816db61cd9a9cabdfeb3298
|
4
|
+
data.tar.gz: d5c1638521b04e8e62ff2dab8a27a27165f27f972ea4bc6e5f6e55e4680a8cb6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 886abf29ad27600528bd42dd88e4c13da41a1c7076b7bc9a69592d07f8d1653962eb9daaab68eb897238f5b4069658496919a254d01a53b5656cbff9c02bc958
|
7
|
+
data.tar.gz: '085c3fd76781bf95c658151398970fdc6de805767d495d4c23cc9e8cbd444cd61335b73162b99cc16d71eb1f8d268d8c7824244c5e2d6eafe47fe1bd20aaed0e'
|
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
|
@@ -90,13 +90,13 @@ class QuoteSql
|
|
90
90
|
when /(?:^|(.*)_)(ident)$/i
|
91
91
|
_ident
|
92
92
|
when /(?:^|(.*)_)constraints?$/i
|
93
|
-
quotable
|
93
|
+
quotable.to_s
|
94
94
|
when /(?:^|(.*)_)(raw|sql)$/i
|
95
|
-
quotable
|
96
|
-
when
|
97
|
-
|
95
|
+
quotable.to_s
|
96
|
+
when /(?:^|(.*)_)json$/i
|
97
|
+
json_recordset
|
98
98
|
when /^(.+)_values$/i
|
99
|
-
|
99
|
+
values
|
100
100
|
when /values$/i
|
101
101
|
insert_values
|
102
102
|
else
|
@@ -106,31 +106,7 @@ class QuoteSql
|
|
106
106
|
|
107
107
|
###############
|
108
108
|
|
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
|
-
|
109
|
+
private def _value(values)
|
134
110
|
rv ||= values.map do |i|
|
135
111
|
case i
|
136
112
|
when :default, :current_timestamp
|
@@ -143,88 +119,92 @@ class QuoteSql
|
|
143
119
|
Raw.sql "(#{rv.join(",")})"
|
144
120
|
end
|
145
121
|
|
146
|
-
def data_json(item = @quotable)
|
122
|
+
# def data_json(item = @quotable)
|
123
|
+
# casts = self.casts(name)
|
124
|
+
# columns = self.columns(name) || casts&.keys
|
125
|
+
# column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
|
126
|
+
# if item.is_a? Integer
|
127
|
+
# rv = "$#{item}"
|
128
|
+
# else
|
129
|
+
# item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
|
130
|
+
# rv = "'#{item.to_json.gsub(/'/, "''")}'"
|
131
|
+
# end
|
132
|
+
# Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
|
133
|
+
# end
|
134
|
+
|
135
|
+
def json_recordset(rows = @quotable)
|
136
|
+
case rows
|
137
|
+
when Array, Integer
|
138
|
+
when Hash
|
139
|
+
rows = [rows]
|
140
|
+
else
|
141
|
+
raise ArgumentError, "just Array<Hash> or Hash (for a single value)"
|
142
|
+
end
|
147
143
|
casts = self.casts(name)
|
148
|
-
columns = self.columns(name) || casts&.keys
|
149
|
-
|
150
|
-
|
151
|
-
rv = "$#{item}"
|
144
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
145
|
+
if rows.is_a? Integer
|
146
|
+
rv = "$#{rows}"
|
152
147
|
else
|
153
|
-
|
154
|
-
|
148
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
149
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
150
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
151
|
+
rv = "'#{rows.map{ _1.slice(*columns)}.to_json.gsub(/'/, "''")}'"
|
155
152
|
end
|
156
|
-
|
153
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
154
|
+
column_cast = columns.map do |column|
|
155
|
+
"#{QuoteSql.quote_column_name column} #{casts&.dig(column, :sql_type) || "TEXT"}"
|
156
|
+
end
|
157
|
+
Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name(name || "json")}(#{column_cast.join(',')})"
|
157
158
|
end
|
158
159
|
|
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
|
165
|
-
end
|
166
|
-
if item.all? { _1.is_a?(Hash) }
|
167
|
-
column_names ||= item.flat_map { _1.keys.sort }.uniq
|
168
|
-
item.map! { _1.fetch_values(*column_names) {} }
|
160
|
+
def values(rows = @quotable)
|
161
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
162
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
169
163
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names.length != length
|
175
|
-
values = item.map { value(_1) }
|
164
|
+
case rows
|
165
|
+
when Array
|
166
|
+
when Hash
|
167
|
+
rows = [rows]
|
176
168
|
else
|
177
|
-
raise ArgumentError, "
|
169
|
+
raise ArgumentError, "just raw or Array<Hash, Integer> or Hash (for a single value)"
|
178
170
|
end
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
171
|
+
casts = self.casts(name)
|
172
|
+
columns = (self.columns(name) || casts&.keys)&.map(&:to_sym)
|
173
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
174
|
+
columns ||= rows.flat_map { _1.keys.sort }.uniq.map(&:to_sym)
|
175
|
+
values = rows.each_with_index.map do |row, i|
|
176
|
+
row.transform_keys(&:to_sym)
|
177
|
+
if i == 0 and casts.present?
|
178
|
+
columns.map{ "#{_quote(row[_1])}::#{casts&.dig(_1, :sql_type) || "TEXT"}" }
|
179
|
+
else
|
180
|
+
columns.map{ _quote(row[_1]) }
|
181
|
+
end.then { "(#{_1.join(",")})"}
|
183
182
|
end
|
184
|
-
# values[0] { _1 << types[_1] || ""}
|
185
|
-
Raw.sql "(VALUES #{values.join(",")}) AS #{_ident name} (#{_ident column_names})"
|
186
|
-
end
|
187
183
|
|
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
|
184
|
+
Raw.sql "(VALUES #{values.join(",")}) AS #{QuoteSql.quote_column_name(name || "values")} (#{columns.map{QuoteSql.quote_column_name(_1)}.join(",")})"
|
185
|
+
end
|
206
186
|
|
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
187
|
|
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
|
188
|
+
def insert_values(rows = @quotable)
|
189
|
+
if rows.class.to_s[/^(Arel::Nodes::SqlLiteral|QuoteSql::Raw)$/]
|
190
|
+
return Raw.sql((item[/^\s*\(/] and item[/\)\s*$/]) ? rows : "(#{rows})")
|
191
|
+
end
|
192
|
+
case rows
|
193
|
+
when Array
|
225
194
|
when Hash
|
226
|
-
|
195
|
+
rows = [rows]
|
196
|
+
else
|
197
|
+
raise ArgumentError, "just raw or Array<Hash> or Hash (for a single value)"
|
227
198
|
end
|
199
|
+
|
200
|
+
rows = rows.compact.map { _1.transform_keys(&:to_sym) }
|
201
|
+
raise ArgumentError, "all values need to be type Hash" if rows.any? { not _1.is_a?(Hash) }
|
202
|
+
casts = self.casts(name)
|
203
|
+
columns = (self.columns(name) || casts&.keys || rows.flat_map { _1.keys.sort }.uniq).map(&:to_sym)
|
204
|
+
raise ArgumentError, "table or columns has to be present" if columns.blank?
|
205
|
+
columns -= (casts&.select { _2[:virtual] }&.keys || [])
|
206
|
+
values = rows.map { _value(_1.fetch_values(*columns) { :default }) }
|
207
|
+
Raw.sql("(#{columns.map { QuoteSql.quote_column_name _1 }.join(",")}) VALUES #{values.join(",")}")
|
228
208
|
end
|
229
209
|
|
230
210
|
def json?(cast = self.cast)
|
@@ -256,8 +236,8 @@ class QuoteSql
|
|
256
236
|
item.compact! if item.delete(nil) == false
|
257
237
|
case self.cast
|
258
238
|
when /hstore/i
|
259
|
-
_quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}"}.join(","))
|
260
|
-
when NilClass,""
|
239
|
+
_quote(item.map { "#{_1}=>#{_2.nil? ? 'NULL' : _2}" }.join(","))
|
240
|
+
when NilClass, ""
|
261
241
|
"#{_quote(item.to_json)}::JSONB"
|
262
242
|
when /jsonb?/i
|
263
243
|
_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,20 +50,49 @@ 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
|
@@ -74,7 +102,8 @@ uuid xml hstore
|
|
74
102
|
_, name, type = quote.to_s.match(re)&.to_a
|
75
103
|
value = quotes.delete quote
|
76
104
|
value = Raw.sql(value) if value.class.to_s == "Arel::Nodes::SqlLiteral"
|
77
|
-
|
105
|
+
send(:"#{type}=", [name, value])
|
106
|
+
# instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
|
78
107
|
end
|
79
108
|
@quotes.update quotes.transform_keys(&:to_sym)
|
80
109
|
self
|
@@ -92,7 +121,7 @@ uuid xml hstore
|
|
92
121
|
if binds.present? and sql.scan(/(?<=\$)\d+/).map(&:to_i).max != binds.length
|
93
122
|
raise ArgumentError, "Wrong number of binds"
|
94
123
|
end
|
95
|
-
_exec(sql, binds, prepare: false
|
124
|
+
_exec(sql, binds, prepare: false)
|
96
125
|
rescue => exc
|
97
126
|
STDERR.puts exc.inspect, self.inspect
|
98
127
|
raise exc
|
@@ -132,7 +161,7 @@ uuid xml hstore
|
|
132
161
|
if @binds.length != record.length
|
133
162
|
next RuntimeError.new("binds are not equal arguments, #{record.inspect}")
|
134
163
|
end
|
135
|
-
_exec(sql, record, prepare: false
|
164
|
+
_exec(sql, record, prepare: false)
|
136
165
|
end
|
137
166
|
end
|
138
167
|
|
@@ -183,7 +212,7 @@ uuid xml hstore
|
|
183
212
|
#
|
184
213
|
# matched = "#{pre}$#{bind_num}#{"::#{cast}" if cast.present?}#{post}"
|
185
214
|
# els
|
186
|
-
|
215
|
+
if has_quote
|
187
216
|
quoted = quoter(key, cast)
|
188
217
|
unresolved.delete key
|
189
218
|
if (i = quoted.scan MIXIN_RE).present?
|
@@ -245,9 +274,3 @@ end
|
|
245
274
|
|
246
275
|
QuoteSql.include QuoteSql::Formater
|
247
276
|
|
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.8
|
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-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: niceql
|