dynamic-records-meritfront 0.1.4 → 0.1.7
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 +4 -4
- data/Gemfile.lock +1 -1
- data/lib/dynamic-records-meritfront/version.rb +1 -1
- data/lib/dynamic-records-meritfront.rb +286 -281
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f1933cb7f3bc36b042f8969cc557509cfef64151ac5882f00243d1baff631ee1
|
4
|
+
data.tar.gz: c4558116af992883844a10035d66c38c4d7a41a5d607021018d4272290862053
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d58ddf4617e1c5f275e0c5b2321d3df1b1ad29aef7385738832f58a54b0e7ce113b165301a2273ee7c1729a446ed154751eaba0d14afb90ff44f4d7bdf8ea8f6
|
7
|
+
data.tar.gz: 4fab3726bf1cbfedabcd83348c367ae833138cde6fda8f156c592835c05a9a3cb3387c97319df160c1414a5d0019bb226d6cdd11bf6780d93ceeb13c288a5bd3
|
data/Gemfile.lock
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
require "dynamic-records-meritfront/version"
|
2
|
+
require 'hashid/rails'
|
2
3
|
|
3
4
|
module DynamicRecordsMeritfront
|
4
5
|
extend ActiveSupport::Concern
|
5
|
-
|
6
|
-
# include hash id gem
|
7
|
-
include Hashid::Rails
|
8
6
|
|
9
7
|
# the two aliases so I dont go insane
|
10
8
|
module Hashid::Rails
|
@@ -14,102 +12,298 @@ module DynamicRecordsMeritfront
|
|
14
12
|
alias hfind find_by_hashid
|
15
13
|
end
|
16
14
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
+
|
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
|
62
|
+
|
63
|
+
#if current version is above or equal, the migration has allready been run
|
64
|
+
migration_ran = (cv >= needed_version)
|
65
|
+
|
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
|
71
|
+
|
72
|
+
return migration_ran
|
73
|
+
end
|
74
|
+
|
75
|
+
def list_associations
|
76
|
+
#lists associations (see has_association? below)
|
77
|
+
reflect_on_all_associations.map(&:name)
|
78
|
+
end
|
79
|
+
|
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
|
83
|
+
|
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
|
105
|
+
|
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
|
166
|
+
|
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
|
170
|
+
|
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
|
186
|
+
|
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
|
+
}
|
200
|
+
|
201
|
+
|
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
|
205
|
+
end
|
206
|
+
|
207
|
+
def headache_sql(name, sql, opts = { }) #see below for opts
|
208
|
+
# - instantiate_class - returns User, Post, etc objects instead of straight sql output.
|
209
|
+
# I prefer doing the alterantive
|
210
|
+
# User.headache_class(...)
|
211
|
+
# which is also supported
|
212
|
+
# - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
|
213
|
+
# - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
|
214
|
+
# - 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.
|
216
|
+
# - async does what it says but I haven't used it yet so. Probabably doesn't work
|
217
|
+
#
|
218
|
+
# Any other option is assumed to be a sql argument (see other examples in code base)
|
219
|
+
|
220
|
+
#grab options from the opts hash
|
221
|
+
instantiate_class = opts.delete(:instantiate_class)
|
222
|
+
name_modifiers = opts.delete(:name_modifiers)
|
223
|
+
name_modifiers ||= []
|
224
|
+
prepare = opts.delete(:prepare) != false
|
225
|
+
multi_query = opts.delete(:multi_query) == true
|
226
|
+
async = opts.delete(:async) == true
|
227
|
+
params = opts
|
228
|
+
|
229
|
+
#allows dynamic sql prepared statements.
|
230
|
+
for mod in name_modifiers
|
231
|
+
name << "_#{mod.to_s}" unless mod.nil?
|
232
|
+
end
|
233
|
+
|
234
|
+
unless multi_query
|
235
|
+
#https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
|
236
|
+
|
237
|
+
#change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
|
238
|
+
#doing the longer ones first prevents id replacing :id_user -> 1_user
|
239
|
+
keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
|
240
|
+
vals = []
|
241
|
+
x = 1
|
242
|
+
for key in keys
|
243
|
+
#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
|
248
|
+
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)
|
266
|
+
else
|
267
|
+
type = type.new
|
268
|
+
end
|
269
|
+
|
270
|
+
vals << ActiveRecord::Relation::QueryAttribute.new( key, v, type )
|
271
|
+
end
|
272
|
+
x += 1
|
273
|
+
end
|
274
|
+
ret = ActiveRecord::Base.connection.exec_query sql, name, vals, prepare: prepare, async: async
|
275
|
+
else
|
276
|
+
ret = ActiveRecord::Base.connection.execute sql, name
|
277
|
+
end
|
278
|
+
|
279
|
+
#this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
|
280
|
+
#the following
|
281
|
+
if instantiate_class or self != ApplicationRecord
|
282
|
+
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
|
+
}
|
289
|
+
end
|
290
|
+
ret
|
291
|
+
end
|
292
|
+
def quick_safe_increment(id, col, val)
|
293
|
+
where(id: id).update_all("#{col} = #{col} + #{val}")
|
294
|
+
end
|
295
|
+
end
|
72
296
|
|
73
297
|
def list_associations
|
74
298
|
#lists associations (see class method above)
|
75
299
|
self.class.list_associations
|
76
300
|
end
|
77
301
|
|
78
|
-
def self.has_association?(*args)
|
79
|
-
#checks whether current class has needed association (for example, checks it has comments)
|
80
|
-
#associations can be seen in has_many belongs_to and other similar methods
|
81
|
-
|
82
|
-
#flattens so you can pass self.has_association?(:comments, :baseable_comments) aswell as
|
83
|
-
# self.has_association?([:comments, :baseable_comments]) without issue
|
84
|
-
#
|
85
|
-
args = args.flatten.map { |a| a.to_sym }
|
86
|
-
associations = list_associations
|
87
|
-
(args.length == (associations & args).length)
|
88
|
-
end
|
89
|
-
|
90
302
|
def has_association?(*args)
|
91
303
|
#just redirects to the class method for ease of use (see class method above)
|
92
304
|
self.class.has_association?(*args)
|
93
305
|
end
|
94
306
|
|
95
|
-
#should work, probably able to override by redefining in ApplicationRecord class
|
96
|
-
|
97
|
-
PROJECT_NAME = Rails.application.class.to_s.split("::").first.to_s.downcase
|
98
|
-
|
99
|
-
# this method is to get an hgid for a class without actually calling it down from the database.
|
100
|
-
# For example Notification.blind_hgid 1 will give gid://PROJECT_NAME/Notification/69DAB69 etc.
|
101
|
-
def self.blind_hgid(id, tag: nil)
|
102
|
-
unless id.class == String
|
103
|
-
id = self.encode_id id
|
104
|
-
end
|
105
|
-
gid = "gid://#{PROJECT_NAME}/#{self.to_s}/#{id}"
|
106
|
-
if !tag
|
107
|
-
gid
|
108
|
-
else
|
109
|
-
"#{gid}@#{tag}"
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
307
|
# custom hash GlobalID
|
114
308
|
def hgid(tag: nil)
|
115
309
|
gid = "gid://#{PROJECT_NAME}/#{self.class.to_s}/#{self.hid}"
|
@@ -127,202 +321,13 @@ module DynamicRecordsMeritfront
|
|
127
321
|
return self.class.string_as_selector(gidstr, attribute: attribute)
|
128
322
|
end
|
129
323
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
end
|
324
|
+
#just for ease of use
|
325
|
+
def headache_preload(records, associations)
|
326
|
+
self.class.headache_preload(records, associations)
|
327
|
+
end
|
135
328
|
|
136
|
-
def self.locate_hgid(hgid_string, with_associations: nil, returns_nil: false)
|
137
|
-
if hgid_string == nil or hgid_string.class != String
|
138
|
-
if returns_nil
|
139
|
-
return nil
|
140
|
-
else
|
141
|
-
raise StandardError.new("non-string class passed to ApplicationRecord#locate_hgid as the hgid_string variable")
|
142
|
-
end
|
143
|
-
end
|
144
|
-
if hgid_string.include?('@')
|
145
|
-
hgid_string = hgid_string.split('@')
|
146
|
-
hgid_string.pop
|
147
|
-
hgid_string = hgid_string.join('@') # incase the model was a tag that was tagged. (few months later: Wtf? Guess ill keep it)
|
148
|
-
end
|
149
|
-
#split the thing
|
150
|
-
splitz = hgid_string.split('/')
|
151
|
-
#get the class
|
152
|
-
begin
|
153
|
-
cls = splitz[-2].constantize
|
154
|
-
rescue NameError, NoMethodError => e
|
155
|
-
if returns_nil
|
156
|
-
nil
|
157
|
-
else
|
158
|
-
raise StandardError.new 'Unusual or unavailable string or hgid'
|
159
|
-
end
|
160
|
-
end
|
161
|
-
#get the hash
|
162
|
-
hash = splitz[-1]
|
163
|
-
# if self == ApplicationRecord (for instance), then check that cls is a subclass
|
164
|
-
# if self is not ApplicationRecord, then check cls == this objects class
|
165
|
-
# if with_associations defined, make sure that the class has the associations given (see has_association above)
|
166
|
-
if ((self.abstract_class? and cls < self) or ( (not self.abstract_class?) and cls == self )) and
|
167
|
-
( with_associations == nil or cls.has_association?(with_associations) )
|
168
|
-
#if all is as expected, return the object with its id.
|
169
|
-
if block_given?
|
170
|
-
yield(hash)
|
171
|
-
else
|
172
|
-
cls.hfind(hash)
|
173
|
-
end
|
174
|
-
elsif returns_nil
|
175
|
-
#allows us to handle issues with input
|
176
|
-
nil
|
177
|
-
else
|
178
|
-
#stops execution as default
|
179
|
-
raise StandardError.new 'Not the expected class, or a subclass of ApplicationRecord if called on that.'
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
def self.get_hgid_tag(hgid_string)
|
184
|
-
if hgid_string.include?('@')
|
185
|
-
return hgid_string.split('@')[-1]
|
186
|
-
else
|
187
|
-
return nil
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
#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
|
192
|
-
#BigIntArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::BigInteger.new).freeze
|
193
|
-
#IntegerArray = ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::Integer.new).freeze
|
194
|
-
|
195
|
-
#https://api.rubyonrails.org/files/activemodel/lib/active_model/type_rb.html
|
196
|
-
# active_model/type/helpers
|
197
|
-
# active_model/type/value
|
198
|
-
# active_model/type/big_integer
|
199
|
-
# active_model/type/binary
|
200
|
-
# active_model/type/boolean
|
201
|
-
# active_model/type/date
|
202
|
-
# active_model/type/date_time
|
203
|
-
# active_model/type/decimal
|
204
|
-
# active_model/type/float
|
205
|
-
# active_model/type/immutable_string
|
206
|
-
# active_model/type/integer
|
207
|
-
# active_model/type/string
|
208
|
-
# active_model/type/time
|
209
|
-
# active_model
|
210
|
-
|
211
|
-
DB_TYPE_MAPS = {
|
212
|
-
String => ActiveModel::Type::String,
|
213
|
-
Symbol => ActiveModel::Type::String,
|
214
|
-
Integer => ActiveModel::Type::BigInteger,
|
215
|
-
BigDecimal => ActiveRecord::Type::Decimal,
|
216
|
-
TrueClass => ActiveModel::Type::Boolean,
|
217
|
-
FalseClass => ActiveModel::Type::Boolean,
|
218
|
-
Date => ActiveModel::Type::Date,
|
219
|
-
DateTime => ActiveModel::Type::DateTime,
|
220
|
-
Time => ActiveModel::Type::Time,
|
221
|
-
Float => ActiveModel::Type::Float,
|
222
|
-
Array => Proc.new{ |first_el_class| ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(DB_TYPE_MAPS[first_el_class].new) }
|
223
|
-
}
|
224
|
-
|
225
|
-
|
226
|
-
#allows us to preload on a list and not a active record relation. So basically from the output of headache_sql
|
227
|
-
def self.headache_preload(records, associations)
|
228
|
-
ActiveRecord::Associations::Preloader.new(records: records, associations: associations).call
|
229
|
-
end
|
230
|
-
|
231
|
-
#just for ease of use
|
232
|
-
def headache_preload(records, associations)
|
233
|
-
self.class.headache_preload(records, associations)
|
234
|
-
end
|
235
|
-
|
236
|
-
def self.headache_sql(name, sql, opts = { }) #see below for opts
|
237
|
-
# - instantiate_class - returns User, Post, etc objects instead of straight sql output.
|
238
|
-
# I prefer doing the alterantive
|
239
|
-
# User.headache_class(...)
|
240
|
-
# which is also supported
|
241
|
-
# - prepare sets whether the db will preprocess the strategy for lookup (defaults true) (I dont think turning this off works...)
|
242
|
-
# - name_modifiers allows one to change the preprocess associated name, useful in cases of dynamic sql.
|
243
|
-
# - multi_query allows more than one query (you can seperate an insert and an update with ';' I dont know how else to say it.)
|
244
|
-
# this disables other options (except name_modifiers). Not sure how it effects prepared statements.
|
245
|
-
# - async does what it says but I haven't used it yet so. Probabably doesn't work
|
246
|
-
#
|
247
|
-
# Any other option is assumed to be a sql argument (see other examples in code base)
|
248
|
-
|
249
|
-
#grab options from the opts hash
|
250
|
-
instantiate_class = opts.delete(:instantiate_class)
|
251
|
-
name_modifiers = opts.delete(:name_modifiers)
|
252
|
-
name_modifiers ||= []
|
253
|
-
prepare = opts.delete(:prepare) != false
|
254
|
-
multi_query = opts.delete(:multi_query) == true
|
255
|
-
async = opts.delete(:async) == true
|
256
|
-
params = opts
|
257
|
-
|
258
|
-
#allows dynamic sql prepared statements.
|
259
|
-
for mod in name_modifiers
|
260
|
-
name << "_#{mod.to_s}" unless mod.nil?
|
261
|
-
end
|
262
|
-
|
263
|
-
unless multi_query
|
264
|
-
#https://stackoverflow.com/questions/49947990/can-i-execute-a-raw-sql-query-leverage-prepared-statements-and-not-use-activer/67442353#67442353
|
265
|
-
|
266
|
-
#change the keys to $1, $2 etc. this step is needed for ex. {id: 1, id_user: 2}.
|
267
|
-
#doing the longer ones first prevents id replacing :id_user -> 1_user
|
268
|
-
keys = params.keys.sort{|a,b| b.to_s.length <=> a.to_s.length}
|
269
|
-
vals = []
|
270
|
-
x = 1
|
271
|
-
for key in keys
|
272
|
-
#replace the key with $1, $2 etc
|
273
|
-
if sql.gsub!(":#{key}", "$#{x}") == nil
|
274
|
-
#nothing changed, param not used, delete it
|
275
|
-
x -= 1
|
276
|
-
params.delete key
|
277
|
-
else
|
278
|
-
#yes its dumb I know dont look at me look at rails
|
279
|
-
|
280
|
-
# https://stackoverflow.com/questions/40407700/rails-exec-query-bindings-ignored
|
281
|
-
# binds = [ ActiveRecord::Relation::QueryAttribute.new(
|
282
|
-
# "id", 6, ActiveRecord::Type::Integer.new
|
283
|
-
# )]
|
284
|
-
# ApplicationRecord.connection.exec_query(
|
285
|
-
# 'SELECT * FROM users WHERE id = $1', 'sql', binds
|
286
|
-
# )
|
287
|
-
v = params[key]
|
288
|
-
type = DB_TYPE_MAPS[v.class]
|
289
|
-
if type.nil?
|
290
|
-
raise StandardError.new("#{v}'s class #{v.class} unsupported type right now for ApplicationRecord#headache_sql")
|
291
|
-
elsif type.class == Proc
|
292
|
-
a = v[0]
|
293
|
-
a.nil? ? a = Integer : a = a.class
|
294
|
-
type = type.call(a)
|
295
|
-
else
|
296
|
-
type = type.new
|
297
|
-
end
|
298
|
-
|
299
|
-
vals << ActiveRecord::Relation::QueryAttribute.new( key, v, type )
|
300
|
-
end
|
301
|
-
x += 1
|
302
|
-
end
|
303
|
-
ret = ActiveRecord::Base.connection.exec_query sql, name, vals, prepare: prepare, async: async
|
304
|
-
else
|
305
|
-
ret = ActiveRecord::Base.connection.execute sql, name
|
306
|
-
end
|
307
|
-
|
308
|
-
#this returns a PG::Result object, which is pretty basic. To make this into User/Post/etc objects we do
|
309
|
-
#the following
|
310
|
-
if instantiate_class or self != ApplicationRecord
|
311
|
-
instantiate_class = self if not instantiate_class
|
312
|
-
#no I am not actually this cool see https://stackoverflow.com/questions/30826015/convert-pgresult-to-an-active-record-model
|
313
|
-
fields = ret.columns
|
314
|
-
vals = ret.rows
|
315
|
-
ret = vals.map { |v|
|
316
|
-
instantiate_class.instantiate(Hash[fields.zip(v)])
|
317
|
-
}
|
318
|
-
end
|
319
|
-
ret
|
320
|
-
end
|
321
329
|
def safe_increment(col, val) #also used in follow, also used in comment#kill
|
322
330
|
self.class.where(id: self.id).update_all("#{col} = #{col} + #{val}")
|
323
331
|
end
|
324
|
-
def self.quick_safe_increment(id, col, val)
|
325
|
-
where(id: id).update_all("#{col} = #{col} + #{val}")
|
326
|
-
end
|
327
332
|
|
328
333
|
end
|