dynamic-records-meritfront 1.1.8 → 1.1.10

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: 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