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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfe070af9a81f751ed10ef8ace30bf5e44a3efb54cff04f2eb5cc137b7fbc0f1
4
- data.tar.gz: 8c8ec59ca80a8c72ceee6567da4beb5f79c96a63e16d945694fed26ea695feca
3
+ metadata.gz: 37390ee5356d22cf9fe8b2ba9d737e35c5ac6d39a69e8db62bb4b554c15328dc
4
+ data.tar.gz: cefa08d46fe1b51de03327f0840daa0059fc9da503225b4379c2cd8213fe455e
5
5
  SHA512:
6
- metadata.gz: 03d69b9b3256cd22762844dc98c31ce2223c673079488d7175078dd82406d8031673368871a2a920ecda17ea68beb79c87f3208d56b1e770c4ba15583fafc648
7
- data.tar.gz: b6d9f335cdcaf0e597f3118090451059220ebd9e540eda483f546301ad148bfebc35150a73a72c6dc54a39f537e8aa9b854970a975218226a26af3ed6add75c0
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 (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
@@ -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 /(?:^|(.*)_)table$/i
86
+ when /(?:^|(.+)_)table_name$/i
87
87
  ident_table
88
- when /(?:^|(.*)_)columns$/i
88
+ when /(?:^|(.+)_)table$/i
89
+ ident_table
90
+ when /(?:^|(.+)_)columns$/i
89
91
  ident_columns
90
- when /(?:^|(.*)_)(ident)$/i
92
+ when /(?:^|(.+)_)(ident|column)$/i
91
93
  _ident
92
- when /(?:^|(.*)_)constraints?$/i
93
- quotable
94
- when /(?:^|(.*)_)(raw|sql)$/i
95
- quotable
96
- when /^(.+)_json$/i
97
- data_json
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
- data_values
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 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
-
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
- column_cast = columns&.map { "#{QuoteSql.quote_column_name(_1)} #{casts&.dig(_1) || "TEXT"}" }
150
- if item.is_a? Integer
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
- item = [item].flatten.compact.as_json.map { _1.slice(*columns.map(&:to_s)) }
154
- rv = "'#{item.to_json.gsub(/'/, "''")}'"
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 name}(#{column_cast.join(',')})"
159
+ Raw.sql "json_to_recordset(#{rv}) AS #{QuoteSql.quote_column_name(name || "json")}(#{column_cast.join(',')})"
157
160
  end
158
161
 
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
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
- 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) {} }
169
- 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) }
166
+ case rows
167
+ when Array
168
+ when Hash
169
+ rows = [rows]
176
170
  else
177
- raise ArgumentError, "Either all type Hash or Array"
171
+ raise ArgumentError, "just raw or Array<Hash, Integer> or Hash (for a single value)"
178
172
  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(",") + ")"
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
- 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
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
- 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
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
- value([item])
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)
@@ -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.9"
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,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].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
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
- instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
107
+ send(:"#{type}=", [name, value])
108
+ # instance_variable_get(:"@#{type.sub(/s*$/,'s')}")[name&.to_sym] = value
78
109
  end
79
- @quotes.update quotes.transform_keys(&:to_sym)
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, async: 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, async: 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
- if has_quote
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.7
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-02-29 00:00:00.000000000 Z
11
+ date: 2024-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: niceql