dynamic-records-meritfront 2.0.21 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0145c6915704e699ab557a6e45348a1555a774e79f4a49925be7f41d88877a6d
4
- data.tar.gz: ebed51c113a9f94b9f4bc481069da98b8771c5b6aa24af2403b9a1283869fed2
3
+ metadata.gz: 6a2496e72da8096b231edb9fab7d9a469926bbcd9a698d0224a78ee7ed5e25a5
4
+ data.tar.gz: 23100e7e24dad43bdfeab2f1291d4616c72e3726eba69a1c3cb2be99cf73df19
5
5
  SHA512:
6
- metadata.gz: cc6978059ea84ee5584a967bbf0c465ebd81fdaee751dd94c745811f2704216b5992d433ed4168f67187fd18e485d586ca220cc468e617b93da3798e5ed8b475
7
- data.tar.gz: 6efa88c7bb6f60789301660d77d874b2252fd69fe0b4de66d223d1df863df9518dce2c89bd60d6f6a30ff167535d7adf9392ea9320979a59780a2b9e130737d7
6
+ metadata.gz: a2861fed694119175940f5153f429491cc25210611da866e55b5a52720610655a89b8291dd7bf1a1042fa155ce8c7dbeacbb846470e01645dda9381d868c6b42
7
+ data.tar.gz: 84110301448a31b997b6acbd89b99dfbe3d5b3c6432e7ba7cafc20b78bfa92a80818a4291da6a30090643fc7eec3da2028f8a30030339e0ac7cdd90f7c12d49a
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamic-records-meritfront (2.0.21)
4
+ dynamic-records-meritfront (3.0.1)
5
5
  hashid-rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,10 +1,33 @@
1
1
  # Dynamic Records Meritfront
2
2
 
3
- Dyanmic Records Meritfront contains some helpers methods for active record. These methods have the goal of allowing one to
3
+ Dyanmic Records Meritfront helps extend active record functionality to make it more dynamic. These methods have the goal of allowing one to
4
4
  1. communicate with the frontend quicker and more effectively through Global HashIds
5
- 2. communicate with the backend more effectively with raw sql queries. This becomes especially relevant when you hit the limits of Active Record Relations and the usual way of querying in rails. For instance, if you have a page-long dynamic sql query.
5
+ 2. communicate with the backend more effectively with sql queries. This becomes especially relevant when you hit the limits of Active Record Relations and the usual way of querying in rails. For instance, if you have dynamic sql queries that are hard to convert properly into ruby.
6
+ 3. add other helper methods to work with your database, such as checking if relations exist, or if a migration has been run.
6
7
 
7
- Note that postgres is a requirement for this gem. I dont tend to get much feedback, so any given would be appreciated.
8
+ Note that postgres is currently a requirement for this gem.
9
+
10
+ ## Basic Examples
11
+ ```ruby
12
+ # returns a json-like hash list of user data
13
+ users = ApplicationRecord.dynamic_sql(
14
+ 'get_5_users',
15
+ 'select * from users limit :our_limit',
16
+ our_limit: 5
17
+ )
18
+
19
+ #returns a list of users (each an instance of User)
20
+ users = User.dynamic_sql(
21
+ 'get_users_from_ids',
22
+ 'select * from users where id = ANY (:ids)',
23
+ ids: [1,2,3]
24
+ )
25
+
26
+ uhgid = users.first.hgid #returns a hashed global id like: 'gid://appname/User/K9YI4K'
27
+ user = User.locate_hgid(uhgid) #returns that user
28
+
29
+ #... and much more!
30
+ ```
8
31
 
9
32
  ## Installation
10
33
 
@@ -38,58 +61,21 @@ end
38
61
 
39
62
  These are methods written for easier sql usage.
40
63
 
41
- #### has_run_migration?(nm)
42
-
43
- put in a string name of the migration's class and it will say if it has allready run the migration.
44
- good during enum migrations as the code to migrate wont run if enumerate is there
45
- as it is not yet enumerated (causing an error when it loads the class that will have the
46
- enumeration in it). This can lead it to being impossible to commit clean code.
47
-
48
- <details><summary>example usage</summary>
49
- only load relationa if it exists in the database
50
-
51
- ```ruby
52
- if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
53
- class UserImageRelation < ApplicationRecord
54
- belongs_to :imageable, polymorphic: true
55
- belongs_to :image
56
- end
57
- else
58
- class UserImageRelation; end
59
- end
60
-
61
- ```
62
- </details>
63
-
64
- #### has_association?(*args)
65
-
66
- accepts a list, checks if the model contains those associations
67
-
68
- <details><summary>example usage</summary>
69
- Check if object is a votable class
70
-
71
- ```ruby
72
- obj = Comment.first
73
- obj.has_association?(:votes) #true
74
- obj = User.first
75
- obj.has_association?(:votes) #false
76
- ```
77
- </details>
78
-
79
64
  #### self.dynamic_sql(name, sql, opts = { })
80
65
  A better and safer way to write sql. Can return either a Hash, ActiveRecord::Response object, or an instantiated model.
66
+
81
67
  with options:
