dynamic-records-meritfront 1.1.8 → 1.1.10

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: 9e6bb65327dba67618a5b1169062f99cabe4058ce81d0bd18da182f11b75fd8f
4
- data.tar.gz: 6337505ce72c61690796a54812977d931c48a96bdf493a4bdb9ade74195d7293
3
+ metadata.gz: 549b1d707e2419d0444845f8c15cd1564572c0ca68a72bf208a39be116feb5f3
4
+ data.tar.gz: 5641ddc311be64741db802cc638b73b586be91d30edcdc171fc04949b3f57542
5
5
  SHA512:
6
- metadata.gz: 0a0eaf0b5886ac66c1df34dac9f627b84a9d8bd1119d1f5e37070364faff2421da21b84480fc2376061c13109cabc6e7f0735d528ba730e1d6400d552fff6ac6
7
- data.tar.gz: 8329c93e4a222ad762a96aff9a4e403f3614e14de277d790bad973bf3baf46e825795bed571928cc52e97937d97e8836eed327b930a790d9e771fb7a154151a1
6
+ metadata.gz: dc0a7c85bbb2ca8cdfd931c5ae9002c319809658d1437f68d3c4356758d71a5db7e277e2bcb87caa37b44608f13bc7a1c0fd38ad0e2aeb7bcc4de3a262519e28
7
+ data.tar.gz: ce81fa5af8b092b10be95b3134b37051a321df53434220bca3ebbb9815b260f3fcba698891282eba179f04c22090466251c0d5a3f3e1e48d6e903b75e5197ce7
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dynamic-records-meritfront (1.1.8)
4
+ dynamic-records-meritfront (1.1.10)
5
5
  hashid-rails
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -11,7 +11,7 @@ I dont tend to get much feedback, so any given would be appreciated.
11
11
  Add this line to your application's Gemfile:
12
12
 
13
13
  ```ruby
14
- gem 'active_record_meritfront'
14
+ gem 'dynamic-records-meritfront'
15
15
  ```
16
16
 
17
17
  And then execute:
@@ -20,10 +20,19 @@ And then execute:
20
20
 
21
21
  Or install it yourself as:
22
22
 
23
- $ gem install active_record_meritfront
23
+ $ gem install dynamic-records-meritfront
24
24
 
25
25
  ## Usage
26
26
 
27
+ ### Apply to your ApplicationRecord class as such
28
+
29
+ ```ruby
30
+ class ApplicationRecord < ActiveRecord::Base
31
+ self.abstract_class = true
32
+ include DynamicRecordsMeritfront
33
+ end
34
+ ```
35
+
27
36
  ### Hashed Global IDS
28
37
 
29
38
  hashed global ids look like this: "gid://meritfront/User/K9YI4K". They also have an optional tag so it can also look like "gid://meritfront/User/K9YI4K@user_image". They are based on global ids.
