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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfe070af9a81f751ed10ef8ace30bf5e44a3efb54cff04f2eb5cc137b7fbc0f1
4
- data.tar.gz: 8c8ec59ca80a8c72ceee6567da4beb5f79c96a63e16d945694fed26ea695feca
3
+ metadata.gz: b1ae8afe082e0867130f3933c76e867ac3f16f53b816db61cd9a9cabdfeb3298
4
+ data.tar.gz: d5c1638521b04e8e62ff2dab8a27a27165f27f972ea4bc6e5f6e55e4680a8cb6
5
5
  SHA512:
6
- metadata.gz: 03d69b9b3256cd22762844dc98c31ce2223c673079488d7175078dd82406d8031673368871a2a920ecda17ea68beb79c87f3208d56b1e770c4ba15583fafc648
7
- data.tar.gz: b6d9f335cdcaf0e597f3118090451059220ebd9e540eda483f546301ad148bfebc35150a73a72c6dc54a39f537e8aa9b854970a975218226a26af3ed6add75c0
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 (1, 'Albert', 'Müller', CURRENT_TIMESTAMP), (DEFAULT, 'herbert', 'Schultz', CURRENT_TIMESTAMP)
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 (%x_columns) SELECT * FROM %x_json").quote({:x_json=>1}).result(v.to_json)
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 = [], prepare: false, async: false)
23
- conn.exec_query(sql, "SQL", binds, prepare:, async:)
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 = [], prepare: false, async: false)
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|
@@ -34,19 +34,19 @@ class QuoteSql
34
34
  @qsql.casts(name || self.name)
35
35
  end
36
36
 
37
- def ident_columns(name = nil)
38
- item = columns(name || self.name)
37
+ def ident_columns(name = self.name)
38
+ item = columns(name)
39
39
  unless item
40
- unless item = casts(name || self.name)&.keys
41
- if (table = self.table(name || self.name))&.respond_to? :column_names
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 ArgumntError, "No columns, casts or table given for #{name}" unless table&.respond_to? :column_names
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 /^(.+)_json$/i
97
- data_json
95
+ quotable.to_s
96
+ when /(?:^|(.*)_)json$/i
97
+ json_recordset
98
98
  when /^(.+)_values$/i
99
- data_values
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 value(values)
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
- column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
150
- if item.is_a? Integer
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
- item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
154
- rv = "'#{item.to_json.gsub(/'/, "''")}'"
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
- Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name name}(#{column_cast.join(',')})"
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 data_values(item = @quotable)
160
- item = Array(item).compact
161
- column_names = columns(name)
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
- 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) }
164
+ case rows
165
+ when Array
166
+ when Hash
167
+ rows = [rows]
176
168
  else
177
- raise ArgumentError, "Either all type Hash or Array"
169
+ raise ArgumentError, "just raw or Array<Hash, Integer> or Hash (for a single value)"
178
170
  end
179
- if types.present?
180
- value = values[0][1..-2].split(/\s*,\s*/)
181
- types.each_with_index { value[_2] << _1 || "" }
182
- values[0] = "(" + value.join(",") + ")"
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
- def insert_values(item = @quotable)
189
- case item
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
- if item.all? { _1.is_a?(Array) }
213
- length, overflow = item.map { _1.length }.uniq
214
- raise ArgumentError, "all values need to have the same length" if overflow
215
- raise ArgumentError, "#{name}_columns and value lengths need to be the same" if column_names and column_names.length != length
216
- values = item.map { value(_1) }
217
- else
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
- value([item])
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)
@@ -50,22 +50,46 @@ class QuoteSql::Test
50
50
  )
51
51
  end
52
52
 
53
- # def test_binds
54
- # expected <<~SQL
55
- # SELECT $1, $2::UUID, $1 AS get_bind_1_again FROM "my_table"
56
- # SQL
57
- # QuoteSql.new("SELECT %bind, %bind__uuid, %bind1 AS get_bind_1_again FROM %table").quote(
58
- # table: "my_table"
59
- # )
60
- # end
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 test_from_values_array
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
- SELECT * FROM (VALUES ('a',1,TRUE,NULL)) AS "x" ("column1","column2","column3","column4")
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
- x_columns: {
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 "x"("name" text,"color" text)
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
- 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" }
136
- "INSERT INTO users (name, color) SELECT * from %x_json".quote_sql(x_casts: { name: "text", color: "text" }, x_json:)
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: ,not_compact: hash, compact: hash.merge(nil => false))
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
- rv += [
226
- "QuoteSql.new(\"#{@test.original}\").quote(#{{ **tables, **columns, **@test.quotes }.inspect}).to_sql", "🎯 #{expected}", "✅ #{sql}"]
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 << sql.gsub(/\s+/, "")&.downcase&.strip
231
- rv << expected&.gsub(/\s+/, "")&.downcase&.strip
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 PseudoActiveRecord
250
- def self.table_name
251
- "pseudo_active_records"
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
- def self.column_names
255
- %w(id column1 column2)
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
- def to_qsl
259
- "SELECT * FROM #{self.class.table_name}"
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
- success << line
275
- else
276
- errors[line] = m&.to_a
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
@@ -1,3 +1,3 @@
1
1
  class QuoteSql
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
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].dup
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
- @columns[name&.to_sym].dup
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
- unless rv = @casts[name&.to_sym]
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
- instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
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, async: 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, async: 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
- if has_quote
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.7
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-02-29 00:00:00.000000000 Z
11
+ date: 2024-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: niceql