82
- - instantiate_class: returns User, Post, etc objects instead of straight sql output.
83
- I prefer doing the alterantive
84
- ```User.headache_sql(...)```
85
- which is also supported
86
- - prepare: sets whether the db will preprocess the strategy for lookup (defaults true) (have not verified the prepared-ness)
87
- - name_modifiers: allows one to change the preprocess associated name, useful in cases of dynamic sql.
68
+ - options not stated below: considered sql arguments, and will replace their ":option_name" with a sql argument. Always use sql arguments to avoid sql injection. Lists are converted into a format such as ```{1,2,3,4}```. Lists of lists are converted into ```(1,2,3), (4,5,6), (7,8,9)``` etc. So as to allow easy inserts/upserts.
69
+ - raw: whether to return a ActiveRecord::Response object or a hash when called on an abstract class (like ApplicationRecord). Default can be switched with DYNAMIC_SQL_RAW variable on the class level.
70
+ - instantiate_class: determines what format to return. Can return ActiveRecord objects (User, Post, etc), or whatever raw is set to. I prefer doing the alterantive ```User.dynamic_sql(...)``` which is also supported. For example, ```User.dynamic_sql(...)``` will return User records. ```ApplicationRecord.dynamic_sql(..., raw: false)``` will return a List of Hashes with the column names as keys. ```ApplicationRecord.dynamic_sql(..., raw: true)``` will return an ActiveRecord::Response.
71
+
72
+ other options:
73
+
74
+ - prepare: Defaults to true. Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. Should change whether the command will be prepared, which means that on subsequent calls the command will be faster. Downsides are when, for example, the sql query has hard-coded arguments, the query always changes, causing technical issues as the number of prepared statements stack up.
75
+ - name_modifiers: allows one to change the associated name dynamically.
88
76
  - multi_query: allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
89
- this disables other options including arguments (except name_modifiers). Not sure how it effects prepared statements.
90
- - async: Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. See that methods documentation for more. I was looking through the source code, and I think it only effects how it logs to the logfile?
91
- - raw: whether to return a ActiveRecord::Response object or a hash
92
- - other options: considered sql arguments
77
+ this disables other options including arguments (except name_modifiers). Not sure how it effects prepared statements. Not super useful.
78
+ - async: Defaults to false. Gets passed to ActiveRecord::Base.connection.exec_query as a parameter. See that methods documentation for more. I was looking through the source code, and I think it only effects how it logs to the logfile?
93
79
 
94
80
  <details>
95
81
  <summary>example usage</summary>
@@ -150,7 +136,7 @@ Get users who match a list of ids. Uses a postgresql Array, see the potential is
150
136
 
151
137
  ```ruby
152
138
  id_list = [1,2,3]
153
- return User.headache_sql('get_usrs', %Q{
139
+ return User.dynamic_sql('get_usrs', %Q{
154
140
  SELECT * FROM users WHERE id = ANY (:id_list)
155
141
  }, id_list: id_list)
156
142
  ```
@@ -175,7 +161,7 @@ Do an upsert
175
161
  DO UPDATE SET updated_at = :time
176
162
  }, rows: rows, time: t)
177
163
  ```
178
- This will output sql similar to below. Note this can be done for multiple conversation_participants. Also note that it only sent one time variable as an argument as headache_sql detected that we were sending duplicate information.
164
+ This will output sql similar to below. Note this can be done for multiple conversation_participants. Also note that it only sent one time variable as an argument as dynamic_sql detected that we were sending duplicate information.
179
165
  ```sql
180
166
  INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
181
167
  VALUES ($1,$2,$3,$4,$4)
@@ -187,16 +173,17 @@ This will output sql similar to below. Note this can be done for multiple conver
187
173
 
188
174
 
189
175
  #### self.dynamic_preload(records, associations)
190
- Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above headache_sql method (as it returns a list of records, and not a record relation). This is basically the same as a normal relation preload but it works on a list.
176
+ Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above dynamic_sql method (as it returns a list of records, and not a record relation). This is basically the same as a normal relation preload but it works on a list.
191
177
 
192
178
  <details>
193
179
  <summary>example usage</summary>
194
180
  Preload :votes on some comments. :votes is an active record has_many relation.
195
181
 
196
182
  ```ruby
197
- comments = Comment.headache_sql('get_comments', %Q{
183
+ comments = Comment.dynamic_sql('get_comments', %Q{
198
184
  SELECT * FROM comments LIMIT 4
199
185
  })
186
+ comments.class.to_s # 'Array' note: not a relation.
200
187
  ApplicationRecord.headache_preload(comments, [:votes])
201
188
  puts comments[0].votes #this line should be preloaded and hence not call the database
202
189
 
@@ -204,12 +191,50 @@ Preload :votes on some comments. :votes is an active record has_many relation.
204
191
  user.comments.preload(:votes)
205
192
  ```
206
193
  </details>
207
-
194
+
195
+ #### has_run_migration?(nm)
196
+
197
+ put in a string name of the migration's class and it will say if it has allready run the migration.
198
+ good during enum migrations as the code to migrate wont run if enumerate is there
199
+ as it is not yet enumerated (causing an error when it loads the class that will have the
200
+ enumeration in it). This can lead it to being impossible to commit clean code.
201
+
202
+ <details><summary>example usage</summary>
203
+ only load relationa if it exists in the database
204
+
205
+ ```ruby
206
+ if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
207
+ class UserImageRelation < ApplicationRecord
208
+ belongs_to :imageable, polymorphic: true
209
+ belongs_to :image
210
+ end
211
+ else
212
+ class UserImageRelation; end
213
+ end
214
+
215
+ ```
216
+ </details>
217
+
218
+ #### has_association?(*args)
219
+
220
+ accepts a list of association names, checks if the model has those associations
221
+
222
+ <details><summary>example usage</summary>
223
+ Check if object is a votable class
224
+
225
+ ```ruby
226
+ obj = Comment.first
227
+ obj.has_association?(:votes) #true
228
+ obj = User.first
229
+ obj.has_association?(:votes) #false
230
+ ```
231
+ </details>
232
+
208
233
  #### self.dynamic_instaload_sql(name, insta_array, opts = { })