@@ -40,14 +49,14 @@ See the hashid-rails gem for more (https://github.com/jcypret/hashid-rails). Als
40
49
  #### methods from this gem
41
50
 
42
51
  1. hgid(tag: nil) - get the hgid with optional tag. Aliased to ghid
43
- 2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end
52
+ 2. hgid_as_selector(str, attribute: 'id') - get a css selector for the hgid, good for updating the front-end (especially over cable-ready and morphdom operations)
44
53
  3. self.locate_hgid(hgid_string, with_associations: nil, returns_nil: false) - locates the database record from a hgid. Here are some examples of usage:
45
54
  - ApplicationRecord.locate_hgid(hgid) - <b>DANGEROUS</b> will return any object referenced by the hgid.
46
55
  - User.locate_hgid(hgid) - locates the User record but only if the hgid references a user class. Fires an error if not.
47
56
  - ApplicationRecord.locate_hgid(hgid, with_associations: [:votes]) - locates the record but only if the record's class has a :votes active record association. So for instance, you can accept only votable objects for upvote functionality. Fires an error if the hgid does not match.
48
57
  - User.locate_hgid(hgid, returns_nil: true) - locates the hgid but only if it is the user class. Returns nil if not.
49
58
  4. get_hgid_tag(hgid) - returns the tag attached to the hgid
50
- 5. self.blind_hgid(id, tag) - creates
59
+ 5. self.blind_hgid(id, tag) - creates a hgid without bringing the object down from the database. Useful with hashid-rails encode_id and decode_id methods
51
60
 
52
61
  ### SQL methods
53
62
 
@@ -101,7 +110,7 @@ with options:
101
110
  - name_modifiers: allows one to change the preprocess associated name, useful in cases of dynamic sql.
102
111
  - multi_query: allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
103
112
  this disables other options (except name_modifiers). Not sure how it effects prepared statements.
104
- - async: does what it says but I haven't used it yet so. Probabably doesn't work
113
+ - 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?
105
114
  - other options: considered sql arguments
106
115
 
107
116
  <details>
@@ -118,7 +127,7 @@ Delete Friend Requests between two users after they have become friends.
118
127
  </details>
119
128
 
120
129
  <details>
121
- <summary>advanced example usage</summary>
130
+ <summary>dynamic sql example usage</summary>
122
131
  Get all users who have made a friend request to a particular user with an optional limit.
123
132
  This is an example of why this method is good for dynamic prepared statements.
124
133
 
@@ -138,13 +147,81 @@ This is an example of why this method is good for dynamic prepared statements.
138
147
  ])