209
- *instaloads* a bunch of diffrent models at the same time by casting them to json before returning them. Kinda cool. Seems to be more efficient to preloading when i tested it.
234
+ *instaloads* a bunch of diffrent models at the same time by casting them to json before returning them. Kinda cool. Maybe a bit overcomplicated. Seems to be more efficient to preloading when i tested it.
210
235
  - name is passed to dynamic_sql and is the name of the sql request
211
236
  - opts are passed to dynamic_sql (except for the raw option which is set to true. Raw output is not allowed on this request)
212
- - insta-array is an array of instaload method outputs. See examples for more.
237
+ - requires a list of instaload method output which provides information for how to treat each sql block.
213
238
 
214
239
  <details>
215
240
  <summary>example usage</summary>
@@ -274,11 +299,37 @@ the output:
274
299
  ...]}
275
300
 
276
301
 
302
+ ```
303
+ </details>
304
+
305
+ #### self.instaload(sql, table_name: nil, relied_on: false, dont_return: false)
306
+ A method used to prepare data for the dynamic_instaload_sql method. It returns a hash of options.
307
+ - klass called on: if called on an abstract class (ApplicationRecord) it will return a list of hashes with the data. Otherwise returns a list of the classes records.
308
+ - table_name: sets the name of the temporary postgresql table. This can then be used in further instaload sql snippets.
309
+ - relied_on: will make it so other instaload sql snippets can reference this table (it makes it use posrgresql's WITH operator)
310
+ - dont_return: when used with relied_on makes it so that this data is not returned to rails from the database.
311
+
312
+ note that the order of the instaload methods matter depending on how they reference eachother.
313
+ <details>
314
+ <summary> format data </summary>
315
+
316
+ ```ruby
317
+
318
+ User.instaload('SELECT id FROM users WHERE users.id = ANY (:user_ids) AND users.created_at > :time', table_name: 'limited_users', relied_on: true)
319
+ #output:
320
+ {
321
+ table_name: "limited_users",
322
+ klass: "User",
323
+ sql: "\tSELECT id FROM users WHERE users.id = ANY (:user_ids) AND users.created_at > :time",
324
+ relied_on: true,
325
+ dont_return: false
326
+ }
327
+
277
328
  ```
278
329
  </details>
279
330
 
280
331
  #### self.dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false)
281
- taking the output of the dynamic_instaload_sql, this method attaches the models together so they have relations.
332
+ taking the output of the dynamic_instaload_sql, this method attaches the models together so they are attached.
282
333
  - base_name: the name of the table we will be attaching to
283
334
  - attach_name: the name of the table that will be attached
284
335
  - base_on: put a proc here to override the matching key for the base table. Default is, for a user and post type, {|user| user.id}
@@ -337,7 +388,8 @@ See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Als
337
388
 
338
389
  ## Potential Issues
339
390
 
340
- This gem was made with a postgresql database. This could cause a lot of issues with the sql-related methods. I dont have the bandwidth to help switch it elsewhere.
391
+ - This gem was made with a postgresql database. This could cause a lot of issues with the sql-related methods if you do not. I dont have the bandwidth to help switch it elsewhere, but if you want to take charge of that, I would be more than happy to assist by answering questions an pointing out any areas that need transitioning.
392
+ - If you return a password column (for example) as pwd, this gem will accept that. That would mean that the password could me accessed as model.pwd. This is cool - until all passwords are getting logged in production servers. So be wary of accessing, storing, and logging of sensative information. Active Record has in built solutions for this type of data, as long as you dont change the column name. This gem is a sharp knife, its very versitile, but its also, you know, sharp.
341
393
 
342
394
  ## Changelog
343
395
 
@@ -363,6 +415,34 @@ This gem was made with a postgresql database. This could cause a lot of issues w
363
415
  - changed dynamic_attach so that it now uses the model.dynamic attribute, instead of using singleton classes. This is better practice, and also contains all the moving parts of this gem in one place.
364
416
  - added the dynamic_print method to easier see the objects one is working with.
365
417
 
418
+ 2.0.21
419
+ - figured out how to add to a model's @attributes, so .dynamic OpenStruct no longer needed, no longer need dynamic_print, singletons are out aswell. unexpected columns are now usable as just regular attributes.
420
+ - overrode inspect to show the dynamic attributes aswell, warning about passwords printed to logs etc.
421
+
422
+ 2.0.24
423
+ - added error logging in dynamic_sql method for the sql query when and if that fails. So just look at log file to see exactly what sql was running and what the args are.
424
+ - added a dont_return option to the instaload method which works with the relied_on option to have a normal WITH statement that is not returned.
425
+
426
+ 3.0.1
427
+ - Previous versions will break when two sql attributes unexpectantly share the same value. Yeah my bad, was trying to be fancy and decrease sql argument count.
428
+ - People using symbols as sql values (and expecting them to be turned to strings) may have breakages. (aka a sql option like "insert_list:
429
+ [[:a, 1], [:b, 2]]" will break)
430
+ - setting DYNAMIC_SQL_RAW apparently did nothing, you actually need to set DynamicRecordsMeritfront::DYNAMIC_SQL_RAW. Changed default to false, which may break things. But
431
+ since things may be broken already, it seemed like a good time to do this.
432
+ - Went to new version due to 1. a large functionality improvement, 2. the fact that previous versions are broken as explained above.
433
+ - more on breaking error
434
+ - got this error: ActiveRecord::StatementInvalid (PG::ProtocolViolation: ERROR: bind message supplies 3 parameters, but prepared statement "a27" requires 4)
435
+ - this tells me that names are not actually required to be unique for prepared statement identification, which was a bad assumption on my part
436
+ - this also tells me that uniq'ing variables to decrease the number of them was a bad idea which could cause random failures.
437
+ - functionality improvements
438
+ - The biggest change is that names are now optional! name_modifiers is now depreciated functionality as it serves no useful purpose. Will leave in for compatibility but take out of documentation. Used to think the name was related to prepared statements. This will lead simpler ruby code.
439
+ - If name is left out, the name will be set to the location in your app which called the method. For example, when dynamic_sql was called from irb, the name was: "(irb):45:in `irb_binding'". This is done using stack trace functionality.
440
+ - dynamic_instaload_sql is now just instaload_sql. dynamic_instaload_sql has been aliased.
441
+ - Name is optional on instaload_sql aswell
442
+ - MultiAttributeArrays (array's of arrays) which can be passed into dynamic_sql largely for inserts/upserts will now treat symbols as an attribute name. This leads to more consise sql without running into above error.
443
+ - When dynamic_sql errors out, it now posts some helpful information to the log.
444
+ - Added a test script. No experience testing, so its just a method you pass a model, and then it does a rollback to reverse any changes.
445
+
366
446
  ## Contributing
367
447
 
368
448
  Bug reports and pull requests are welcome on GitHub at https://github.com/LukeClancy/dynamic-records-meritfront. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/LukeClancy/dynamic-records-meritfront/blob/master/CODE_OF_CONDUCT.md).
@@ -1,5 +1,5 @@
1
1
 
2
2
  module DynamicRecordsMeritfront
3
- VERSION = '2.0.21'
3
+ VERSION = '3.0.1'
4
4
  end
5
5
  #this file gets overwritten automatically on minor updates, major ones need to be manually changed
@@ -11,15 +11,102 @@ module DynamicRecordsMeritfront
11
11
  module Hashid::Rails::ClassMethods
12
12
  alias hfind find_by_hashid
13
13
  end
14
-
15
14
  included do
16
15
  # include hash id gem
17
16
  include Hashid::Rails
18
17
  #should work, probably able to override by redefining in ApplicationRecord class.
19
18
  #Note we defined here as it breaks early on as Rails.application returns nil
20
19
  PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
21
- DYNAMIC_SQL_RAW = true
20
+ DYNAMIC_SQL_RAW = false
22
21
  end
22
+ class DynamicSqlVariables
23
+ attr_accessor :sql_hash
24
+ attr_accessor :params
25
+ def initialize(params)
26
+ @sql_hash = {}
27
+ self.params = params
28
+ end
29
+
30
+ def add_key_value(key, value = nil)
31
+ value = params[key] if value.nil?
32
+ #tracks the variable and returns the keys sql variable number
33
+ sql_hash[key] ||= convert_to_query_attribute(key, value)
34
+ return sql_hash.keys.index(key) + 1
35
+ end
36
+
37
+ def next_sql_num
38
+ #gets the next sql variable number
39
+ sql_hash.keys.length + 1
40
+ end
41
+
42
+ def get_array_for_exec_query
43
+ sql_hash.values
44
+ end
45
+
46
+ #thank god for some stack overflow people are pretty awesome https://stackoverflow.com/questions/64894375/executing-a-raw-sql-query-in-rails-with-an-array-parameter-against-postgresql
47
+ #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
48
+ #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
49
+
50
+ #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
51
+ # active_model/type/helpers
52
+ # active_model/type/value
53
+ # active_model/type/big_integer
54
+ # active_model/type/binary
55
+ # active_model/type/boolean
56
+ # active_model/type/date
57
+ # active_model/type/date_time
58
+ # active_model/type/decimal
59
+ # active_model/type/float
60
+ # active_model/type/immutable_string
61
+ # active_model/type/integer
62
+ # active_model/type/string
63
+ # active_model/type/time
64
+ # active_model
65
+
66
+ DB_TYPE_MAPS = {
67
+ String => ActiveModel::Type::String,
68
+ Symbol => ActiveModel::Type::String,
69
+ Integer => ActiveModel::Type::BigInteger,
70
+ BigDecimal => ActiveRecord::Type::Decimal,
71
+ TrueClass => ActiveModel::Type::Boolean,
72
+ FalseClass => ActiveModel::Type::Boolean,
73
+ Date => ActiveModel::Type::Date,
74
+ DateTime => ActiveModel::Type::DateTime,
75
+ Time => ActiveModel::Type::Time,
76
+ Float => ActiveModel::Type::Float,
77
+ Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
78
+ }
79
+
80
+ def convert_to_query_attribute(name, v)
81
+ #yes its dumb I know dont look at me look at rails
82
+
83
+ # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
84
+ # binds = [ ActiveRecord::Relation::QueryAttribute.new(
85
+ # "id", 6, ActiveRecord::Type::Integer.new
86
+ # )]
87
+ # ApplicationRecord.connection.exec_query(
88
+ # 'SELECT * FROM users WHERE id = $1', 'sql', binds
89
+ # )
90
+
91
+ return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
92
+ #that we didn't handle properly.
93
+
94
+ type = DB_TYPE_MAPS[v.class]
95
+ if type.nil?
96
+ raise StandardError.new("#{name} (of value: #{v}, class: #{v.class}) unsupported class for ApplicationRecord#headache_sql")
97
+ elsif type.class == Proc
98
+ a = v[0]
99
+ # if a.nil?
100
+ # a = Integer
101
+ # elsif a.class == Array
102
+ a = a.nil? ? Integer : a.class
103
+ type = type.call(a)
104
+ else
105
+ type = type.new
106
+ end
107
+ ActiveRecord::Relation::QueryAttribute.new( name, v, type )
108
+ end
109
+ end
23
110
 
24
111
  class MultiRowExpression
25
112
  #this class is meant to be used in congunction with headache_sql method
@@ -42,26 +129,23 @@ module DynamicRecordsMeritfront
42
129
  #assuming we are putting in an array of arrays.
43
130
  self.val = val
44
131
  end
45
- def for_query(x = 0, unique_value_hash:)
132
+ def for_query(key, var_track)
46
133
  #accepts x = current number of variables previously processed
47
134
  #returns ["sql string with $# location information", variables themselves in order, new x]
48
- db_val = val.map{|attribute_array| "(#{
49
- attribute_index = 0
135
+ x = -1
136
+ db_val = val.map{|attribute_array| "(#{
50
137
  attribute_array.map{|attribute|
51
- prexist_num = unique_value_hash[attribute]
52
- if prexist_num
53
- attribute_array[attribute_index] = nil
54
- ret = "$#{prexist_num}"
55
- else
56
- unique_value_hash[attribute] = x
57
- ret = "$#{x}"
58
- x += 1
59
- end
60
- attribute_index += 1
61
- next ret
138
+ if attribute.kind_of? Symbol
139
+ #allow pointers to other more explicit variables through symbols
140
+ x = var_track.add_key_value(attribute, nil)
141
+ else
142
+ k = "#{key}_#{var_track.next_sql_num.to_s}"
143
+ x = var_track.add_key_value(k, attribute)
144
+ end
145
+ next "$" + x.to_s
62
146
  }.join(",")
63
147
  })"}.join(",")
64
- return db_val, val.flatten.select{|a| not a.nil?}, x
148
+ return db_val
65
149
  end
66
150
  end
67
151
 
@@ -230,70 +314,6 @@ module DynamicRecordsMeritfront
230
314
  end
231
315
  end
232
316
 
233
- #thank god for some stack overflow people are pretty awesome https://stackoverflow.com/questions/64894375/executing-a-raw-sql-query-in-rails-with-an-array-parameter-against-postgresql
234
- #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
235
- #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
236
-
237
- #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
238
- # active_model/type/helpers
239
- # active_model/type/value
240
- # active_model/type/big_integer
241
- # active_model/type/binary
242
- # active_model/type/boolean
243
- # active_model/type/date
244
- # active_model/type/date_time
245
- # active_model/type/decimal
246
- # active_model/type/float
247
- # active_model/type/immutable_string
248
- # active_model/type/integer
249
- # active_model/type/string
250
- # active_model/type/time
251
- # active_model
252
-
253
- DB_TYPE_MAPS = {
254
- String => ActiveModel::Type::String,
255
- Symbol => ActiveModel::Type::String,
256
- Integer => ActiveModel::Type::BigInteger,
257
- BigDecimal => ActiveRecord::Type::Decimal,
258
- TrueClass => ActiveModel::Type::Boolean,
259
- FalseClass => ActiveModel::Type::Boolean,
260
- Date => ActiveModel::Type::Date,
261
- DateTime => ActiveModel::Type::DateTime,
262
- Time => ActiveModel::Type::Time,
263
- Float => ActiveModel::Type::Float,
264
- Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
265
- }
266
-
267
- def convert_to_query_attribute(name, v)
268
- #yes its dumb I know dont look at me look at rails
269
-
270
- # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
271
- # binds = [ ActiveRecord::Relation::QueryAttribute.new(
272
- # "id", 6, ActiveRecord::Type::Integer.new
273
- # )]
274
- # ApplicationRecord.connection.exec_query(
275
- # 'SELECT * FROM users WHERE id = $1', 'sql', binds
276
- # )
277
-
278
- return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
279
- #that we didn't handle properly.
280
-
281
- type = DB_TYPE_MAPS[v.class]
282
- if type.nil?
283
- raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
284
- elsif type.class == Proc
285
- a = v[0]
286
- # if a.nil?
287
- # a = Integer
288
- # elsif a.class == Array
289
- a = a.nil? ? Integer : a.class
290
- type = type.call(a)
291
- else
292
- type = type.new
293
- end
294
-
295
- ActiveRecord::Relation::QueryAttribute.new( name, v, type )
296
- end
297
317
  #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
298
318
  def dynamic_preload(records, associations)
299
319
  ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
@@ -301,122 +321,137 @@ module DynamicRecordsMeritfront
301
321
 
302
322
  alias headache_preload dynamic_preload
303
323
 
304
- def dynamic_sql(name, sql, opts = { }) #see below for opts
305
- # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
306
- # I prefer doing the alterantive
307
- # User.headache_class(...)
308
- # which is also supported
309
- # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
310
- # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
311
- # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
312
- # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
313
- # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
314
- # - async does what it says but I haven't used it yet so. Probabably doesn't work
315
- # - raw switches between using a Hash or a ActiveRecord::Response object when used on a abstract class
316
-
317
- #
318
- # Any other option is assumed to be a sql argument (see other examples in code base)
319
-
320
- #grab options from the opts hash
321
- instantiate_class = opts.delete(:instantiate_class)
322
- name_modifiers = opts.delete(:name_modifiers)
323
- raw = opts.delete(:raw)
324
- raw = DYNAMIC_SQL_RAW if raw.nil?
325
- name_modifiers ||= []
326
- prepare = opts.delete(:prepare) != false
327
- multi_query = opts.delete(:multi_query) == true
328
- async = opts.delete(:async) == true
329
- params = opts
330
-
331
- #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
332
- #by checking if there is an equal existing argument and then using that argument number instead.
333
- #If this functionality is used at a lower level we should probably remove this.
334
- unique_value_hash = {}
335
-
336
- #allows dynamic sql prepared statements.
337
- for mod in name_modifiers
338
- name << "_#{mod.to_s}" unless mod.nil?
339
- end
340
-
341
- unless multi_query
342
- #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
343
- #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
344
- #doing the longer ones first prevents id replacing :id_user -> 1_user
345
- keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
346
- sql_vals = []
347
- x = 1
348
- for key in keys
349
- #replace the key with $1, $2 etc
350
- v = params[key]
351
-
352
- #this is where we guess what it is
353
- looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
354
-
355
- if v.class == MultiRowExpression or looks_like_multi_attribute_array
356
- #it looks like or is a multi-row expression (like those in an insert statement)
357
- v = MultiRowExpression.new(v) if looks_like_multi_attribute_array
358
- #process into usable information
359
- sql_for_replace, mat_vars, new_x = v.for_query(x, unique_value_hash: unique_value_hash)
360
- #replace the key with the sql
361
- if sql.gsub!(":#{key}", sql_for_replace) != nil
362
- #if successful set the new x number and append variables to our sql variables
363
- x = new_x
364
- name_num = 0
365
- mat_vars.each{|mat_var|
366
- name_num += 1
367
- sql_vals << convert_to_query_attribute("#{key}_#{name_num}", mat_var)
368
- }
369
- end
370
- else
371
- prexist_arg_num = unique_value_hash[v]
372
- if prexist_arg_num
373
- sql.gsub!(":#{key}", "$#{prexist_arg_num}")
374
- else
375
- if sql.gsub!(":#{key}", "$#{x}") == nil
376
- #nothing changed, param not used, delete it
377
- params.delete key
378
- else
379
- unique_value_hash[v] = x
380
- sql_vals << convert_to_query_attribute(key, v)
381
- x += 1
382
- end
383
- end
384
- end
385
- end
386
- ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
387
- else
388
- ret = ActiveRecord::Base.connection.execute sql, name
389
- end
390
-
391
- #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
392
- #the following
393
- if instantiate_class or not self.abstract_class
394
- instantiate_class = self if not instantiate_class
395
- #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
396
- ret = zip_ar_result(ret)
397
- return ret.map{|r| dynamic_init(instantiate_class, r)}
398
- # fields = ret.columns
399
- # vals = ret.rows
400
- # ret = vals.map { |v|
401
- # dynamic_init()
402
- # instantiate_class.instantiate(Hash[fields.zip(v)])
403
- # }
404
- else
405
- if raw
406
- return ret
407
- else
408
- return zip_ar_result(ret)
409
- end
410
- end
411
- end
324
+ def dynamic_sql(*args) #see below for opts
325
+ # call like: dynamic_sql(name, sql, option_1: 1, option_2: 2)
326
+ # or like: dynamic_sql(sql, {option: 1, option_2: 2})
327
+ # or like: dynamic_sql(sql, option: 1, option_2: 2)
328
+ # or just: dynamic_sql(sql)
329
+ #
330
+ # Options: (options not listed will be sql arguments)
331
+ # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
332
+ # I prefer doing the alterantive
333
+ # User.headache_class(...)
334
+ # which is also supported
335
+ # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
336
+ # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
337
+ # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
338
+ # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
339
+ # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
340
+ # - async does what it says but I haven't used it yet so. Probabably doesn't work
341
+ # - raw switches between using a Hash or a ActiveRecord::Response object when used on a abstract class
342
+ args << {} unless args[-1].kind_of? Hash
343
+ if args.length == 3
344
+ name, sql, opts = args
345
+ elsif args.length == 2
346
+ sql, opts = args
347
+ #give default name functionality as a pointer to source code location
348
+ #of the method that called this. Love ruby. Meta up the a$$
349
+ first_app_stack_trace = caller[0...3].select{|str| not str.include?('dynamic_records_meritfront.rb')}.first
350
+ shorter_source_loc = first_app_stack_trace.split('/')[-1]
351
+ name = shorter_source_loc
352
+ else
353
+ raise StandardError.new("bad input to DynamicRecordsMeritfront#dynamic_sql method.")
354
+ end
355
+
356
+ #grab options from the opts hash
357
+ instantiate_class = opts.delete(:instantiate_class)
358
+ name_modifiers = opts.delete(:name_modifiers)
359
+ raw = opts.delete(:raw)
360
+ raw = DYNAMIC_SQL_RAW if raw.nil?
361
+ name_modifiers ||= []
362
+ prepare = opts.delete(:prepare) != false
363
+ multi_query = opts.delete(:multi_query) == true
364
+ async = opts.delete(:async) == true
365
+ params = opts
366
+
367
+ #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
368
+ #by checking if there is an equal existing argument and then using that argument number instead.
369
+ #If this functionality is used at a lower level we should probably remove this.
370
+ #________________________________
371
+ #got this error: ActiveRecord::StatementInvalid (PG::ProtocolViolation: ERROR: bind message supplies 3 parameters, but prepared statement "a27" requires 4)
372
+ #this error tells me two things
373
+ # 1. the name of a sql statement actually has no effect on prepared statements (whoops).
374
+ # This means we should accept queries with no name.
375
+ # 2. Need to get rid of the unique variable name functionality which uniques all the variables
376
+ # to decrease the amount sent to database
377
+
378
+ #name_modifiers are super unnecessary now I realize the given name is not actually related
379
+ #to prepped statements. But will keep it as it is backwards compatitable and sorta useful maybe.
380
+ for mod in name_modifiers
381
+ name << "_#{mod.to_s}" unless mod.nil?
382
+ end
383
+ begin
384
+ var_track = DynamicSqlVariables.new(params)
385
+ unless multi_query
386
+ #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
387
+ #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
388
+ #doing the longer ones first prevents id replacing :id_user -> $1_user
389
+ keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
390
+
391
+ for key in keys
392
+ #replace MultiRowExpressions
393
+ v = params[key]
394
+ #check if it looks like one
395
+ looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
396
+ if v.class == MultiRowExpression or looks_like_multi_attribute_array
397
+ #we need to substitute with the correct sql now.
398
+ v = MultiRowExpression.new(v) if looks_like_multi_attribute_array #standardize
399
+ #process into appropriate sql while keeping track of variables
400
+ sql_for_replace = v.for_query(key, var_track)
401
+ #replace the key with the sql
402
+ sql.gsub!(":#{key}", sql_for_replace)
403
+ else
404
+ x = var_track.next_sql_num
405
+ if sql.gsub!(":#{key}", "$#{x}")
406
+ var_track.add_key_value(key, v)
407
+ end
408
+ end
409
+ end
410
+ sql_vals = var_track.get_array_for_exec_query
411
+ ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
412
+ else
413
+ ret = ActiveRecord::Base.connection.execute sql, name
414
+ end
415
+ rescue Exception => e
416
+ #its ok if some of these are empty, just dont want the error
417
+ name ||= ''
418
+ sql ||= ''
419
+ sql_vals ||= ''
420
+ prepare ||= ''
421
+ async ||= ''
422
+ Rails.logger.error(%Q{
423
+ DynamicRecords#dynamic_sql debug info.
424
+ name: #{name.to_s}
425
+ sql: #{sql.to_s}
426
+ sql_vals: #{sql_vals.to_s}
427
+ prepare: #{prepare.to_s}
428
+ async: #{async.to_s}
429
+ })
430
+ raise e
431
+ end
432
+
433
+ #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
434
+ #the following
435
+ if instantiate_class or not self.abstract_class
436
+ instantiate_class = self if not instantiate_class
437
+ #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
438
+ ret = ret.to_a
439
+ return ret.map{|r| dynamic_init(instantiate_class, r)}
440
+ # fields = ret.columns
441
+ # vals = ret.rows
442
+ # ret = vals.map { |v|
443
+ # dynamic_init()
444
+ # instantiate_class.instantiate(Hash[fields.zip(v)])
445
+ # }
446
+ else
447
+ if raw
448
+ return ret
449
+ else
450
+ return ret.to_a
451
+ end
452
+ end
453
+ end
412
454
  alias headache_sql dynamic_sql
413
-
414
- def instaload(sql, table_name: nil, relied_on: false)
415
- table_name ||= "_" + self.to_s.underscore.downcase.pluralize
416
- klass = self.to_s
417
- sql = "\t" + sql.strip
418
- return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on}
419
- end
420
455
 