139
148
  ```
140
149
  </details>
150
+ <details>
151
+ <summary>example usage with selecting records that match list of ids</summary>
152
+ Get users who match a list of ids. Uses a postgresql Array, see the potential issues section
153
+
154
+ ```ruby
155
+ id_list = [1,2,3]
156
+ return User.headache_sql('get_usrs', %Q{
157
+ SELECT * FROM users WHERE id = ANY (:id_list)
158
+ }, id_list: id_list)
159
+ ```
160
+ </details>
161
+
162
+ <details>
163
+ <summary>example usage a custom upsert</summary>
164
+ Do an upsert
165
+
166
+ ```ruby
167
+ rows = uzrs.map{|u| [
168
+ u.id, #user_id
169
+ self.id, #conversation_id
170
+ from, #invited_by
171
+ t, #created_at
172
+ t, #updated_at
173
+ ]}
174
+ ApplicationRecord.headache_sql("upsert_conversation_invites_2", %Q{
175
+ INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
176
+ VALUES :rows
177
+ ON CONFLICT (conversation_id,user_id)
178
+ DO UPDATE SET updated_at = :time
179
+ }, rows: rows, time: t)
180
+ ```
181
+ 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.
182
+ ```sql
183
+ INSERT INTO conversation_participants (user_id, conversation_id, invited_by, created_at, updated_at)
184
+ VALUES ($1,$2,$3,$4,$4)
185
+ ON CONFLICT (conversation_id,user_id)
186
+ DO UPDATE SET updated_at = $4
187
+ -- [["rows_1", 15], ["rows_2", 67], ["rows_3", 6], ["rows_4", "2022-10-13 20:49:27.441372"]]
188
+ ```
189
+ </details>
190
+
191
+
141
192
 
142
193
  #### self.headache_preload(records, associations)
143
- Preloads from a list of records, and not from a ActiveRecord_Relation. This will be useful when using the above headache_sql method.
194
+ 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).
195
+
196
+ <details>
197
+ <summary>example usage</summary>
198
+ Preload :votes on some comments. :votes is an active record has_many relation.
199
+
200
+ ```ruby
201
+ comments = Comment.headache_sql('get_comments', %Q{
202
+ SELECT * FROM comments LIMIT 4
203
+ })
204
+ ApplicationRecord.headache_preload(comments, [:votes])
205
+ puts comments[0].votes #this line should be preloaded and hence not call the database
206
+ ```
207
+ </details>
208
+
209
+ ## Potential Issues
210
+
211
+ This gem was made with a postgresql database. Although most of the headache_sql code <i>should</i> be usable between databases, there is no abstracted ActiveRecord array type, and no similar classes to ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array for non-postgresql databases (at least, none I could find). I am not 100% sure that it will be an issue, but it might be.
212
+
213
+ Let me know if this actually becomes an issue for someone and I will throw in a workaround.
144
214
 
215
+ ## Changelog
216
+
217
+ 1.1.10
218
+ - Added functionality in headache_sql where for sql arguments that are equal, we only use one sql argument instead of repeating arguments
219
+ - Added functionality in headache_sql for 'multi row expressions' which are inputtable as an Array of Arrays. See the upsert example in the headache_sql documentation above for more.
220
+ - Added a warning in the README for non-postgresql databases. Contact me if you hit issues and we can work it out.
221
+
145
222
  ## Contributing
146
223
 
147
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_record_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/[USERNAME]/active_record_meritfront/blob/master/CODE_OF_CONDUCT.md).
224
+ 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).
148
225
 
149
226
  ## License
150
227
 
@@ -152,4 +229,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
152
229
 
153
230
  ## Code of Conduct
154
231
 
155
- Everyone interacting in the ActiveRecordMeritfront project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/active_record_meritfront/blob/master/CODE_OF_CONDUCT.md).
232
+ Everyone interacting in the ActiveRecordMeritfront project's codebases, issue trackers, chat rooms and mailing lists is expected to follow 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 = '1.1.8'
3
+ VERSION = '1.1.10'
4
4
  end
5
5
  #this file gets overwritten automatically on minor updates, major ones need to be manually changed
@@ -2,7 +2,7 @@ require "dynamic-records-meritfront/version"
2
2
  require 'hashid/rails'
3
3
 
4
4
  module DynamicRecordsMeritfront
5
- extend ActiveSupport::Concern
5
+ extend ActiveSupport::Concern
6
6
 
7
7
  # the two aliases so I dont go insane
8
8
  module Hashid::Rails
@@ -12,199 +12,273 @@ module DynamicRecordsMeritfront
12
12
  alias hfind find_by_hashid
13
13
  end
14
14
 
15
- included do
16
- # include hash id gem
17
- include Hashid::Rails
18
- #should work, probably able to override by redefining in ApplicationRecord class.
19
- #Note we defined here as it breaks early on as Rails.application returns nil
20
- PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
21
- end
15
+ included do
16
+ # include hash id gem
17
+ include Hashid::Rails
18
+ #should work, probably able to override by redefining in ApplicationRecord class.
19
+ #Note we defined here as it breaks early on as Rails.application returns nil
20
+ PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
21
+ end
22
22
 
23
- module ClassMethods
24
- def has_run_migration?(nm)
25
- #put in a string name of the class and it will say if it has allready run the migration.
26
- #good during enum migrations as the code to migrate wont run if enumerate is there
27
- #as it is not yet enumerated (causing an error when it loads the class that will have the
28
- #enumeration in it). This can lead it to being impossible to commit clean code.
29
- #
30
- # example usage one: only create the record class if it currently exists in the database
31
- # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
32
- # class UserImageRelation < ApplicationRecord
33
- # belongs_to :imageable, polymorphic: true
34
- # belongs_to :image
35
- # end
36
- # else
37
- # class UserImageRelation; end
38
- # end
39
- # example usage two: only load relation if it exists in the database
40
- # class UserImageRelation < ApplicationRecord
41
- # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
42
- # belongs_to :imageable, polymorphic: true
43
- # end
44
- # end
45
- #
46
- #current version of migrations
47
- cv = ActiveRecord::Base.connection.migration_context.current_version
48
-
49
- #find the migration object for the name
50
- migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
51
- a.name == nm
52
- }.first
53
-
54
- #if the migration object is nil, it has not yet been created
55
- if migration.nil?
56
- Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
57
- return false
58
- end
59
-
60
- #get the version number for the migration name
61
- needed_version = migration.version
23
+ class MultiRowExpression
24
+ #this class is meant to be used in congunction with headache_sql method
25
+ #Could be used like so in headache_sql:
26
+
27
+ #ApplicationRecord.headache_sql( "teeeest", %Q{
28
+ # INSERT INTO tests(id, username, is_awesome)
29
+ # VALUES :rows
30
+ # ON CONFLICT SET is_awesome = true
31
+ #}, rows: [[1, luke, true], [2, josh, false]])
62
32
 
63
- #if current version is above or equal, the migration has allready been run
64
- migration_ran = (cv >= needed_version)
33
+ #which would output this sql
65
34
 
66
- if migration_ran
67
- Rails.logger.info "#{nm} migration was run on #{needed_version}. If old and all instances are migrated, consider removing code check."
68
- else
69
- Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
70
- end
35
+ # INSERT INTO tests(id, username, is_awesome)
36
+ # VALUES ($0,$1,$2),($3,$4,$5)
37
+ # ON CONFLICT SET is_awesome = true
71
38
 
72
- return migration_ran
39
+ attr_accessor :val
40
+ def initialize(val)
41
+ #assuming we are putting in an array of arrays.
42
+ self.val = val
73
43
  end
74
-
75
- def list_associations
76
- #lists associations (see has_association? below)
77
- reflect_on_all_associations.map(&:name)
44
+ def for_query(x = 0, unique_value_hash:)
45
+ #accepts x = current number of variables previously processed
46
+ #returns ["sql string with $# location information", variables themselves in order, new x]
47
+ db_val = val.map{|attribute_array| "(#{
48
+ attribute_index = 0
49
+ attribute_array.map{|attribute|
50
+ prexist_num = unique_value_hash[attribute]
51
+ if prexist_num
52
+ attribute_array[attribute_index] = nil
53
+ ret = "$#{prexist_num}"
54
+ else
55
+ unique_value_hash[attribute] = x
56
+ ret = "$#{x}"
57
+ x += 1
58
+ end
59
+ attribute_index += 1
60
+ next ret
61
+ }.join(",")
62
+ })"}.join(",")
63
+ return db_val, val.flatten.select{|a| not a.nil?}, x
78
64
  end
65
+ end
79
66
 
80
- def has_association?(*args)
81
- #checks whether current class has needed association (for example, checks it has comments)
82
- #associations can be seen in has_many belongs_to and other similar methods
67
+ module ClassMethods
68
+ def has_run_migration?(nm)
69
+ #put in a string name of the class and it will say if it has allready run the migration.
70
+ #good during enum migrations as the code to migrate wont run if enumerate is there
71
+ #as it is not yet enumerated (causing an error when it loads the class that will have the
72
+ #enumeration in it). This can lead it to being impossible to commit clean code.
73
+ #
74
+ # example usage one: only create the record class if it currently exists in the database
75
+ # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
76
+ # class UserImageRelation < ApplicationRecord
77
+ # belongs_to :imageable, polymorphic: true
78
+ # belongs_to :image
79
+ # end
80
+ # else
81
+ # class UserImageRelation; end
82
+ # end
83
+ # example usage two: only load relation if it exists in the database
84
+ # class UserImageRelation < ApplicationRecord
85
+ # if ApplicationRecord.has_run_migration?('UserImageRelationsTwo')
86
+ # belongs_to :imageable, polymorphic: true
87
+ # end
88
+ # end
89
+ #
90
+ #current version of migrations
91
+ cv = ActiveRecord::Base.connection.migration_context.current_version
92
+
93
+ #find the migration object for the name
94
+ migration = ActiveRecord::Base.connection.migration_context.migrations.filter!{|a|
95
+ a.name == nm
96
+ }.first
97
+
98
+ #if the migration object is nil, it has not yet been created
99
+ if migration.nil?
100
+ Rails.logger.info "No migration found for #{nm}. The migration has not yet been created, or is foreign to this database."
101
+ return false
102
+ end
103
+
104
+ #get the version number for the migration name
105
+ needed_version = migration.version
106
+
107
+ #if current version is above or equal, the migration has allready been run
108
+ migration_ran = (cv >= needed_version)
109
+
110
+ if migration_ran
111
+ Rails.logger.info "#{nm} migration was run on #{needed_version}. If old and all instances are migrated, consider removing code check."
112
+ else
113
+ Rails.logger.info "#{nm} migration has not run yet. This may lead to limited functionality"
114
+ end
115
+
116
+ return migration_ran
117
+ end
118
+
119
+ def list_associations
120
+ #lists associations (see has_association? below)
121
+ reflect_on_all_associations.map(&:name)
122
+ end
83
123
 
84
- #flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
85
- # self.has_association?([:comments, :baseable_comments]) without issue
86
- #
87
- args = args.flatten.map { |a| a.to_sym }
88
- associations = list_associations
89
- (args.length == (associations & args).length)
90
- end
91
-
92
- def blind_hgid(id, tag: nil)
93
- # this method is to get an hgid for a class without actually calling it down from the database.
94
- # For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
95
- unless id.class == String
96
- id = self.encode_id id
97
- end
98
- gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
99
- if !tag
100
- gid
101
- else
102
- "#{gid}@#{tag}"
103
- end
104
- end
124
+ def has_association?(*args)
125
+ #checks whether current class has needed association (for example, checks it has comments)
126
+ #associations can be seen in has_many belongs_to and other similar methods
105
127
 
106
- def string_as_selector(str, attribute: 'id')
107
- #this is needed to allow us to quey various strange characters in the id etc. (see hgids)
108
- #also useful for querying various attributes
109
- return "*[#{attribute}=\"#{str}\"]"
110
- end
111
-
112
- def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
113
- if hgid_string == nil or hgid_string.class != String
114
- if returns_nil
115
- return nil
116
- else
117
- raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
118
- end
119
- end
120
- if hgid_string.include?('@')
121
- hgid_string = hgid_string.split('@')
122
- hgid_string.pop
123
- hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
124
- end
125
- #split the thing
126
- splitz = hgid_string.split('/')
127
- #get the class
128
- begin
129
- cls = splitz[-2].constantize
130
- rescue NameError, NoMethodError
131
- if returns_nil
132
- nil
133
- else
134
- raise StandardError.new 'Unusual or unavailable string or hgid'
135
- end
136
- end
137
- #get the hash
138
- hash = splitz[-1]
139
- # if self == ApplicationRecord (for instance), then check that cls is a subclass
140
- # if self is not ApplicationRecord, then check cls == this objects class
141
- # if with_associations defined, make sure that the class has the associations given (see has_association above)
142
- if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
143
- ( with_associations == nil or cls.has_association?(with_associations) )
144
- #if all is as expected, return the object with its id.
145
- if block_given?
146
- yield(hash)
147
- else
148
- cls.hfind(hash)
149
- end
150
- elsif returns_nil
151
- #allows us to handle issues with input
152
- nil
153
- else
154
- #stops execution as default
155
- raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
156
- end
157
- end
158
-
159
- def get_hgid_tag(hgid_string)
160
- if hgid_string.include?('@')
161
- return hgid_string.split('@')[-1]
162
- else
163
- return nil
164
- end
165
- end
128
+ #flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
129
+ # self.has_association?([:comments, :baseable_comments]) without issue
130
+ #
131
+ args = args.flatten.map { |a| a.to_sym }
132
+ associations = list_associations
133
+ (args.length == (associations & args).length)
134
+ end
135
+
136
+ def blind_hgid(id, tag: nil)
137
+ # this method is to get an hgid for a class without actually calling it down from the database.
138
+ # For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
139
+ unless id.class == String
140
+ id = self.encode_id id
141
+ end
142
+ gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
143
+ if !tag
144
+ gid
145
+ else
146
+ "#{gid}@#{tag}"
147
+ end
148
+ end
166
149
 
167
- #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
168
- #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
169
- #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
150
+ def string_as_selector(str, attribute: 'id')
151
+ #this is needed to allow us to quey various strange characters in the id etc. (see hgids)
152
+ #also useful for querying various attributes
153
+ return "*[#{attribute}=\"#{str}\"]"
154
+ end
155
+
156
+ def locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
157
+ if hgid_string == nil or hgid_string.class != String
158
+ if returns_nil
159
+ return nil
160
+ else
161
+ raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
162
+ end
163
+ end
164
+ if hgid_string.include?('@')
165
+ hgid_string = hgid_string.split('@')
166
+ hgid_string.pop
167
+ hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
168
+ end
169
+ #split the thing
170
+ splitz = hgid_string.split('/')
171
+ #get the class
172
+ begin
173
+ cls = splitz[-2].constantize
174
+ rescue NameError, NoMethodError
175
+ if returns_nil
176
+ nil
177
+ else
178
+ raise StandardError.new 'Unusual or unavailable string or hgid'
179
+ end
180
+ end
181
+ #get the hash
182
+ hash = splitz[-1]
183
+ # if self == ApplicationRecord (for instance), then check that cls is a subclass
184
+ # if self is not ApplicationRecord, then check cls == this objects class
185
+ # if with_associations defined, make sure that the class has the associations given (see has_association above)
186
+ if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
187
+ ( with_associations == nil or cls.has_association?(with_associations) )
188
+ #if all is as expected, return the object with its id.
189
+ if block_given?
190
+ yield(hash)
191
+ else
192
+ cls.hfind(hash)
193
+ end
194
+ elsif returns_nil
195
+ #allows us to handle issues with input
196
+ nil
197
+ else
198
+ #stops execution as default
199
+ raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
200
+ end
201
+ end
202
+
203
+ def get_hgid_tag(hgid_string)
204
+ if hgid_string.include?('@')
205
+ return hgid_string.split('@')[-1]
206
+ else
207
+ return nil
208
+ end
209
+ end
170
210
 
171
- #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
172
- # active_model/type/helpers
173
- # active_model/type/value
174
- # active_model/type/big_integer
175
- # active_model/type/binary
176
- # active_model/type/boolean
177
- # active_model/type/date
178
- # active_model/type/date_time
179
- # active_model/type/decimal
180
- # active_model/type/float
181
- # active_model/type/immutable_string
182
- # active_model/type/integer
183
- # active_model/type/string
184
- # active_model/type/time
185
- # active_model
211
+ #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
212
+ #BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
213
+ #IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
186
214
 
187
- DB_TYPE_MAPS = {
188
- String => ActiveModel::Type::String,
189
- Symbol => ActiveModel::Type::String,
190
- Integer => ActiveModel::Type::BigInteger,
191
- BigDecimal => ActiveRecord::Type::Decimal,
192
- TrueClass => ActiveModel::Type::Boolean,
193
- FalseClass => ActiveModel::Type::Boolean,
194
- Date => ActiveModel::Type::Date,
195
- DateTime => ActiveModel::Type::DateTime,
196
- Time => ActiveModel::Type::Time,
197
- Float => ActiveModel::Type::Float,
198
- Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
199
- }
215
+ #https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
216
+ # active_model/type/helpers
217
+ # active_model/type/value
218
+ # active_model/type/big_integer
219
+ # active_model/type/binary
220
+ # active_model/type/boolean
221
+ # active_model/type/date
222
+ # active_model/type/date_time
223
+ # active_model/type/decimal
224
+ # active_model/type/float
225
+ # active_model/type/immutable_string
226
+ # active_model/type/integer
227
+ # active_model/type/string
228
+ # active_model/type/time
229
+ # active_model
200
230
 
231
+ DB_TYPE_MAPS = {
232
+ String => ActiveModel::Type::String,
233
+ Symbol => ActiveModel::Type::String,
234
+ Integer => ActiveModel::Type::BigInteger,
235
+ BigDecimal => ActiveRecord::Type::Decimal,
236
+ TrueClass => ActiveModel::Type::Boolean,
237
+ FalseClass => ActiveModel::Type::Boolean,
238
+ Date => ActiveModel::Type::Date,
239
+ DateTime => ActiveModel::Type::DateTime,
240
+ Time => ActiveModel::Type::Time,
241
+ Float => ActiveModel::Type::Float,
242
+ Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
243
+ }
201
244
 
202
- #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
203
- def headache_preload(records, associations)
204
- ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
245
+ def convert_to_query_attribute(name, v)
246
+ #yes its dumb I know dont look at me look at rails
247
+
248
+ # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
249
+ # binds = [ ActiveRecord::Relation::QueryAttribute.new(
250
+ # "id", 6, ActiveRecord::Type::Integer.new
251
+ # )]
252
+ # ApplicationRecord.connection.exec_query(
253
+ # 'SELECT * FROM users WHERE id = $1', 'sql', binds
254
+ # )
255
+
256
+ return v if v.kind_of? ActiveRecord::Relation::QueryAttribute #so users can have fine-grained control if they are trying to do something
257
+ #that we didn't handle properly.
258
+
259
+ type = DB_TYPE_MAPS[v.class]
260
+ if type.nil?
261
+ raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
262
+ elsif type.class == Proc
263
+ a = v[0]
264
+ # if a.nil?
265
+ # a = Integer
266
+ # elsif a.class == Array
267
+ a = a.nil? ? Integer : a.class
268
+ type = type.call(a)
269
+ else
270
+ type = type.new
271
+ end
272
+
273
+ ActiveRecord::Relation::QueryAttribute.new( name, v, type )
205
274
  end
206
275
 
207
- def headache_sql(name, sql, opts = { }) #see below for opts
276
+ #allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
277
+ def headache_preload(records, associations)
278
+ ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
279
+ end
280
+
281
+ def headache_sql(name, sql, opts = { }) #see below for opts
208
282
  # - instantiate_class - returns User, Post, etc objects instead of straight sql output.
209
283
  # I prefer doing the alterantive
210
284
  # User.headache_class(...)
@@ -212,11 +286,12 @@ module DynamicRecordsMeritfront
212
286
  # - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
213
287
  # - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
214
288
  # - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
215
- # this disables other options (except name_modifiers). Not sure how it effects prepared statements.
289
+ # this disables other options (except name_modifiers). Not sure how it effects prepared statements. Its a fairly useless
290
+ # command as you can do multiple queries anyway with 'WITH' statements and also gain the other options.
216
291
  # - async does what it says but I haven't used it yet so. Probabably doesn't work
217
292
  #
218
293
  # Any other option is assumed to be a sql argument (see other examples in code base)
219
-
294
+
220
295
  #grab options from the opts hash
221
296
  instantiate_class = opts.delete(:instantiate_class)
222
297
  name_modifiers = opts.delete(:name_modifiers)
@@ -225,74 +300,85 @@ module DynamicRecordsMeritfront
225
300
  multi_query = opts.delete(:multi_query) == true
226
301
  async = opts.delete(:async) == true
227
302
  params = opts
228
-
303
+
304
+ #unique value hash cuts down on the number of repeated arguments like in an update or insert statement
305
+ #by checking if there is an equal existing argument and then using that argument number instead.
306
+ #If this functionality is used at a lower level we should probably remove this.
307
+ unique_value_hash = {}
308
+
229
309
  #allows dynamic sql prepared statements.
230
310
  for mod in name_modifiers
231
311
  name << "_#{mod.to_s}" unless mod.nil?
232
312
  end
233
-
313
+
234
314
  unless multi_query
235
315
  #https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
236
-
237
316
  #change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
238
317
  #doing the longer ones first prevents id replacing :id_user -> 1_user
239
318
  keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
240
- vals = []
319
+ sql_vals = []
241
320
  x = 1
242
321
  for key in keys
243
322
  #replace the key with $1, $2 etc
244
- if sql.gsub!(":#{key}", "$#{x}") == nil
245
- #nothing changed, param not used, delete it
246
- x -= 1
247
- params.delete key
323
+ v = params[key]
324
+
325
+ #this is where we guess what it is
326
+ looks_like_multi_attribute_array = ((v.class == Array) and (not v.first.nil?) and (v.first.class == Array))
327
+
328
+ if v.class == MultiRowExpression or looks_like_multi_attribute_array
329
+ #it looks like or is a multi-row expression (like those in an insert statement)
330
+ v = MultiRowExpression.new(v) if looks_like_multi_attribute_array
331
+ #process into usable information
332
+ sql_for_replace, mat_vars, new_x = v.for_query(x, unique_value_hash: unique_value_hash)
333
+ #replace the key with the sql
334
+ if sql.gsub!(":#{key}", sql_for_replace) != nil
335
+ #if successful set the new x number and append variables to our sql variables
336
+ x = new_x
337
+ name_num = 0
338
+ mat_vars.each{|mat_var|
339
+ name_num += 1
340
+ sql_vals << convert_to_query_attribute("#{key}_#{name_num}", mat_var)
341
+ }
342
+ end
248
343
  else
249
- #yes its dumb I know dont look at me look at rails
250
-
251
- # https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
252
- # binds = [ ActiveRecord::Relation::QueryAttribute.new(
253
- # "id", 6, ActiveRecord::Type::Integer.new
254
- # )]
255
- # ApplicationRecord.connection.exec_query(
256
- # 'SELECT * FROM users WHERE id = $1', 'sql', binds
257
- # )
258
- v = params[key]
259
- type = DB_TYPE_MAPS[v.class]
260
- if type.nil?
261
- raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
262
- elsif type.class == Proc
263
- a = v[0]
264
- a.nil? ? a = Integer : a = a.class
265
- type = type.call(a)
344
+ prexist_arg_num = unique_value_hash[v]
345
+ if prexist_arg_num
346
+ sql.gsub!(":#{key}", "$#{prexist_arg_num}")
266
347
  else
267
- type = type.new
348
+ if sql.gsub!(":#{key}", "$#{x}") == nil
349
+ #nothing changed, param not used, delete it
350
+ params.delete key
351
+ else
352
+ unique_value_hash[v] = x
353
+ sql_vals << convert_to_query_attribute(key, v)
354
+ x += 1
355
+ end
268
356
  end
269
-
270
- vals << ActiveRecord::Relation::QueryAttribute.new( key, v, type )
271
357
  end
272
- x += 1
273
358
  end
274
- ret = ActiveRecord::Base.connection.exec_query sql, name, vals, prepare: prepare, async: async
359
+ ret = ActiveRecord::Base.connection.exec_query sql, name, sql_vals, prepare: prepare, async: async
275
360
  else
276
361
  ret = ActiveRecord::Base.connection.execute sql, name
277
362
  end
278
-
363
+
279
364
  #this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
280
365
  #the following
281
366
  if instantiate_class or self != ApplicationRecord
282
367
  instantiate_class = self if not instantiate_class
283
- #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
284
- fields = ret.columns
285
- vals = ret.rows
286
- ret = vals.map { |v|
287
- instantiate_class.instantiate(Hash[fields.zip(v)])
288
- }
368
+ #no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
369
+ fields = ret.columns
370
+ vals = ret.rows
371
+ ret = vals.map { |v|
372
+ instantiate_class.instantiate(Hash[fields.zip(v)])
373
+ }
289
374
  end
290
375
  ret
291
376
  end
292
- def quick_safe_increment(id, col, val)
293
- where(id: id).update_all("#{col} = #{col} + #{val}")
294
- end
295
- end
377
+
378
+ def quick_safe_increment(id, col, val)
379
+ where(id: id).update_all("#{col} = #{col} + #{val}")
380
+ end
381
+ end
296
382
 
297
383
  def list_associations
298
384
  #lists associations (see class method above)
@@ -321,11 +407,10 @@ module DynamicRecordsMeritfront
321
407
  return self.class.string_as_selector(gidstr, attribute: attribute)
322
408
  end
323
409
 
324
- #just for ease of use
325
- def headache_preload(records, associations)
326
- self.class.headache_preload(records, associations)
327
- end
328
-
410
+ #just for ease of use
411
+ def headache_preload(records, associations)
412
+ self.class.headache_preload(records, associations)
413
+ end
329
414
  def safe_increment(col, val) #also used in follow, also used in comment#kill
330
415
  self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
331
416
  end
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: 1.1.8
4
+ version: 1.1.10
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-05 00:00:00.000000000 Z
11
+ date: 2022-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashid-rails