421
456
  def _dynamic_instaload_handle_with_statements(with_statements)
422
457
  %Q{WITH #{
@@ -427,7 +462,9 @@ module DynamicRecordsMeritfront
427
462
  end
428
463
 
429
464
  def _dynamic_instaload_union(insta_array)
430
- insta_array.map{|insta|
465
+ insta_array.select{|insta|
466
+ not insta[:dont_return]
467
+ }.map{|insta|
431
468
  start = "SELECT row_to_json(#{insta[:table_name]}.*) AS row, '#{insta[:klass]}' AS _klass, '#{insta[:table_name]}' AS _table_name FROM "
432
469
  if insta[:relied_on]
433
470
  ending = "#{insta[:table_name]}\n"
@@ -439,13 +476,31 @@ module DynamicRecordsMeritfront
439
476
  #{ other_statements.map{|os| "SELECT row_to_json(#{os[:table_name]}.*) AS row, '#{os[:klass]}' AS _klass FROM (\n#{os[:sql]}\n)) AS #{os[:table_name]}\n" }.join(' UNION ALL ')}
440
477
  end
441
478
 
442
- def dynamic_instaload_sql(name, insta_array, opts = { })
479
+ def instaload(sql, table_name: nil, relied_on: false, dont_return: false)
480
+ table_name ||= "_" + self.to_s.underscore.downcase.pluralize
481
+ klass = self.to_s
482
+ sql = "\t" + sql.strip
483
+ return {table_name: table_name, klass: klass, sql: sql, relied_on: relied_on, dont_return: dont_return}
484
+ end
485
+
486
+ def instaload_sql(*args) #name, insta_array, opts = { })
487
+ args << {} unless args[-1].kind_of? Hash
488
+ if args.length == 3
489
+ name, insta_array, opts = args
490
+ elsif args.length == 2
491
+ insta_array, opts = args
492
+ name = nil
493
+ else
494
+ raise StandardError.new("bad input to DynamicRecordsMeritfront#instaload_sql method.")
495
+ end
496
+
443
497
  with_statements = insta_array.select{|a| a[:relied_on]}
444
498
  sql = %Q{
445
499
  #{ _dynamic_instaload_handle_with_statements(with_statements) if with_statements.any? }
446
500
  #{ _dynamic_instaload_union(insta_array)}
447
501
  }
448
- ret_hash = insta_array.map{|ar| [ar[:table_name].to_s, []]}.to_h
502
+ returned_arrays = insta_array.select{|ar| not ar[:dont_return]}
503
+ ret_hash = returned_arrays.map{|ar| [ar[:table_name].to_s, []]}.to_h
449
504
  opts[:raw] = true
450
505
  ApplicationRecord.headache_sql(name, sql, opts).rows.each{|row|
451
506
  #need to pre-parsed as it has a non-normal output.
@@ -454,13 +509,94 @@ module DynamicRecordsMeritfront
454
509
  json = row[0]
455
510
  parsed = JSON.parse(json)
456
511
 
457
- ret_hash[table_name ].push dynamic_init(klass, parsed)
512
+ ret_hash[table_name].push dynamic_init(klass, parsed)
458
513
  }
459
514
  return ret_hash
460
515
  end
461
- alias swiss_instaload_sql dynamic_instaload_sql
516
+ alias swiss_instaload_sql instaload_sql
517
+ alias dynamic_instaload_sql instaload_sql
518
+
519
+ def test_drmf(model_with_an_id_column_and_timestamps)
520
+ m = model_with_an_id_column_and_timestamps
521
+ ar = m.superclass
522
+ mtname = m.table_name
523
+ ApplicationRecord.transaction do
524
+ puts 'test recieving columns not normally in the record.'
525
+ rec = m.dynamic_sql(%Q{
526
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
527
+ }).first
528
+ raise StandardError.new('no id') unless rec.id
529
+ raise StandardError.new('no dynamic column') unless rec.random_column
530
+ puts 'pass 1'
531
+
532
+ puts 'test raw off with a custom name'
533
+ recs = ar.dynamic_sql('test_2', %Q{
534
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
535
+ }, raw: false)
536
+ raise StandardError.new('not array of hashes') unless recs.first.class == Hash and recs.class == Array
537
+ rec = recs.first
538
+ raise StandardError.new('no id [raw off]') unless rec['id']
539
+ raise StandardError.new('no dynamic column [raw off]') unless rec['random_column']
540
+ puts 'pass 2'
541
+
542
+ puts 'test raw on'
543
+ recs = ar.dynamic_sql('test_3', %Q{
544
+ SELECT id, 5 AS random_column from #{mtname} LIMIT 10
545
+ }, raw: true)
546
+ raise StandardError.new('not raw') unless recs.class == ActiveRecord::Result
547
+ rec = recs.first
548
+ raise StandardError.new('no id [raw]') unless rec['id']
549
+ raise StandardError.new('no dynamic column [raw]') unless rec['random_column']
550
+ puts 'pass 3'
551
+
552
+ puts 'test when some of the variables are diffrent then the same (#see version 3.0.1 notes)'
553
+ x = Proc.new { |a, b|
554
+ recs = ar.dynamic_sql('test_4', %Q{
555
+ SELECT id, 5 AS random_column from #{mtname} WHERE id > :a LIMIT :b
556
+ }, a: a, b: b)
557
+ }
558
+ x.call(1, 2)
559
+ x.call(1, 1)
560
+ puts 'pass 4'
561
+
562
+ puts 'test MultiAttributeArrays, including symbols and duplicate values.'
563
+ time = DateTime.now
564
+ ids = m.limit(5).pluck(:id)
565
+ values = ids.map{|id|
566
+ [id, :time, time]
567
+ }
568
+ ar.dynamic_sql(%Q{
569
+ INSERT INTO #{mtname} (id, created_at, updated_at)
570
+ VALUES :values
571
+ ON CONFLICT (id) DO NOTHING
572
+ }, values: values, time: time)
573
+ puts 'pass 5'
574
+
575
+ puts 'test arrays'
576
+ recs = ar.dynamic_sql(%Q{
577
+ SELECT id from #{mtname} where id = ANY(:idz)
578
+ }, idz: ids, raw: false)
579
+ puts recs
580
+ raise StandardError.new('wrong length') if recs.length != 5
581
+ puts 'pass 6'
462
582
 
463
-
583
+
584
+ puts 'test instaload_sql'
585
+ out = ar.instaload_sql([
586
+ ar.instaload("SELECT id FROM users", relied_on: true, dont_return: true, table_name: "users_2"),
587
+ ar.instaload("SELECT id FROM users_2 WHERE id % 2 != 0 LIMIT :limit", table_name: 'a'),
588
+ m.instaload("SELECT id FROM users_2 WHERE id % 2 != 1 LIMIT :limit", table_name: 'b')
589
+ ], limit: 2)
590
+ puts out
591
+ raise StandardError.new('Bad return') if out["users_2"]
592
+ raise StandardError.new('Bad return') unless out["a"]
593
+ raise StandardError.new('Bad return') unless out["b"]
594
+ puts 'pass 7'
595
+
596
+ raise ActiveRecord::Rollback
597
+ #ApplicationRecord.dynamic_sql("SELECT * FROM")
598
+ end
599
+ end
464
600
 
465
601
  def dynamic_attach(instaload_sql_output, base_name, attach_name, base_on: nil, attach_on: nil, one_to_one: false)
466
602
  base_arr = instaload_sql_output[base_name]
@@ -563,11 +699,7 @@ module DynamicRecordsMeritfront
563
699
  alias swiss_attach dynamic_attach
564
700
 
565
701
  def zip_ar_result(x)
566
- fields = x.columns
567
- vals = x.rows
568
- vals.map { |v|
569
- Hash[fields.zip(v)]
570
- }
702
+ x.to_a
571
703
  end
572
704
 
573
705
  def dynamic_init(klass, input)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic-records-meritfront
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.21
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luke Clancy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-26 00:00:00.000000000 Z
11
+ date: 2022-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashid-rails