Compare commits
54 Commits
master
...
2012032101
| Author | SHA1 | Date | |
|---|---|---|---|
| f9527d8e29 | |||
| 1c56403272 | |||
| 779ff22770 | |||
| 7923288dfd | |||
| 37a3af105a | |||
| ca0e36aa04 | |||
| f10e00e3f7 | |||
| 33af203de3 | |||
| 8afa25e359 | |||
| 95128fcad8 | |||
| 0cb151ddc6 | |||
| cdbdd476c9 | |||
| 7124971d00 | |||
| b7ca835316 | |||
| a0b0c7528d | |||
| fee0c04b0e | |||
| 84d2b3cb1a | |||
| e50d27cda8 | |||
| ad0251e256 | |||
| d70596ab44 | |||
| 0e4fd2040d | |||
| c94a2961d0 | |||
| 245c2063f0 | |||
| 77a9b36901 | |||
| 060115edf4 | |||
| 057a22569b | |||
| f5e520cc53 | |||
| 6ed9bf8430 | |||
| 2fe08d9e42 | |||
| 4cc6b8fb04 | |||
| 1cc655b678 | |||
| 607a782855 | |||
| a690cd959b | |||
| dae115cc8f | |||
| d32f8edb6e | |||
| d0c74cff8c | |||
| 4eb79ea6b5 | |||
| a75febcb59 | |||
| dc139ff2fd | |||
| 531c1e611c | |||
| 4586112241 | |||
| 7f9cd45dca | |||
| 5700bf9db4 | |||
| b3925c4465 | |||
| f40df69100 | |||
| dd42f241f7 | |||
| 15065ba627 | |||
| 9a37792328 | |||
| f52445930e | |||
| fdea0ad9c7 | |||
| 14270fe49f | |||
| 4259168703 | |||
| 5249e74ca7 | |||
| cf0cbff302 |
@@ -0,0 +1,3 @@
|
||||
source "http://rubygems.org"
|
||||
|
||||
gem 'metasploit_data_models', :path => "../metasploit_data_models"
|
||||
@@ -0,0 +1,47 @@
|
||||
PATH
|
||||
remote: ../metasploit_data_models
|
||||
specs:
|
||||
metasploit_data_models (0.0.1)
|
||||
activerecord
|
||||
activesupport
|
||||
pg
|
||||
pry
|
||||
|
||||
GEM
|
||||
remote: http://rubygems.org/
|
||||
specs:
|
||||
activemodel (3.1.3)
|
||||
activesupport (= 3.1.3)
|
||||
builder (~> 3.0.0)
|
||||
i18n (~> 0.6)
|
||||
activerecord (3.1.3)
|
||||
activemodel (= 3.1.3)
|
||||
activesupport (= 3.1.3)
|
||||
arel (~> 2.2.1)
|
||||
tzinfo (~> 0.3.29)
|
||||
activesupport (3.1.3)
|
||||
multi_json (~> 1.0)
|
||||
arel (2.2.1)
|
||||
builder (3.0.0)
|
||||
coderay (0.9.8)
|
||||
i18n (0.6.0)
|
||||
method_source (0.6.7)
|
||||
ruby_parser (>= 2.3.1)
|
||||
multi_json (1.0.4)
|
||||
pg (0.12.2)
|
||||
pry (0.9.7.4)
|
||||
coderay (~> 0.9.8)
|
||||
method_source (~> 0.6.7)
|
||||
ruby_parser (>= 2.3.1)
|
||||
slop (~> 2.1.0)
|
||||
ruby_parser (2.3.1)
|
||||
sexp_processor (~> 3.0)
|
||||
sexp_processor (3.0.10)
|
||||
slop (2.1.0)
|
||||
tzinfo (0.3.31)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
metasploit_data_models!
|
||||
Executable
+388
@@ -0,0 +1,388 @@
|
||||
# EXTENSIONS => CONTENT TYPE
|
||||
csh: application/x-csh
|
||||
x_t: model/vnd.parasolid.transmit.text
|
||||
kpt: application/vnd.kde.kpresenter
|
||||
vst: application/vnd.visio
|
||||
ksp: application/vnd.kde.kspread
|
||||
fsc: application/vnd.fsc.weblaunch
|
||||
vcs: text/x-vcalendar
|
||||
hvs: application/vnd.yamaha.hv-script
|
||||
seml: application/vnd.sealed.eml
|
||||
lzh: application/octet-stream
|
||||
movie: video/x-sgi-movie
|
||||
wav: audio/x-wav
|
||||
tbz2: application/x-gtar
|
||||
plt: application/vnd.hp-HPGL
|
||||
3gpp: video/3gpp
|
||||
eol: audio/vnd.digital-winds
|
||||
vsw: application/vnd.visio
|
||||
rtf: text/rtf
|
||||
rgb: image/x-rgb
|
||||
midi: audio/x-midi
|
||||
sit: application/x-stuffit
|
||||
mov: video/quicktime
|
||||
kfo: application/vnd.kde.kformula
|
||||
rdf: application/rdf+xml
|
||||
wpd: application/vnd.wordperfect
|
||||
hbc: application/vnd.hbci
|
||||
ogg: application/ogg
|
||||
dwf: x-drawing/dwf
|
||||
pbm: image/x-portable-bitmap
|
||||
cpp: text/plain
|
||||
smp3: audio/vnd.sealedmedia.softseal.mpeg
|
||||
html: text/html
|
||||
igs: model/iges
|
||||
dwg: image/vnd.dwg
|
||||
see: application/vnd.seemail
|
||||
ram: audio/x-pn-realaudio
|
||||
jad: text/vnd.sun.j2me.app-descriptor
|
||||
iges: model/iges
|
||||
pot: application/powerpoint
|
||||
exe: application/octet-stream
|
||||
siv: application/sieve
|
||||
wml: text/vnd.wap.wml
|
||||
hlp: text/plain
|
||||
pkd: application/vnd.hbci
|
||||
ice: x-conference/x-cooltalk
|
||||
ustar: application/x-ustar
|
||||
vis: application/vnd.visionary
|
||||
pkipath: application/pkix-pkipath
|
||||
ecelp4800: audio/vnd.nuera.ecelp4800
|
||||
tgz: application/x-gtar
|
||||
roff: text/troff
|
||||
ltx: application/x-latex
|
||||
nim: video/vnd.nokia.interleaved-multimedia
|
||||
qcp: audio/QCELP
|
||||
ai: application/postscript
|
||||
sppt: application/vnd.sealed.ppt
|
||||
igx: application/vnd.micrografx.igx
|
||||
tcl: application/x-tcl
|
||||
viv: video/vnd.vivo
|
||||
css: text/css
|
||||
wpl: application/vnd.ms-wpl
|
||||
ami: application/vnd.amiga.ami
|
||||
l16: audio/L16
|
||||
vivo: video/vnd.vivo
|
||||
dat: text/plain
|
||||
vrml: x-world/x-vrml
|
||||
pqa: application/vnd.palm
|
||||
request: application/vnd.nervana
|
||||
oprc: application/vnd.palm
|
||||
vbk: audio/vnd.nortel.vbk
|
||||
pki: application/pkixcmp
|
||||
ras: image/x-cmu-raster
|
||||
asc: text/plain
|
||||
kom: application/vnd.hbci
|
||||
jpeg: image/jpeg
|
||||
sem: application/vnd.sealed.eml
|
||||
chrt: application/vnd.kde.kchart
|
||||
tif: image/tiff
|
||||
cil: application/vnd.ms-artgalry
|
||||
xwd: image/x-xwindowdump
|
||||
dgn: image/x-vnd.dgn
|
||||
mxu: video/vnd.mpegurl
|
||||
csv: text/comma-separated-values
|
||||
kon: application/vnd.kde.kontour
|
||||
png: image/png
|
||||
bkm: application/vnd.nervana
|
||||
sxl: application/vnd.sealed.xls
|
||||
xfdf: application/vnd.adobe.xfdf
|
||||
snd: audio/basic
|
||||
dl: video/dl
|
||||
sxls: application/vnd.sealed.xls
|
||||
karbon: application/vnd.kde.karbon
|
||||
ico: image/vnd.microsoft.icon
|
||||
sus: application/vnd.sus-calendar
|
||||
pdb: x-chemical/x-pdb
|
||||
wif: application/watcherinfo+xml
|
||||
ser: application/x-java-serialized-object
|
||||
xmt_txt: model/vnd.parasolid.transmit.text
|
||||
upa: application/vnd.hbci
|
||||
pnm: image/x-portable-anymap
|
||||
jar: application/x-java-archive
|
||||
qt: video/quicktime
|
||||
tsv: text/tab-separated-values
|
||||
rtx: text/richtext
|
||||
mdi: image/vnd.ms-modi
|
||||
rcprofile: application/vnd.ipunplugged.rcprofile
|
||||
gl: video/gl
|
||||
me: application/x-troff-me
|
||||
man: application/x-troff-man
|
||||
tr: text/troff
|
||||
amr: audio/AMR
|
||||
wp5: application/wordperfect5.1
|
||||
pdf: application/pdf
|
||||
pgb: image/vnd.globalgraphics.pgb
|
||||
au: audio/basic
|
||||
avi: video/x-msvideo
|
||||
qxb: application/vnd.Quark.QuarkXPress
|
||||
wp: application/wordperfect5.1
|
||||
wmlsc: application/vnd.wap.wmlscriptc
|
||||
wbxml: application/vnd.wap.wbxml
|
||||
s1a: application/vnd.sealedmedia.softseal.pdf
|
||||
saf: application/vnd.yamaha.smaf-audio
|
||||
gtar: application/x-gtar
|
||||
Z: application/x-compressed
|
||||
crl: application/pkix-crl
|
||||
pti: application/vnd.pvi.ptid1
|
||||
rdz: application/vnd.data-vision.rdz
|
||||
aif: audio/x-aiff
|
||||
flo: application/vnd.micrografx.flo
|
||||
qxd: application/vnd.Quark.QuarkXPress
|
||||
rpm: audio/x-pn-realaudio-plugin
|
||||
djv: image/vnd.djvu
|
||||
jpe: image/jpeg
|
||||
kne: application/vnd.Kinar
|
||||
lvp: audio/vnd.lucent.voice
|
||||
stml: application/vnd.sealedmedia.softseal.html
|
||||
p7c: application/pkcs7-mime
|
||||
dms: application/octet-stream
|
||||
s1e: application/vnd.sealed.xls
|
||||
sdf: application/vnd.Kinar
|
||||
sc: application/vnd.ibm.secure-container
|
||||
jnlp: application/x-java-jnlp-file
|
||||
dvi: application/x-dvi
|
||||
smov: video/vnd.sealedmedia.softseal.mov
|
||||
jisp: application/vnd.jisp
|
||||
aifc: audio/x-aiff
|
||||
latex: application/x-latex
|
||||
cc: text/plain
|
||||
s1g: image/vnd.sealedmedia.softseal.gif
|
||||
wv: application/vnd.wv.csp+wbxml
|
||||
mseq: application/vnd.mseq
|
||||
jpg: image/jpeg
|
||||
mmf: application/vnd.smaf
|
||||
xmt_bin: model/vnd.parasolid.transmit.binary
|
||||
s1h: application/vnd.sealedmedia.softseal.html
|
||||
mpc: application/vnd.mophun.certificate
|
||||
hdf: application/x-hdf
|
||||
stk: application/hyperstudio
|
||||
txd: application/vnd.genomatix.tuxedo
|
||||
ent: application/vnd.nervana
|
||||
xml: text/xml
|
||||
aiff: audio/x-aiff
|
||||
sh: application/x-sh
|
||||
mpe: video/mpeg
|
||||
s1j: image/vnd.sealedmedia.softseal.jpg
|
||||
psid: audio/prs.sid
|
||||
mpga: audio/mpeg
|
||||
pgm: image/x-portable-graymap
|
||||
si: text/vnd.wap.si
|
||||
stm: application/vnd.sealedmedia.softseal.html
|
||||
lbd: application/vnd.llamagraphics.life-balance.desktop
|
||||
flw: application/vnd.kde.kivio
|
||||
mpg: video/mpeg
|
||||
c: text/plain
|
||||
sgi: image/vnd.sealedmedia.softseal.gif
|
||||
zip: application/zip
|
||||
ecelp7470: audio/vnd.nuera.ecelp7470
|
||||
lbe: application/vnd.llamagraphics.life-balance.exchange+xml
|
||||
qxl: application/vnd.Quark.QuarkXPress
|
||||
p10: application/pkcs10
|
||||
bpd: application/vnd.hbci
|
||||
ief: image/ief
|
||||
gz: application/x-gzip
|
||||
doc: application/word
|
||||
efif: application/vnd.picsel
|
||||
jpm: image/jpm
|
||||
hpgl: application/vnd.hp-HPGL
|
||||
s1m: audio/vnd.sealedmedia.softseal.mpeg
|
||||
xhtml: application/xhtml+xml
|
||||
xpm: image/x-xpixmap
|
||||
ms: application/x-troff-ms
|
||||
bcpio: application/x-bcpio
|
||||
sl: text/vnd.wap.sl
|
||||
wrl: x-world/x-vrml
|
||||
s1n: image/vnd.sealed.png
|
||||
irm: application/vnd.ibm.rights-management
|
||||
pgp: application/octet-stream
|
||||
entity: application/vnd.nervana
|
||||
mcd: application/vnd.mcd
|
||||
ecelp9600: audio/vnd.nuera.ecelp9600
|
||||
kwd: application/vnd.kde.kword
|
||||
gif: image/gif
|
||||
sdo: application/vnd.sealed.doc
|
||||
cer: application/pkix-cert
|
||||
m4u: video/vnd.mpegurl
|
||||
rst: text/prs.fallenstein.rst
|
||||
htm: text/html
|
||||
mxmf: audio/vnd.nokia.mobile-xmf
|
||||
psb: application/vnd.3gpp.pic-bw-small
|
||||
knp: application/vnd.Kinar
|
||||
cab: application/vnd.ms-cab-compressed
|
||||
mj2: video/MJ2
|
||||
sgm: text/sgml
|
||||
wbmp: image/vnd.wap.wbmp
|
||||
p7m: application/pkcs7-mime
|
||||
spng: image/vnd.sealed.png
|
||||
lha: application/octet-stream
|
||||
s1p: application/vnd.sealed.ppt
|
||||
texi: application/x-texinfo
|
||||
s1q: video/vnd.sealedmedia.softseal.mov
|
||||
troff: text/troff
|
||||
h: text/plain
|
||||
shtml: text/html
|
||||
msh: model/mesh
|
||||
irp: application/vnd.irepository.package+xml
|
||||
rct: application/prs.nprend
|
||||
smht: application/vnd.sealed.mht
|
||||
s11: video/vnd.sealed.mpeg1
|
||||
htke: application/vnd.kenameaapp
|
||||
ps: application/postscript
|
||||
mpm: application/vnd.blueice.multipass
|
||||
dfac: application/vnd.dreamfactory
|
||||
pvb: application/vnd.3gpp.pic-bw-var
|
||||
lrm: application/vnd.ms-lrm
|
||||
smh: application/vnd.sealed.mht
|
||||
mpn: application/vnd.mophun.application
|
||||
spd: application/vnd.sealedmedia.softseal.pdf
|
||||
tiff: image/tiff
|
||||
jp2: image/jp2
|
||||
rpss: application/vnd.nokia.radio-presets
|
||||
qxt: application/vnd.Quark.QuarkXPress
|
||||
wmlc: application/vnd.wap.wmlc
|
||||
rpst: application/vnd.nokia.radio-preset
|
||||
etx: text/x-setext
|
||||
bmp: image/bmp
|
||||
s14: video/vnd.sealed.mpeg4
|
||||
\"123\": application/vnd.lotus-1-2-3
|
||||
mpp: application/vnd.ms-project
|
||||
spf: application/vnd.yamaha.smaf-phrase
|
||||
kar: audio/x-midi
|
||||
mid: audio/x-midi
|
||||
3gp: video/3gpp
|
||||
3g2: video/3gpp2
|
||||
hqx: application/mac-binhex40
|
||||
p7s: application/pkcs7-signature
|
||||
ppm: image/x-portable-pixmap
|
||||
pspimage: image/x-paintshoppro
|
||||
cdf: application/netcdf
|
||||
texinfo: application/x-texinfo
|
||||
sjp: image/vnd.sealedmedia.softseal.jpg
|
||||
wbs: application/vnd.criticaltools.wbs+xml
|
||||
emm: application/vnd.ibm.electronic-media
|
||||
s1w: application/vnd.sealed.doc
|
||||
ra: audio/x-realaudio
|
||||
jpx: image/jpx
|
||||
evc: audio/EVRC
|
||||
mif: application/x-mif
|
||||
qwd: application/vnd.Quark.QuarkXPress
|
||||
mp2: video/mpeg
|
||||
spdf: application/vnd.sealedmedia.softseal.pdf
|
||||
tbz: application/x-gtar
|
||||
txt: text/plain
|
||||
x_b: model/vnd.parasolid.transmit.binary
|
||||
mp3: audio/mpeg
|
||||
class: application/x-java-vm
|
||||
smo: video/vnd.sealedmedia.softseal.mov
|
||||
mp4: video/vnd.objectvideo
|
||||
m4v: video/x-m4v
|
||||
htx: text/html
|
||||
hbci: application/vnd.hbci
|
||||
tex: application/x-tex
|
||||
vsc: application/vnd.vidsoft.vidconference
|
||||
wqd: application/vnd.wqd
|
||||
mfm: application/vnd.mfmp
|
||||
sgml: text/sgml
|
||||
smp: audio/vnd.sealedmedia.softseal.mpeg
|
||||
curl: application/vnd.curl
|
||||
cw: application/prs.cww
|
||||
djvu: image/vnd.djvu
|
||||
tga: image/targa
|
||||
vsd: application/vnd.visio
|
||||
t: text/troff
|
||||
wtb: application/vnd.webturbo
|
||||
spn: image/vnd.sealed.png
|
||||
plb: application/vnd.3gpp.pic-bw-large
|
||||
pps: application/powerpoint
|
||||
yaml: text/x-yaml
|
||||
psp: image/x-paintshoppro
|
||||
mjp2: video/MJ2
|
||||
sms: application/vnd.3gpp.sms
|
||||
hvd: application/vnd.yamaha.hv-dic
|
||||
acutc: application/vnd.acucorp
|
||||
ppt: application/powerpoint
|
||||
les: application/vnd.hhe.lesson-player
|
||||
vcf: text/x-vcard
|
||||
sjpg: image/vnd.sealedmedia.softseal.jpg
|
||||
kwt: application/vnd.kde.kword
|
||||
sic: application/vnd.wap.sic
|
||||
spp: application/vnd.sealed.ppt
|
||||
cmc: application/vnd.cosmocaller
|
||||
dot: application/word
|
||||
sv4cpio: application/x-sv4cpio
|
||||
cpio: application/x-cpio
|
||||
sswf: video/vnd.sealed.swf
|
||||
silo: model/mesh
|
||||
sid: audio/prs.sid
|
||||
yml: text/x-yaml
|
||||
smv: audio/SMV
|
||||
eps: application/postscript
|
||||
ptid: application/vnd.pvi.ptid1
|
||||
wks: application/vnd.lotus-1-2-3
|
||||
z: application/x-compressed
|
||||
hpp: text/plain
|
||||
htmlx: text/html
|
||||
ani: application/octet-stream
|
||||
sig: application/pgp-signature
|
||||
slc: application/vnd.wap.slc
|
||||
rm: audio/x-pn-realaudio
|
||||
smpg: video/vnd.sealed.mpeg4
|
||||
wmls: text/vnd.wap.wmlscript
|
||||
bin: application/x-mac
|
||||
mesh: model/mesh
|
||||
atc: application/vnd.acucorp
|
||||
pfr: application/font-tdpfr
|
||||
plj: audio/vnd.everad.plj
|
||||
rnd: application/prs.nprend
|
||||
xls: application/excel
|
||||
tar: application/x-tar
|
||||
mp3g: video/mpeg
|
||||
sgif: image/vnd.sealedmedia.softseal.gif
|
||||
oda: application/oda
|
||||
sdoc: application/vnd.sealed.doc
|
||||
kia: application/vnd.kidspiration
|
||||
prc: application/vnd.palm
|
||||
req: application/vnd.nervana
|
||||
xyz: x-chemical/x-xyz
|
||||
soc: application/sgml-open-catalog
|
||||
xlt: application/excel
|
||||
awb: audio/AMR-WB
|
||||
susp: application/vnd.sus-calendar
|
||||
xbm: image/x-xbm
|
||||
ccc: text/vnd.net2phone.commcenter.command
|
||||
hh: text/plain
|
||||
qwt: application/vnd.Quark.QuarkXPress
|
||||
shar: application/x-shar
|
||||
ssw: video/vnd.sealed.swf
|
||||
xul: application/vnd.mozilla.xul+xml
|
||||
kcm: application/vnd.nervana
|
||||
kpr: application/vnd.kde.kpresenter
|
||||
cdy: application/vnd.cinderella
|
||||
nc: application/netcdf
|
||||
src: application/x-wais-source
|
||||
sv4crc: application/x-sv4crc
|
||||
dtd: text/xml
|
||||
hvp: application/vnd.yamaha.hv-voice
|
||||
cww: application/prs.cww
|
||||
vss: application/vnd.visio
|
||||
rb: application/x-ruby
|
||||
log: text/plain
|
||||
swf: application/x-shockwave-flash
|
||||
flv: video/x-flv
|
||||
asf: video/x-ms-asf
|
||||
asx: video/x-ms-asf
|
||||
wma: audio/x-ms-wma
|
||||
wax: audio/x-ms-wax
|
||||
wmv: audio/x-ms-wmv
|
||||
wvx: video/x-ms-wvx
|
||||
wm: video/x-ms-wm
|
||||
wmx: video/x-ms-wmx
|
||||
wmz: application/x-ms-wmz
|
||||
wmd: application/x-ms-wmd
|
||||
m3u: audio/x-mpequrl
|
||||
rdp: application/rdp
|
||||
pcap: application/vnd.tcpdump.pcap
|
||||
torrent: application/x-bittorrent
|
||||
xlb: application/excel
|
||||
cue: application/x-cue
|
||||
@@ -4,7 +4,7 @@ class MigrateCredData < ActiveRecord::Migration
|
||||
begin # Wrap the whole thing in a giant rescue.
|
||||
skipped_notes = []
|
||||
new_creds = []
|
||||
Msf::DBManager::Note.find(:all).each do |note|
|
||||
Mdm::Note.find(:all).each do |note|
|
||||
next unless note.ntype[/^auth\.(.*)/]
|
||||
service_name = $1
|
||||
if !service_name
|
||||
@@ -46,7 +46,7 @@ class MigrateCredData < ActiveRecord::Migration
|
||||
if candidate_services.size == 1
|
||||
svc_id = candidate_services.first.id
|
||||
elsif candidate_services.empty?
|
||||
Msf::DBManager::Service.new do |svc|
|
||||
Mdm::Service.new do |svc|
|
||||
svc.host_id = note.host.id
|
||||
svc.port = default_port
|
||||
svc.proto = 'tcp'
|
||||
@@ -115,7 +115,7 @@ class MigrateCredData < ActiveRecord::Migration
|
||||
|
||||
say "Migrating #{new_creds.size} credentials."
|
||||
new_creds.uniq.each do |note|
|
||||
Msf::DBManager::Cred.new do |cred|
|
||||
Mdm::Cred.new do |cred|
|
||||
cred.service_id = note[0]
|
||||
cred.user = note[2]
|
||||
cred.pass = note[3]
|
||||
@@ -126,7 +126,7 @@ class MigrateCredData < ActiveRecord::Migration
|
||||
|
||||
say "Migrating #{skipped_notes.size} notes."
|
||||
skipped_notes.uniq.each do |note|
|
||||
Msf::DBManager::Note.new do |new_note|
|
||||
Mdm::Note.new do |new_note|
|
||||
new_note.host_id = note.host_id
|
||||
new_note.ntype = "migrated_auth"
|
||||
new_note.data = note.data.merge(:migrated_auth_type => note.ntype)
|
||||
@@ -135,7 +135,7 @@ class MigrateCredData < ActiveRecord::Migration
|
||||
end
|
||||
|
||||
say "Deleting migrated auth notes."
|
||||
Msf::DBManager::Note.find(:all).each do |note|
|
||||
Mdm::Note.find(:all).each do |note|
|
||||
next unless note.ntype[/^auth\.(.*)/]
|
||||
note.delete
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ class RequireAdminFlag < ActiveRecord::Migration
|
||||
# Make the admin flag required.
|
||||
def self.up
|
||||
# update any existing records
|
||||
Msf::DBManager::User.update_all({:admin => true}, {:admin => nil})
|
||||
Mdm::User.update_all({:admin => true}, {:admin => nil})
|
||||
|
||||
change_column :users, :admin, :boolean, :null => false, :default => true
|
||||
end
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
class InetColumns < ActiveRecord::Migration
|
||||
|
||||
def self.up
|
||||
change_column :hosts, :address, 'INET using address::INET'
|
||||
remove_column :hosts, :address6
|
||||
end
|
||||
def self.up
|
||||
change_column :hosts, :address, 'INET using address::INET'
|
||||
remove_column :hosts, :address6
|
||||
end
|
||||
|
||||
def self.down
|
||||
change_column :hosts, :address, :text
|
||||
add_column :hosts, :address6, :text
|
||||
end
|
||||
def self.down
|
||||
change_column :hosts, :address, :text
|
||||
add_column :hosts, :address6, :text
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
# TODO: Review explicit loads to see if they will automatically be handled by the initilizer.
|
||||
def self.load_all!
|
||||
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
|
||||
end
|
||||
|
||||
autoload :VERSION, 'active_record/version'
|
||||
|
||||
autoload :ActiveRecordError, 'active_record/base'
|
||||
autoload :ConnectionNotEstablished, 'active_record/base'
|
||||
|
||||
autoload :Aggregations, 'active_record/aggregations'
|
||||
autoload :AssociationPreload, 'active_record/association_preload'
|
||||
autoload :Associations, 'active_record/associations'
|
||||
autoload :AttributeMethods, 'active_record/attribute_methods'
|
||||
autoload :AutosaveAssociation, 'active_record/autosave_association'
|
||||
autoload :Base, 'active_record/base'
|
||||
autoload :Batches, 'active_record/batches'
|
||||
autoload :Calculations, 'active_record/calculations'
|
||||
autoload :Callbacks, 'active_record/callbacks'
|
||||
autoload :Dirty, 'active_record/dirty'
|
||||
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
|
||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||
autoload :Migration, 'active_record/migration'
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :NamedScope, 'active_record/named_scope'
|
||||
autoload :NestedAttributes, 'active_record/nested_attributes'
|
||||
autoload :Observing, 'active_record/observer'
|
||||
autoload :QueryCache, 'active_record/query_cache'
|
||||
autoload :Reflection, 'active_record/reflection'
|
||||
autoload :Schema, 'active_record/schema'
|
||||
autoload :SchemaDumper, 'active_record/schema_dumper'
|
||||
autoload :Serialization, 'active_record/serialization'
|
||||
autoload :SessionStore, 'active_record/session_store'
|
||||
autoload :TestCase, 'active_record/test_case'
|
||||
autoload :Timestamp, 'active_record/timestamp'
|
||||
autoload :Transactions, 'active_record/transactions'
|
||||
autoload :Validations, 'active_record/validations'
|
||||
|
||||
module Locking
|
||||
autoload :Optimistic, 'active_record/locking/optimistic'
|
||||
autoload :Pessimistic, 'active_record/locking/pessimistic'
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
autoload :AbstractAdapter, 'active_record/connection_adapters/abstract_adapter'
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_record/i18n_interpolation_deprecation'
|
||||
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
||||
@@ -1,261 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Aggregations # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
def clear_aggregation_cache #:nodoc:
|
||||
self.class.reflect_on_all_aggregations.to_a.each do |assoc|
|
||||
instance_variable_set "@#{assoc.name}", nil
|
||||
end unless self.new_record?
|
||||
end
|
||||
|
||||
# Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
|
||||
# as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
|
||||
# composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
|
||||
# attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
|
||||
# and how it can be turned back into attributes (when the entity is saved to the database). Example:
|
||||
#
|
||||
# class Customer < ActiveRecord::Base
|
||||
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
|
||||
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
# end
|
||||
#
|
||||
# The customer class now has the following methods to manipulate the value objects:
|
||||
# * <tt>Customer#balance, Customer#balance=(money)</tt>
|
||||
# * <tt>Customer#address, Customer#address=(address)</tt>
|
||||
#
|
||||
# These methods will operate with value objects like the ones described below:
|
||||
#
|
||||
# class Money
|
||||
# include Comparable
|
||||
# attr_reader :amount, :currency
|
||||
# EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
|
||||
#
|
||||
# def initialize(amount, currency = "USD")
|
||||
# @amount, @currency = amount, currency
|
||||
# end
|
||||
#
|
||||
# def exchange_to(other_currency)
|
||||
# exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
|
||||
# Money.new(exchanged_amount, other_currency)
|
||||
# end
|
||||
#
|
||||
# def ==(other_money)
|
||||
# amount == other_money.amount && currency == other_money.currency
|
||||
# end
|
||||
#
|
||||
# def <=>(other_money)
|
||||
# if currency == other_money.currency
|
||||
# amount <=> amount
|
||||
# else
|
||||
# amount <=> other_money.exchange_to(currency).amount
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Address
|
||||
# attr_reader :street, :city
|
||||
# def initialize(street, city)
|
||||
# @street, @city = street, city
|
||||
# end
|
||||
#
|
||||
# def close_to?(other_address)
|
||||
# city == other_address.city
|
||||
# end
|
||||
#
|
||||
# def ==(other_address)
|
||||
# city == other_address.city && street == other_address.street
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
|
||||
# composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
|
||||
# +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
|
||||
#
|
||||
# customer.balance = Money.new(20) # sets the Money value object and the attribute
|
||||
# customer.balance # => Money value object
|
||||
# customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
|
||||
# customer.balance > Money.new(10) # => true
|
||||
# customer.balance == Money.new(20) # => true
|
||||
# customer.balance < Money.new(5) # => false
|
||||
#
|
||||
# Value objects can also be composed of multiple attributes, such as the case of Address. The order of the mappings will
|
||||
# determine the order of the parameters. Example:
|
||||
#
|
||||
# customer.address_street = "Hyancintvej"
|
||||
# customer.address_city = "Copenhagen"
|
||||
# customer.address # => Address.new("Hyancintvej", "Copenhagen")
|
||||
# customer.address = Address.new("May Street", "Chicago")
|
||||
# customer.address_street # => "May Street"
|
||||
# customer.address_city # => "Chicago"
|
||||
#
|
||||
# == Writing value objects
|
||||
#
|
||||
# Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
|
||||
# $5. Two Money objects both representing $5 should be equal (through methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking
|
||||
# makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as Customer can
|
||||
# easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
|
||||
# relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
|
||||
#
|
||||
# It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
|
||||
# creation. Create a new Money object with the new value instead. This is exemplified by the Money#exchange_to method that
|
||||
# returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
|
||||
# changed through means other than the writer method.
|
||||
#
|
||||
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
|
||||
# change it afterwards will result in a ActiveSupport::FrozenObjectError.
|
||||
#
|
||||
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
|
||||
# immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
|
||||
#
|
||||
# == Custom constructors and converters
|
||||
#
|
||||
# By default value objects are initialized by calling the <tt>new</tt> constructor of the value class passing each of the
|
||||
# mapped attributes, in the order specified by the <tt>:mapping</tt> option, as arguments. If the value class doesn't support
|
||||
# this convention then +composed_of+ allows a custom constructor to be specified.
|
||||
#
|
||||
# When a new value is assigned to the value object the default assumption is that the new value is an instance of the value
|
||||
# class. Specifying a custom converter allows the new value to be automatically converted to an instance of value class if
|
||||
# necessary.
|
||||
#
|
||||
# For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be aggregated using the
|
||||
# NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor for the value class is called +create+ and it
|
||||
# expects a CIDR address string as a parameter. New values can be assigned to the value object using either another
|
||||
# NetAddr::CIDR object, a string or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to
|
||||
# meet these requirements:
|
||||
#
|
||||
# class NetworkResource < ActiveRecord::Base
|
||||
# composed_of :cidr,
|
||||
# :class_name => 'NetAddr::CIDR',
|
||||
# :mapping => [ %w(network_address network), %w(cidr_range bits) ],
|
||||
# :allow_nil => true,
|
||||
# :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
|
||||
# :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
|
||||
# end
|
||||
#
|
||||
# # This calls the :constructor
|
||||
# network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
|
||||
#
|
||||
# # These assignments will both use the :converter
|
||||
# network_resource.cidr = [ '192.168.2.1', 8 ]
|
||||
# network_resource.cidr = '192.168.0.1/24'
|
||||
#
|
||||
# # This assignment won't use the :converter as the value is already an instance of the value class
|
||||
# network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
|
||||
#
|
||||
# # Saving and then reloading will use the :constructor on reload
|
||||
# network_resource.save
|
||||
# network_resource.reload
|
||||
#
|
||||
# == Finding records by a value object
|
||||
#
|
||||
# Once a +composed_of+ relationship is specified for a model, records can be loaded from the database by specifying an instance
|
||||
# of the value object in the conditions hash. The following example finds all customers with +balance_amount+ equal to 20 and
|
||||
# +balance_currency+ equal to "USD":
|
||||
#
|
||||
# Customer.find(:all, :conditions => {:balance => Money.new(20, "USD")})
|
||||
#
|
||||
module ClassMethods
|
||||
# Adds reader and writer methods for manipulating a value object:
|
||||
# <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
|
||||
#
|
||||
# Options are:
|
||||
# * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name can't be inferred
|
||||
# from the part id. So <tt>composed_of :address</tt> will by default be linked to the Address class, but
|
||||
# if the real class name is CompanyAddress, you'll have to specify it with this option.
|
||||
# * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value object. Each mapping
|
||||
# is represented as an array where the first item is the name of the entity attribute and the second item is the
|
||||
# name the attribute in the value object. The order in which mappings are defined determine the order in which
|
||||
# attributes are sent to the value class constructor.
|
||||
# * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
|
||||
# attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all mapped attributes.
|
||||
# This defaults to +false+.
|
||||
# * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that is called to
|
||||
# initialize the value object. The constructor is passed all of the mapped attributes, in the order that they
|
||||
# are defined in the <tt>:mapping option</tt>, as arguments and uses them to instantiate a <tt>:class_name</tt> object.
|
||||
# The default is <tt>:new</tt>.
|
||||
# * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt> or a Proc that is
|
||||
# called when a new value is assigned to the value object. The converter is passed the single value that is used
|
||||
# in the assignment and is only called if the new value is not an instance of <tt>:class_name</tt>.
|
||||
#
|
||||
# Option examples:
|
||||
# composed_of :temperature, :mapping => %w(reading celsius)
|
||||
# composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
|
||||
# composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
|
||||
# composed_of :gps_location
|
||||
# composed_of :gps_location, :allow_nil => true
|
||||
# composed_of :ip_address,
|
||||
# :class_name => 'IPAddr',
|
||||
# :mapping => %w(ip to_i),
|
||||
# :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
|
||||
# :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
|
||||
#
|
||||
def composed_of(part_id, options = {}, &block)
|
||||
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
|
||||
|
||||
name = part_id.id2name
|
||||
class_name = options[:class_name] || name.camelize
|
||||
mapping = options[:mapping] || [ name, name ]
|
||||
mapping = [ mapping ] unless mapping.first.is_a?(Array)
|
||||
allow_nil = options[:allow_nil] || false
|
||||
constructor = options[:constructor] || :new
|
||||
converter = options[:converter] || block
|
||||
|
||||
ActiveSupport::Deprecation.warn('The conversion block has been deprecated, use the :converter option instead.', caller) if block_given?
|
||||
|
||||
reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
|
||||
create_reflection(:composed_of, part_id, options, self)
|
||||
end
|
||||
|
||||
private
|
||||
def reader_method(name, class_name, mapping, allow_nil, constructor)
|
||||
module_eval do
|
||||
define_method(name) do |*args|
|
||||
force_reload = args.first || false
|
||||
if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
|
||||
attrs = mapping.collect {|pair| read_attribute(pair.first)}
|
||||
object = case constructor
|
||||
when Symbol
|
||||
class_name.constantize.send(constructor, *attrs)
|
||||
when Proc, Method
|
||||
constructor.call(*attrs)
|
||||
else
|
||||
raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.'
|
||||
end
|
||||
instance_variable_set("@#{name}", object)
|
||||
end
|
||||
instance_variable_get("@#{name}")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def writer_method(name, class_name, mapping, allow_nil, converter)
|
||||
module_eval do
|
||||
define_method("#{name}=") do |part|
|
||||
if part.nil? && allow_nil
|
||||
mapping.each { |pair| self[pair.first] = nil }
|
||||
instance_variable_set("@#{name}", nil)
|
||||
else
|
||||
unless part.is_a?(class_name.constantize) || converter.nil?
|
||||
part = case converter
|
||||
when Symbol
|
||||
class_name.constantize.send(converter, part)
|
||||
when Proc, Method
|
||||
converter.call(part)
|
||||
else
|
||||
raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
|
||||
end
|
||||
end
|
||||
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
||||
instance_variable_set("@#{name}", part.freeze)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,389 +0,0 @@
|
||||
module ActiveRecord
|
||||
# See ActiveRecord::AssociationPreload::ClassMethods for documentation.
|
||||
module AssociationPreload #:nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Implements the details of eager loading of ActiveRecord associations.
|
||||
# Application developers should not use this module directly.
|
||||
#
|
||||
# ActiveRecord::Base is extended with this module. The source code in
|
||||
# ActiveRecord::Base references methods defined in this module.
|
||||
#
|
||||
# Note that 'eager loading' and 'preloading' are actually the same thing.
|
||||
# However, there are two different eager loading strategies.
|
||||
#
|
||||
# The first one is by using table joins. This was only strategy available
|
||||
# prior to Rails 2.1. Suppose that you have an Author model with columns
|
||||
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
|
||||
# this strategy, ActiveRecord would try to retrieve all data for an author
|
||||
# and all of its books via a single query:
|
||||
#
|
||||
# SELECT * FROM authors
|
||||
# LEFT OUTER JOIN books ON authors.id = books.id
|
||||
# WHERE authors.name = 'Ken Akamatsu'
|
||||
#
|
||||
# However, this could result in many rows that contain redundant data. After
|
||||
# having received the first row, we already have enough data to instantiate
|
||||
# the Author object. In all subsequent rows, only the data for the joined
|
||||
# 'books' table is useful; the joined 'authors' data is just redundant, and
|
||||
# processing this redundant data takes memory and CPU time. The problem
|
||||
# quickly becomes worse and worse as the level of eager loading increases
|
||||
# (i.e. if ActiveRecord is to eager load the associations' assocations as
|
||||
# well).
|
||||
#
|
||||
# The second strategy is to use multiple database queries, one for each
|
||||
# level of association. Since Rails 2.1, this is the default strategy. In
|
||||
# situations where a table join is necessary (e.g. when the +:conditions+
|
||||
# option references an association's column), it will fallback to the table
|
||||
# join strategy.
|
||||
#
|
||||
# See also ActiveRecord::Associations::ClassMethods, which explains eager
|
||||
# loading in a more high-level (application developer-friendly) manner.
|
||||
module ClassMethods
|
||||
protected
|
||||
|
||||
# Eager loads the named associations for the given ActiveRecord record(s).
|
||||
#
|
||||
# In this description, 'association name' shall refer to the name passed
|
||||
# to an association creation method. For example, a model that specifies
|
||||
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
|
||||
# names +:author+ and +:buyers+.
|
||||
#
|
||||
# == Parameters
|
||||
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
|
||||
# i.e. +records+ itself may also contain arrays of records. In any case,
|
||||
# +preload_associations+ will preload the associations all records by
|
||||
# flattening +records+.
|
||||
#
|
||||
# +associations+ specifies one or more associations that you want to
|
||||
# preload. It may be:
|
||||
# - a Symbol or a String which specifies a single association name. For
|
||||
# example, specifiying +:books+ allows this method to preload all books
|
||||
# for an Author.
|
||||
# - an Array which specifies multiple association names. This array
|
||||
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
|
||||
# allows this method to preload an author's avatar as well as all of his
|
||||
# books.
|
||||
# - a Hash which specifies multiple association names, as well as
|
||||
# association names for the to-be-preloaded association objects. For
|
||||
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
|
||||
# book's author, as well as that author's avatar.
|
||||
#
|
||||
# +:associations+ has the same format as the +:include+ option for
|
||||
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
|
||||
#
|
||||
# :books
|
||||
# [ :books, :author ]
|
||||
# { :author => :avatar }
|
||||
# [ :books, { :author => :avatar } ]
|
||||
#
|
||||
# +preload_options+ contains options that will be passed to ActiveRecord#find
|
||||
# (which is called under the hood for preloading records). But it is passed
|
||||
# only one level deep in the +associations+ argument, i.e. it's not passed
|
||||
# to the child associations when +associations+ is a Hash.
|
||||
def preload_associations(records, associations, preload_options={})
|
||||
records = [records].flatten.compact.uniq
|
||||
return if records.empty?
|
||||
case associations
|
||||
when Array then associations.each {|association| preload_associations(records, association, preload_options)}
|
||||
when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
|
||||
when Hash then
|
||||
associations.each do |parent, child|
|
||||
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
|
||||
preload_associations(records, parent, preload_options)
|
||||
reflection = reflections[parent]
|
||||
parents = records.map {|record| record.send(reflection.name)}.flatten.compact
|
||||
unless parents.empty?
|
||||
parents.first.class.preload_associations(parents, child)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Preloads a specific named association for the given records. This is
|
||||
# called by +preload_associations+ as its base case.
|
||||
def preload_one_association(records, association, preload_options={})
|
||||
class_to_reflection = {}
|
||||
# Not all records have the same class, so group then preload
|
||||
# group on the reflection itself so that if various subclass share the same association then we do not split them
|
||||
# unnecessarily
|
||||
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
|
||||
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
|
||||
|
||||
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
|
||||
# the following could call 'preload_belongs_to_association',
|
||||
# 'preload_has_many_association', etc.
|
||||
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
|
||||
end
|
||||
end
|
||||
|
||||
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
association_proxy = parent_record.send(reflection_name)
|
||||
association_proxy.loaded
|
||||
association_proxy.target.push(*[associated_record].flatten)
|
||||
end
|
||||
end
|
||||
|
||||
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
parent_record.send("set_#{reflection_name}_target", associated_record)
|
||||
end
|
||||
end
|
||||
|
||||
def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
|
||||
associated_records.each do |associated_record|
|
||||
mapped_records = id_to_record_map[associated_record[key].to_s]
|
||||
add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
|
||||
end
|
||||
end
|
||||
|
||||
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
|
||||
seen_keys = {}
|
||||
associated_records.each do |associated_record|
|
||||
#this is a has_one or belongs_to: there should only be one record.
|
||||
#Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
|
||||
# only one row per distinct foo_id' so this where we enforce that
|
||||
next if seen_keys[associated_record[key].to_s]
|
||||
seen_keys[associated_record[key].to_s] = true
|
||||
mapped_records = id_to_record_map[associated_record[key].to_s]
|
||||
mapped_records.each do |mapped_record|
|
||||
mapped_record.send("set_#{reflection_name}_target", associated_record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Given a collection of ActiveRecord objects, constructs a Hash which maps
|
||||
# the objects' IDs to the relevant objects. Returns a 2-tuple
|
||||
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
|
||||
# and +ids+ is an Array of record IDs.
|
||||
def construct_id_map(records, primary_key=nil)
|
||||
id_to_record_map = {}
|
||||
ids = []
|
||||
records.each do |record|
|
||||
primary_key ||= record.class.primary_key
|
||||
ids << record[primary_key]
|
||||
mapped_records = (id_to_record_map[ids.last.to_s] ||= [])
|
||||
mapped_records << record
|
||||
end
|
||||
ids.uniq!
|
||||
return id_to_record_map, ids
|
||||
end
|
||||
|
||||
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
|
||||
table_name = reflection.klass.quoted_table_name
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
options = reflection.options
|
||||
|
||||
conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
associated_records = reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
|
||||
end
|
||||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key])
|
||||
options = reflection.options
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
if options[:through]
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
through_records.first.class.preload_associations(through_records, source)
|
||||
if through_reflection.macro == :belongs_to
|
||||
rev_id_to_record_map, rev_ids = construct_id_map(records, through_primary_key)
|
||||
rev_primary_key = through_reflection.klass.primary_key
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(rev_id_to_record_map[through_record[rev_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
else
|
||||
through_records.each do |through_record|
|
||||
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
|
||||
reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
|
||||
end
|
||||
end
|
||||
|
||||
def preload_has_many_association(records, reflection, preload_options={})
|
||||
return if records.first.send(reflection.name).loaded?
|
||||
options = reflection.options
|
||||
|
||||
primary_key_name = reflection.through_reflection_primary_key_name
|
||||
id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key])
|
||||
records.each {|record| record.send(reflection.name).loaded}
|
||||
|
||||
if options[:through]
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
unless through_records.empty?
|
||||
source = reflection.source_reflection.name
|
||||
through_records.first.class.preload_associations(through_records, source, options)
|
||||
through_records.each do |through_record|
|
||||
through_record_id = through_record[reflection.through_reflection_primary_key].to_s
|
||||
add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options),
|
||||
reflection.primary_key_name)
|
||||
end
|
||||
end
|
||||
|
||||
def preload_through_records(records, reflection, through_association)
|
||||
through_reflection = reflections[through_association]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
|
||||
if reflection.options[:source_type]
|
||||
interface = reflection.source_reflection.options[:foreign_type]
|
||||
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
|
||||
|
||||
records.compact!
|
||||
records.first.class.preload_associations(records, through_association, preload_options)
|
||||
|
||||
# Dont cache the association - we would only be caching a subset
|
||||
through_records = []
|
||||
records.each do |record|
|
||||
proxy = record.send(through_association)
|
||||
|
||||
if proxy.respond_to?(:target)
|
||||
through_records << proxy.target
|
||||
proxy.reset
|
||||
else # this is a has_one :through reflection
|
||||
through_records << proxy if proxy
|
||||
end
|
||||
end
|
||||
through_records.flatten!
|
||||
else
|
||||
records.first.class.preload_associations(records, through_association)
|
||||
through_records = records.map {|record| record.send(through_association)}.flatten
|
||||
end
|
||||
through_records.compact!
|
||||
through_records
|
||||
end
|
||||
|
||||
def preload_belongs_to_association(records, reflection, preload_options={})
|
||||
return if records.first.send("loaded_#{reflection.name}?")
|
||||
options = reflection.options
|
||||
primary_key_name = reflection.primary_key_name
|
||||
|
||||
if options[:polymorphic]
|
||||
polymorph_type = options[:foreign_type]
|
||||
klasses_and_ids = {}
|
||||
|
||||
# Construct a mapping from klass to a list of ids to load and a mapping of those ids back to their parent_records
|
||||
records.each do |record|
|
||||
if klass = record.send(polymorph_type)
|
||||
klass_id = record.send(primary_key_name)
|
||||
if klass_id
|
||||
id_map = klasses_and_ids[klass] ||= {}
|
||||
id_list_for_klass_id = (id_map[klass_id.to_s] ||= [])
|
||||
id_list_for_klass_id << record
|
||||
end
|
||||
end
|
||||
end
|
||||
klasses_and_ids = klasses_and_ids.to_a
|
||||
else
|
||||
id_map = {}
|
||||
records.each do |record|
|
||||
key = record.send(primary_key_name)
|
||||
if key
|
||||
mapped_records = (id_map[key.to_s] ||= [])
|
||||
mapped_records << record
|
||||
end
|
||||
end
|
||||
klasses_and_ids = [[reflection.klass.name, id_map]]
|
||||
end
|
||||
|
||||
klasses_and_ids.each do |klass_and_id|
|
||||
klass_name, id_map = *klass_and_id
|
||||
next if id_map.empty?
|
||||
klass = klass_name.constantize
|
||||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.map do |id|
|
||||
if column_type == :integer
|
||||
id.to_i
|
||||
elsif column_type == :float
|
||||
id.to_f
|
||||
else
|
||||
id
|
||||
end
|
||||
end
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
associated_records = klass.with_exclusive_scope do
|
||||
klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
:joins => options[:joins],
|
||||
:order => options[:order])
|
||||
end
|
||||
set_association_single_records(id_map, reflection.name, associated_records, primary_key)
|
||||
end
|
||||
end
|
||||
|
||||
def find_associated_records(ids, reflection, preload_options)
|
||||
options = reflection.options
|
||||
table_name = reflection.klass.quoted_table_name
|
||||
|
||||
if interface = reflection.options[:as]
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
|
||||
else
|
||||
foreign_key = reflection.primary_key_name
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
|
||||
end
|
||||
|
||||
conditions << append_conditions(reflection, preload_options)
|
||||
|
||||
reflection.klass.with_exclusive_scope do
|
||||
reflection.klass.find(:all,
|
||||
:select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
|
||||
:include => preload_options[:include] || options[:include],
|
||||
:conditions => [conditions, ids],
|
||||
:joins => options[:joins],
|
||||
:group => preload_options[:group] || options[:group],
|
||||
:order => preload_options[:order] || options[:order])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def interpolate_sql_for_preload(sql)
|
||||
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
||||
end
|
||||
|
||||
def append_conditions(reflection, preload_options)
|
||||
sql = ""
|
||||
sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
|
||||
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
|
||||
sql
|
||||
end
|
||||
|
||||
def in_or_equals_for_ids(ids)
|
||||
ids.size > 1 ? "IN (?)" : "= ?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2241 +0,0 @@
|
||||
module ActiveRecord
|
||||
class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
def initialize(owner_class_name, reflection)
|
||||
super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
|
||||
def initialize(owner_class_name, reflection, source_reflection)
|
||||
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
|
||||
def initialize(owner_class_name, reflection, source_reflection)
|
||||
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
through_reflection = reflection.through_reflection
|
||||
source_reflection_names = reflection.source_reflection_names
|
||||
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
|
||||
super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughSourceAssociationMacroError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
through_reflection = reflection.through_reflection
|
||||
source_reflection = reflection.source_reflection
|
||||
super("Invalid source reflection macro :#{source_reflection.macro}#{" :through" if source_reflection.options[:through]} for has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}. Use :source to specify the source reflection.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.")
|
||||
end
|
||||
end
|
||||
HasManyThroughCantAssociateThroughHasManyReflection = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection', 'ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection')
|
||||
|
||||
class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
|
||||
end
|
||||
end
|
||||
|
||||
class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
|
||||
end
|
||||
end
|
||||
|
||||
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
|
||||
end
|
||||
end
|
||||
|
||||
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
|
||||
def initialize(reflection)
|
||||
super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
|
||||
end
|
||||
end
|
||||
|
||||
# See ActiveRecord::Associations::ClassMethods for documentation.
|
||||
module Associations # :nodoc:
|
||||
# These classes will be loaded when associations are created.
|
||||
# So there is no need to eager load them.
|
||||
autoload :AssociationCollection, 'active_record/associations/association_collection'
|
||||
autoload :AssociationProxy, 'active_record/associations/association_proxy'
|
||||
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
|
||||
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
|
||||
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
|
||||
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
|
||||
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
|
||||
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
|
||||
autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association'
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Clears out the association cache
|
||||
def clear_association_cache #:nodoc:
|
||||
self.class.reflect_on_all_associations.to_a.each do |assoc|
|
||||
instance_variable_set "@#{assoc.name}", nil
|
||||
end unless self.new_record?
|
||||
end
|
||||
|
||||
private
|
||||
# Gets the specified association instance if it responds to :loaded?, nil otherwise.
|
||||
def association_instance_get(name)
|
||||
association = instance_variable_get("@#{name}")
|
||||
association if association.respond_to?(:loaded?)
|
||||
end
|
||||
|
||||
# Set the specified association instance.
|
||||
def association_instance_set(name, association)
|
||||
instance_variable_set("@#{name}", association)
|
||||
end
|
||||
|
||||
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
|
||||
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
|
||||
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
|
||||
# methods. Example:
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# belongs_to :portfolio
|
||||
# has_one :project_manager
|
||||
# has_many :milestones
|
||||
# has_and_belongs_to_many :categories
|
||||
# end
|
||||
#
|
||||
# The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:
|
||||
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
|
||||
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
|
||||
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
|
||||
# <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.find(:all, options),</tt>
|
||||
# <tt>Project#milestones.build, Project#milestones.create</tt>
|
||||
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
|
||||
# <tt>Project#categories.delete(category1)</tt>
|
||||
#
|
||||
# === A word of warning
|
||||
#
|
||||
# Don't create associations that have the same name as instance methods of ActiveRecord::Base. Since the association
|
||||
# adds a method with that name to its model, it will override the inherited method and break things.
|
||||
# For instance, +attributes+ and +connection+ would be bad choices for association names.
|
||||
#
|
||||
# == Auto-generated methods
|
||||
#
|
||||
# === Singular associations (one-to-one)
|
||||
# | | belongs_to |
|
||||
# generated methods | belongs_to | :polymorphic | has_one
|
||||
# ----------------------------------+------------+--------------+---------
|
||||
# other | X | X | X
|
||||
# other=(other) | X | X | X
|
||||
# build_other(attributes={}) | X | | X
|
||||
# create_other(attributes={}) | X | | X
|
||||
# other.create!(attributes={}) | | | X
|
||||
#
|
||||
# ===Collection associations (one-to-many / many-to-many)
|
||||
# | | | has_many
|
||||
# generated methods | habtm | has_many | :through
|
||||
# ----------------------------------+-------+----------+----------
|
||||
# others | X | X | X
|
||||
# others=(other,other,...) | X | X | X
|
||||
# other_ids | X | X | X
|
||||
# other_ids=(id,id,...) | X | X | X
|
||||
# others<< | X | X | X
|
||||
# others.push | X | X | X
|
||||
# others.concat | X | X | X
|
||||
# others.build(attributes={}) | X | X | X
|
||||
# others.create(attributes={}) | X | X | X
|
||||
# others.create!(attributes={}) | X | X | X
|
||||
# others.size | X | X | X
|
||||
# others.length | X | X | X
|
||||
# others.count | X | X | X
|
||||
# others.sum(args*,&block) | X | X | X
|
||||
# others.empty? | X | X | X
|
||||
# others.clear | X | X | X
|
||||
# others.delete(other,other,...) | X | X | X
|
||||
# others.delete_all | X | X |
|
||||
# others.destroy_all | X | X | X
|
||||
# others.find(*args) | X | X | X
|
||||
# others.find_first | X | |
|
||||
# others.exists? | X | X | X
|
||||
# others.uniq | X | X | X
|
||||
# others.reset | X | X | X
|
||||
#
|
||||
# == Cardinality and associations
|
||||
#
|
||||
# Active Record associations can be used to describe one-to-one, one-to-many and many-to-many
|
||||
# relationships between models. Each model uses an association to describe its role in
|
||||
# the relation. The +belongs_to+ association is always used in the model that has
|
||||
# the foreign key.
|
||||
#
|
||||
# === One-to-one
|
||||
#
|
||||
# Use +has_one+ in the base, and +belongs_to+ in the associated model.
|
||||
#
|
||||
# class Employee < ActiveRecord::Base
|
||||
# has_one :office
|
||||
# end
|
||||
# class Office < ActiveRecord::Base
|
||||
# belongs_to :employee # foreign key - employee_id
|
||||
# end
|
||||
#
|
||||
# === One-to-many
|
||||
#
|
||||
# Use +has_many+ in the base, and +belongs_to+ in the associated model.
|
||||
#
|
||||
# class Manager < ActiveRecord::Base
|
||||
# has_many :employees
|
||||
# end
|
||||
# class Employee < ActiveRecord::Base
|
||||
# belongs_to :manager # foreign key - manager_id
|
||||
# end
|
||||
#
|
||||
# === Many-to-many
|
||||
#
|
||||
# There are two ways to build a many-to-many relationship.
|
||||
#
|
||||
# The first way uses a +has_many+ association with the <tt>:through</tt> option and a join model, so
|
||||
# there are two stages of associations.
|
||||
#
|
||||
# class Assignment < ActiveRecord::Base
|
||||
# belongs_to :programmer # foreign key - programmer_id
|
||||
# belongs_to :project # foreign key - project_id
|
||||
# end
|
||||
# class Programmer < ActiveRecord::Base
|
||||
# has_many :assignments
|
||||
# has_many :projects, :through => :assignments
|
||||
# end
|
||||
# class Project < ActiveRecord::Base
|
||||
# has_many :assignments
|
||||
# has_many :programmers, :through => :assignments
|
||||
# end
|
||||
#
|
||||
# For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
|
||||
# that has no corresponding model or primary key.
|
||||
#
|
||||
# class Programmer < ActiveRecord::Base
|
||||
# has_and_belongs_to_many :projects # foreign keys in the join table
|
||||
# end
|
||||
# class Project < ActiveRecord::Base
|
||||
# has_and_belongs_to_many :programmers # foreign keys in the join table
|
||||
# end
|
||||
#
|
||||
# Choosing which way to build a many-to-many relationship is not always simple.
|
||||
# If you need to work with the relationship model as its own entity,
|
||||
# use <tt>has_many :through</tt>. Use +has_and_belongs_to_many+ when working with legacy schemas or when
|
||||
# you never work directly with the relationship itself.
|
||||
#
|
||||
# == Is it a +belongs_to+ or +has_one+ association?
|
||||
#
|
||||
# Both express a 1-1 relationship. The difference is mostly where to place the foreign key, which goes on the table for the class
|
||||
# declaring the +belongs_to+ relationship. Example:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# # I reference an account.
|
||||
# belongs_to :account
|
||||
# end
|
||||
#
|
||||
# class Account < ActiveRecord::Base
|
||||
# # One user references me.
|
||||
# has_one :user
|
||||
# end
|
||||
#
|
||||
# The tables for these classes could look something like:
|
||||
#
|
||||
# CREATE TABLE users (
|
||||
# id int(11) NOT NULL auto_increment,
|
||||
# account_id int(11) default NULL,
|
||||
# name varchar default NULL,
|
||||
# PRIMARY KEY (id)
|
||||
# )
|
||||
#
|
||||
# CREATE TABLE accounts (
|
||||
# id int(11) NOT NULL auto_increment,
|
||||
# name varchar default NULL,
|
||||
# PRIMARY KEY (id)
|
||||
# )
|
||||
#
|
||||
# == Unsaved objects and associations
|
||||
#
|
||||
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
|
||||
# aware of, mostly involving the saving of associated objects.
|
||||
#
|
||||
# Unless you set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
|
||||
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
|
||||
# to +true+ will _always_ save the members, whereas setting it to +false+ will
|
||||
# _never_ save the members.
|
||||
#
|
||||
# === One-to-one associations
|
||||
#
|
||||
# * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in
|
||||
# order to update their primary keys - except if the parent object is unsaved (<tt>new_record? == true</tt>).
|
||||
# * If either of these saves fail (due to one of the objects being invalid) the assignment statement returns +false+ and the assignment
|
||||
# is cancelled.
|
||||
# * If you wish to assign an object to a +has_one+ association without saving it, use the <tt>association.build</tt> method (documented below).
|
||||
# * Assigning an object to a +belongs_to+ association does not save the object, since the foreign key field belongs on the parent. It
|
||||
# does not save the parent either.
|
||||
#
|
||||
# === Collections
|
||||
#
|
||||
# * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically saves that object, except if the parent object
|
||||
# (the owner of the collection) is not yet stored in the database.
|
||||
# * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) fails, then <tt>push</tt> returns +false+.
|
||||
# * You can add an object to a collection without automatically saving it by using the <tt>collection.build</tt> method (documented below).
|
||||
# * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically saved when the parent is saved.
|
||||
#
|
||||
# === Association callbacks
|
||||
#
|
||||
# Similar to the normal callbacks that hook into the lifecycle of an Active Record object, you can also define callbacks that get
|
||||
# triggered when you add an object to or remove an object from an association collection. Example:
|
||||
#
|
||||
# class Project
|
||||
# has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
|
||||
#
|
||||
# def evaluate_velocity(developer)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# It's possible to stack callbacks by passing them as an array. Example:
|
||||
#
|
||||
# class Project
|
||||
# has_and_belongs_to_many :developers, :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
|
||||
# end
|
||||
#
|
||||
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
|
||||
#
|
||||
# Should any of the +before_add+ callbacks throw an exception, the object does not get added to the collection. Same with
|
||||
# the +before_remove+ callbacks; if an exception is thrown the object doesn't get removed.
|
||||
#
|
||||
# === Association extensions
|
||||
#
|
||||
# The proxy objects that control the access to associations can be extended through anonymous modules. This is especially
|
||||
# beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association.
|
||||
# Example:
|
||||
#
|
||||
# class Account < ActiveRecord::Base
|
||||
# has_many :people do
|
||||
# def find_or_create_by_name(name)
|
||||
# first_name, last_name = name.split(" ", 2)
|
||||
# find_or_create_by_first_name_and_last_name(first_name, last_name)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Account.find(:first).people.find_or_create_by_name("David Heinemeier Hansson")
|
||||
# person.first_name # => "David"
|
||||
# person.last_name # => "Heinemeier Hansson"
|
||||
#
|
||||
# If you need to share the same extensions between many associations, you can use a named extension module. Example:
|
||||
#
|
||||
# module FindOrCreateByNameExtension
|
||||
# def find_or_create_by_name(name)
|
||||
# first_name, last_name = name.split(" ", 2)
|
||||
# find_or_create_by_first_name_and_last_name(first_name, last_name)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Account < ActiveRecord::Base
|
||||
# has_many :people, :extend => FindOrCreateByNameExtension
|
||||
# end
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# has_many :people, :extend => FindOrCreateByNameExtension
|
||||
# end
|
||||
#
|
||||
# If you need to use multiple named extension modules, you can specify an array of modules with the <tt>:extend</tt> option.
|
||||
# In the case of name conflicts between methods in the modules, methods in modules later in the array supercede
|
||||
# those earlier in the array. Example:
|
||||
#
|
||||
# class Account < ActiveRecord::Base
|
||||
# has_many :people, :extend => [FindOrCreateByNameExtension, FindRecentExtension]
|
||||
# end
|
||||
#
|
||||
# Some extensions can only be made to work with knowledge of the association proxy's internals.
|
||||
# Extensions can access relevant state using accessors on the association proxy:
|
||||
#
|
||||
# * +proxy_owner+ - Returns the object the association is part of.
|
||||
# * +proxy_reflection+ - Returns the reflection object that describes the association.
|
||||
# * +proxy_target+ - Returns the associated object for +belongs_to+ and +has_one+, or the collection of associated objects for +has_many+ and +has_and_belongs_to_many+.
|
||||
#
|
||||
# === Association Join Models
|
||||
#
|
||||
# Has Many associations can be configured with the <tt>:through</tt> option to use an explicit join model to retrieve the data. This
|
||||
# operates similarly to a +has_and_belongs_to_many+ association. The advantage is that you're able to add validations,
|
||||
# callbacks, and extra attributes on the join model. Consider the following schema:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :authorships
|
||||
# has_many :books, :through => :authorships
|
||||
# end
|
||||
#
|
||||
# class Authorship < ActiveRecord::Base
|
||||
# belongs_to :author
|
||||
# belongs_to :book
|
||||
# end
|
||||
#
|
||||
# @author = Author.find :first
|
||||
# @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to.
|
||||
# @author.books # selects all books by using the Authorship join model
|
||||
#
|
||||
# You can also go through a +has_many+ association on the join model:
|
||||
#
|
||||
# class Firm < ActiveRecord::Base
|
||||
# has_many :clients
|
||||
# has_many :invoices, :through => :clients
|
||||
# end
|
||||
#
|
||||
# class Client < ActiveRecord::Base
|
||||
# belongs_to :firm
|
||||
# has_many :invoices
|
||||
# end
|
||||
#
|
||||
# class Invoice < ActiveRecord::Base
|
||||
# belongs_to :client
|
||||
# end
|
||||
#
|
||||
# @firm = Firm.find :first
|
||||
# @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
|
||||
# @firm.invoices # selects all invoices by going through the Client join model.
|
||||
#
|
||||
# Similarly you can go through a +has_one+ association on the join model:
|
||||
#
|
||||
# class Group < ActiveRecord::Base
|
||||
# has_many :users
|
||||
# has_many :avatars, :through => :users
|
||||
# end
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# belongs_to :group
|
||||
# has_one :avatar
|
||||
# end
|
||||
#
|
||||
# class Avatar < ActiveRecord::Base
|
||||
# belongs_to :user
|
||||
# end
|
||||
#
|
||||
# @group = Group.first
|
||||
# @group.users.collect { |u| u.avatar }.flatten # select all avatars for all users in the group
|
||||
# @group.avatars # selects all avatars by going through the User join model.
|
||||
#
|
||||
# An important caveat with going through +has_one+ or +has_many+ associations on the join model is that these associations are
|
||||
# *read-only*. For example, the following would not work following the previous example:
|
||||
#
|
||||
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around.
|
||||
# @group.avatars.delete(@group.avatars.last) # so would this
|
||||
#
|
||||
# === Polymorphic Associations
|
||||
#
|
||||
# Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
|
||||
# specify an interface that a +has_many+ association must adhere to.
|
||||
#
|
||||
# class Asset < ActiveRecord::Base
|
||||
# belongs_to :attachable, :polymorphic => true
|
||||
# end
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
|
||||
# end
|
||||
#
|
||||
# @asset.attachable = @post
|
||||
#
|
||||
# This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need
|
||||
# an +attachable_id+ integer column and an +attachable_type+ string column.
|
||||
#
|
||||
# Using polymorphic associations in combination with single table inheritance (STI) is a little tricky. In order
|
||||
# for the associations to work as expected, ensure that you store the base model for the STI models in the
|
||||
# type column of the polymorphic association. To continue with the asset example above, suppose there are guest posts
|
||||
# and member posts that use the posts table for STI. In this case, there must be a +type+ column in the posts table.
|
||||
#
|
||||
# class Asset < ActiveRecord::Base
|
||||
# belongs_to :attachable, :polymorphic => true
|
||||
#
|
||||
# def attachable_type=(sType)
|
||||
# super(sType.to_s.classify.constantize.base_class.to_s)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# # because we store "Post" in attachable_type now :dependent => :destroy will work
|
||||
# has_many :assets, :as => :attachable, :dependent => :destroy
|
||||
# end
|
||||
#
|
||||
# class GuestPost < Post
|
||||
# end
|
||||
#
|
||||
# class MemberPost < Post
|
||||
# end
|
||||
#
|
||||
# == Caching
|
||||
#
|
||||
# All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
|
||||
# instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without
|
||||
# worrying too much about performance at the first go. Example:
|
||||
#
|
||||
# project.milestones # fetches milestones from the database
|
||||
# project.milestones.size # uses the milestone cache
|
||||
# project.milestones.empty? # uses the milestone cache
|
||||
# project.milestones(true).size # fetches milestones from the database
|
||||
# project.milestones # uses the milestone cache
|
||||
#
|
||||
# == Eager loading of associations
|
||||
#
|
||||
# Eager loading is a way to find objects of a certain class and a number of named associations. This is
|
||||
# one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 posts that each need to display their author
|
||||
# triggers 101 database queries. Through the use of eager loading, the 101 queries can be reduced to 2. Example:
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# belongs_to :author
|
||||
# has_many :comments
|
||||
# end
|
||||
#
|
||||
# Consider the following loop using the class above:
|
||||
#
|
||||
# for post in Post.all
|
||||
# puts "Post: " + post.title
|
||||
# puts "Written by: " + post.author.name
|
||||
# puts "Last comment on: " + post.comments.first.created_on
|
||||
# end
|
||||
#
|
||||
# To iterate over these one hundred posts, we'll generate 201 database queries. Let's first just optimize it for retrieving the author:
|
||||
#
|
||||
# for post in Post.find(:all, :include => :author)
|
||||
#
|
||||
# This references the name of the +belongs_to+ association that also used the <tt>:author</tt> symbol. After loading the posts, find
|
||||
# will collect the +author_id+ from each one and load all the referenced authors with one query. Doing so will cut down the number of queries from 201 to 102.
|
||||
#
|
||||
# We can improve upon the situation further by referencing both associations in the finder with:
|
||||
#
|
||||
# for post in Post.find(:all, :include => [ :author, :comments ])
|
||||
#
|
||||
# This will load all comments with a single query. This reduces the total number of queries to 3. More generally the number of queries
|
||||
# will be 1 plus the number of associations named (except if some of the associations are polymorphic +belongs_to+ - see below).
|
||||
#
|
||||
# To include a deep hierarchy of associations, use a hash:
|
||||
#
|
||||
# for post in Post.find(:all, :include => [ :author, { :comments => { :author => :gravatar } } ])
|
||||
#
|
||||
# That'll grab not only all the comments but all their authors and gravatar pictures. You can mix and match
|
||||
# symbols, arrays and hashes in any combination to describe the associations you want to load.
|
||||
#
|
||||
# All of this power shouldn't fool you into thinking that you can pull out huge amounts of data with no performance penalty just because you've reduced
|
||||
# the number of queries. The database still needs to send all the data to Active Record and it still needs to be processed. So it's no
|
||||
# catch-all for performance problems, but it's a great way to cut down on the number of queries in a situation as the one described above.
|
||||
#
|
||||
# Since only one table is loaded at a time, conditions or orders cannot reference tables other than the main one. If this is the case
|
||||
# Active Record falls back to the previously used LEFT OUTER JOIN based strategy. For example
|
||||
#
|
||||
# Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true])
|
||||
#
|
||||
# will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
|
||||
# <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions like this can have unintended consequences.
|
||||
# In the above example posts with no approved comments are not returned at all, because the conditions apply to the SQL statement as a whole
|
||||
# and not just to the association. You must disambiguate column references for this fallback to happen, for example
|
||||
# <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
|
||||
#
|
||||
# If you do want eagerload only some members of an association it is usually more natural to <tt>:include</tt> an association
|
||||
# which has conditions defined on it:
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :approved_comments, :class_name => 'Comment', :conditions => ['approved = ?', true]
|
||||
# end
|
||||
#
|
||||
# Post.find(:all, :include => :approved_comments)
|
||||
#
|
||||
# will load posts and eager load the +approved_comments+ association, which contains only those comments that have been approved.
|
||||
#
|
||||
# If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, returning all the associated objects:
|
||||
#
|
||||
# class Picture < ActiveRecord::Base
|
||||
# has_many :most_recent_comments, :class_name => 'Comment', :order => 'id DESC', :limit => 10
|
||||
# end
|
||||
#
|
||||
# Picture.find(:first, :include => :most_recent_comments).most_recent_comments # => returns all associated comments.
|
||||
#
|
||||
# When eager loaded, conditions are interpolated in the context of the model class, not the model instance. Conditions are lazily interpolated
|
||||
# before the actual model exists.
|
||||
#
|
||||
# Eager loading is supported with polymorphic associations.
|
||||
#
|
||||
# class Address < ActiveRecord::Base
|
||||
# belongs_to :addressable, :polymorphic => true
|
||||
# end
|
||||
#
|
||||
# A call that tries to eager load the addressable model
|
||||
#
|
||||
# Address.find(:all, :include => :addressable)
|
||||
#
|
||||
# will execute one query to load the addresses and load the addressables with one query per addressable type.
|
||||
# For example if all the addressables are either of class Person or Company then a total of 3 queries will be executed. The list of
|
||||
# addressable types to load is determined on the back of the addresses loaded. This is not supported if Active Record has to fallback
|
||||
# to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent
|
||||
# model's type is a column value so its corresponding table name cannot be put in the +FROM+/+JOIN+ clauses of that query.
|
||||
#
|
||||
# == Table Aliasing
|
||||
#
|
||||
# Active Record uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once,
|
||||
# the standard table name is used. The second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. Indexes are appended
|
||||
# for any more successive uses of the table name.
|
||||
#
|
||||
# Post.find :all, :joins => :comments
|
||||
# # => SELECT ... FROM posts INNER JOIN comments ON ...
|
||||
# Post.find :all, :joins => :special_comments # STI
|
||||
# # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
|
||||
# Post.find :all, :joins => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
|
||||
# # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts
|
||||
#
|
||||
# Acts as tree example:
|
||||
#
|
||||
# TreeMixin.find :all, :joins => :children
|
||||
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
||||
# TreeMixin.find :all, :joins => {:children => :parent}
|
||||
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
||||
# INNER JOIN parents_mixins ...
|
||||
# TreeMixin.find :all, :joins => {:children => {:parent => :children}}
|
||||
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
|
||||
# INNER JOIN parents_mixins ...
|
||||
# INNER JOIN mixins childrens_mixins_2
|
||||
#
|
||||
# Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix:
|
||||
#
|
||||
# Post.find :all, :joins => :categories
|
||||
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
||||
# Post.find :all, :joins => {:categories => :posts}
|
||||
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
||||
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
|
||||
# Post.find :all, :joins => {:categories => {:posts => :categories}}
|
||||
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
|
||||
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
|
||||
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
|
||||
#
|
||||
# If you wish to specify your own custom joins using a <tt>:joins</tt> option, those table names will take precedence over the eager associations:
|
||||
#
|
||||
# Post.find :all, :joins => :comments, :joins => "inner join comments ..."
|
||||
# # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
|
||||
# Post.find :all, :joins => [:comments, :special_comments], :joins => "inner join comments ..."
|
||||
# # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
|
||||
# INNER JOIN comments special_comments_posts ...
|
||||
# INNER JOIN comments ...
|
||||
#
|
||||
# Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database.
|
||||
#
|
||||
# == Modules
|
||||
#
|
||||
# By default, associations will look for objects within the current module scope. Consider:
|
||||
#
|
||||
# module MyApplication
|
||||
# module Business
|
||||
# class Firm < ActiveRecord::Base
|
||||
# has_many :clients
|
||||
# end
|
||||
#
|
||||
# class Client < ActiveRecord::Base; end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
|
||||
# If you want to associate with a class in another module scope, this can be done by specifying the complete class name.
|
||||
# Example:
|
||||
#
|
||||
# module MyApplication
|
||||
# module Business
|
||||
# class Firm < ActiveRecord::Base; end
|
||||
# end
|
||||
#
|
||||
# module Billing
|
||||
# class Account < ActiveRecord::Base
|
||||
# belongs_to :firm, :class_name => "MyApplication::Business::Firm"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
|
||||
#
|
||||
# If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
|
||||
# get an <tt>ActiveRecord::AssociationTypeMismatch</tt>.
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones
|
||||
# possible.
|
||||
module ClassMethods
|
||||
# Specifies a one-to-many association. The following methods for retrieval and query of
|
||||
# collections of associated objects will be added:
|
||||
#
|
||||
# [collection(force_reload = false)]
|
||||
# Returns an array of all the associated objects.
|
||||
# An empty array is returned if none are found.
|
||||
# [collection<<(object, ...)]
|
||||
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
||||
# [collection.delete(object, ...)]
|
||||
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
||||
# Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
|
||||
# and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
|
||||
# [collection=objects]
|
||||
# Replaces the collections content by deleting and adding objects as appropriate.
|
||||
# [collection_singular_ids]
|
||||
# Returns an array of the associated objects' ids
|
||||
# [collection_singular_ids=ids]
|
||||
# Replace the collection with the objects identified by the primary keys in +ids+
|
||||
# [collection.clear]
|
||||
# Removes every object from the collection. This destroys the associated objects if they
|
||||
# are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the
|
||||
# database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
|
||||
# [collection.empty?]
|
||||
# Returns +true+ if there are no associated objects.
|
||||
# [collection.size]
|
||||
# Returns the number of associated objects.
|
||||
# [collection.find(...)]
|
||||
# Finds an associated object according to the same rules as ActiveRecord::Base.find.
|
||||
# [collection.exists?(...)]
|
||||
# Checks whether an associated object with the given conditions exists.
|
||||
# Uses the same rules as ActiveRecord::Base.exists?.
|
||||
# [collection.build(attributes = {}, ...)]
|
||||
# Returns one or more new objects of the collection type that have been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key, but have not yet
|
||||
# been saved. <b>Note:</b> This only works if an associated object already exists, not if
|
||||
# it's +nil+!
|
||||
# [collection.create(attributes = {})]
|
||||
# Returns a new object of the collection type that has been instantiated
|
||||
# with +attributes+, linked to this object through a foreign key, and that has already
|
||||
# been saved (if it passed the validation). <b>Note:</b> This only works if an associated
|
||||
# object already exists, not if it's +nil+!
|
||||
#
|
||||
# (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
||||
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => ["firm_id = ?", id]</tt>)
|
||||
# * <tt>Firm#clients<<</tt>
|
||||
# * <tt>Firm#clients.delete</tt>
|
||||
# * <tt>Firm#clients=</tt>
|
||||
# * <tt>Firm#client_ids</tt>
|
||||
# * <tt>Firm#client_ids=</tt>
|
||||
# * <tt>Firm#clients.clear</tt>
|
||||
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
||||
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
||||
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
||||
# * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>)
|
||||
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
||||
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# === Supported options
|
||||
# [:class_name]
|
||||
# Specify the class name of the association. Use it only if that name can't be inferred
|
||||
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
|
||||
# if the real class name is SpecialProduct, you'll have to specify it with this option.
|
||||
# [:conditions]
|
||||
# Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
|
||||
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
|
||||
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
||||
# or <tt>@blog.posts.build</tt>.
|
||||
# [:order]
|
||||
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
||||
# such as <tt>last_name, first_name DESC</tt>.
|
||||
# [:foreign_key]
|
||||
# Specify the foreign key used for the association. By default this is guessed to be the name
|
||||
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
|
||||
# as the default <tt>:foreign_key</tt>.
|
||||
# [:primary_key]
|
||||
# Specify the method that returns the primary key used for the association. By default this is +id+.
|
||||
# [:dependent]
|
||||
# If set to <tt>:destroy</tt> all the associated objects are destroyed
|
||||
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
||||
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
||||
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
|
||||
# the <tt>:through</tt> option.
|
||||
# [:finder_sql]
|
||||
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
||||
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
||||
# [:counter_sql]
|
||||
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
|
||||
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
|
||||
# [:extend]
|
||||
# Specify a named module for extending the proxy. See "Association extensions".
|
||||
# [:include]
|
||||
# Specify second-order associations that should be eager loaded when the collection is loaded.
|
||||
# [:group]
|
||||
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
||||
# [:having]
|
||||
# Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
|
||||
# [:limit]
|
||||
# An integer determining the limit on the number of rows that should be returned.
|
||||
# [:offset]
|
||||
# An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
||||
# [:select]
|
||||
# By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
|
||||
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
||||
# [:as]
|
||||
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
||||
# [:through]
|
||||
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
||||
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
|
||||
# <tt>has_one</tt> or <tt>has_many</tt> association on the join model.
|
||||
# [:source]
|
||||
# Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
||||
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
|
||||
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
|
||||
# [:source_type]
|
||||
# Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
|
||||
# association is a polymorphic +belongs_to+.
|
||||
# [:uniq]
|
||||
# If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
||||
# [:readonly]
|
||||
# If true, all the associated objects are readonly through the association.
|
||||
# [:validate]
|
||||
# If false, don't validate the associated objects when saving the parent object. true by default.
|
||||
# [:autosave]
|
||||
# If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_many :comments, :order => "posted_on"
|
||||
# has_many :comments, :include => :author
|
||||
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
|
||||
# has_many :tracks, :order => "position", :dependent => :destroy
|
||||
# has_many :comments, :dependent => :nullify
|
||||
# has_many :tags, :as => :taggable
|
||||
# has_many :reports, :readonly => true
|
||||
# has_many :subscribers, :through => :subscriptions, :source => :user
|
||||
# has_many :subscribers, :class_name => "Person", :finder_sql =>
|
||||
# 'SELECT DISTINCT people.* ' +
|
||||
# 'FROM people p, post_subscriptions ps ' +
|
||||
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
||||
# 'ORDER BY p.first_name'
|
||||
def has_many(association_id, options = {}, &extension)
|
||||
reflection = create_has_many_reflection(association_id, options, &extension)
|
||||
configure_dependency_for_has_many(reflection)
|
||||
add_association_callbacks(reflection.name, reflection.options)
|
||||
|
||||
if options[:through]
|
||||
collection_accessor_methods(reflection, HasManyThroughAssociation)
|
||||
else
|
||||
collection_accessor_methods(reflection, HasManyAssociation)
|
||||
end
|
||||
end
|
||||
|
||||
# Specifies a one-to-one association with another class. This method should only be used
|
||||
# if the other class contains the foreign key. If the current class contains the foreign key,
|
||||
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
|
||||
# on when to use has_one and when to use belongs_to.
|
||||
#
|
||||
# The following methods for retrieval and query of a single associated object will be added:
|
||||
#
|
||||
# [association(force_reload = false)]
|
||||
# Returns the associated object. +nil+ is returned if none is found.
|
||||
# [association=(associate)]
|
||||
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
|
||||
# and saves the associate object.
|
||||
# [build_association(attributes = {})]
|
||||
# Returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key, but has not
|
||||
# yet been saved. <b>Note:</b> This ONLY works if an association already exists.
|
||||
# It will NOT work if the association is +nil+.
|
||||
# [create_association(attributes = {})]
|
||||
# Returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+, linked to this object through a foreign key, and that
|
||||
# has already been saved (if it passed the validation).
|
||||
#
|
||||
# (+association+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
|
||||
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
|
||||
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
|
||||
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
|
||||
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
|
||||
#
|
||||
# === Options
|
||||
#
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# Options are:
|
||||
# [:class_name]
|
||||
# Specify the class name of the association. Use it only if that name can't be inferred
|
||||
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
|
||||
# if the real class name is Person, you'll have to specify it with this option.
|
||||
# [:conditions]
|
||||
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
||||
# SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
|
||||
# is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create an enabled account with <tt>@company.create_account</tt>
|
||||
# or <tt>@company.build_account</tt>.
|
||||
# [:order]
|
||||
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
||||
# such as <tt>last_name, first_name DESC</tt>.
|
||||
# [:dependent]
|
||||
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
||||
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
|
||||
# object's foreign key is set to +NULL+. Also, association is assigned.
|
||||
# [:foreign_key]
|
||||
# Specify the foreign key used for the association. By default this is guessed to be the name
|
||||
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association will use "person_id"
|
||||
# as the default <tt>:foreign_key</tt>.
|
||||
# [:primary_key]
|
||||
# Specify the method that returns the primary key used for the association. By default this is +id+.
|
||||
# [:include]
|
||||
# Specify second-order associations that should be eager loaded when this object is loaded.
|
||||
# [:as]
|
||||
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
||||
# [:select]
|
||||
# By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
||||
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
||||
# [:through]
|
||||
# Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
||||
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a
|
||||
# <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model.
|
||||
# [:source]
|
||||
# Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
|
||||
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
|
||||
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
|
||||
# [:source_type]
|
||||
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
|
||||
# association is a polymorphic +belongs_to+.
|
||||
# [:readonly]
|
||||
# If true, the associated object is readonly through the association.
|
||||
# [:validate]
|
||||
# If false, don't validate the associated object when saving the parent object. +false+ by default.
|
||||
# [:autosave]
|
||||
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
|
||||
# has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it
|
||||
# has_one :last_comment, :class_name => "Comment", :order => "posted_on"
|
||||
# has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
|
||||
# has_one :attachment, :as => :attachable
|
||||
# has_one :boss, :readonly => :true
|
||||
# has_one :club, :through => :membership
|
||||
# has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable
|
||||
def has_one(association_id, options = {})
|
||||
if options[:through]
|
||||
reflection = create_has_one_through_reflection(association_id, options)
|
||||
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
|
||||
else
|
||||
reflection = create_has_one_reflection(association_id, options)
|
||||
association_accessor_methods(reflection, HasOneAssociation)
|
||||
association_constructor_method(:build, reflection, HasOneAssociation)
|
||||
association_constructor_method(:create, reflection, HasOneAssociation)
|
||||
configure_dependency_for_has_one(reflection)
|
||||
end
|
||||
end
|
||||
|
||||
# Specifies a one-to-one association with another class. This method should only be used
|
||||
# if this class contains the foreign key. If the other class contains the foreign key,
|
||||
# then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
|
||||
# on when to use +has_one+ and when to use +belongs_to+.
|
||||
#
|
||||
# Methods will be added for retrieval and query for a single associated object, for which
|
||||
# this object holds an id:
|
||||
#
|
||||
# [association(force_reload = false)]
|
||||
# Returns the associated object. +nil+ is returned if none is found.
|
||||
# [association=(associate)]
|
||||
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
||||
# [build_association(attributes = {})]
|
||||
# Returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
|
||||
# [create_association(attributes = {})]
|
||||
# Returns a new object of the associated type that has been instantiated
|
||||
# with +attributes+, linked to this object through a foreign key, and that
|
||||
# has already been saved (if it passed the validation).
|
||||
#
|
||||
# (+association+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# A Post class declares <tt>belongs_to :author</tt>, which will add:
|
||||
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
||||
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
||||
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
||||
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
||||
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
||||
# The declaration can also include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# === Options
|
||||
#
|
||||
# [:class_name]
|
||||
# Specify the class name of the association. Use it only if that name can't be inferred
|
||||
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
|
||||
# if the real class name is Person, you'll have to specify it with this option.
|
||||
# [:conditions]
|
||||
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
||||
# SQL fragment, such as <tt>authorized = 1</tt>.
|
||||
# [:select]
|
||||
# By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
||||
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
||||
# [:foreign_key]
|
||||
# Specify the foreign key used for the association. By default this is guessed to be the name
|
||||
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
|
||||
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
|
||||
# will use a foreign key of "favorite_person_id".
|
||||
# [:primary_key]
|
||||
# Specify the method that returns the primary key of associated object used for the association. By default this is id.
|
||||
# [:dependent]
|
||||
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
||||
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
|
||||
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
|
||||
# orphaned records behind.
|
||||
# [:counter_cache]
|
||||
# Caches the number of belonging objects on the associate class through the use of +increment_counter+
|
||||
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
|
||||
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
||||
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
|
||||
# a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
||||
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
|
||||
# [:include]
|
||||
# Specify second-order associations that should be eager loaded when this object is loaded.
|
||||
# [:polymorphic]
|
||||
# Specify this association is a polymorphic association by passing +true+.
|
||||
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
||||
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
||||
# [:readonly]
|
||||
# If true, the associated object is readonly through the association.
|
||||
# [:validate]
|
||||
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
|
||||
# [:autosave]
|
||||
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
|
||||
# [:touch]
|
||||
# If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or
|
||||
# destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute.
|
||||
#
|
||||
# Option examples:
|
||||
# belongs_to :firm, :foreign_key => "client_of"
|
||||
# belongs_to :person, :primary_key => "name", :foreign_key => "person_name"
|
||||
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
||||
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
||||
# :conditions => 'discounts > #{payments_count}'
|
||||
# belongs_to :attachable, :polymorphic => true
|
||||
# belongs_to :project, :readonly => true
|
||||
# belongs_to :post, :counter_cache => true
|
||||
# belongs_to :company, :touch => true
|
||||
# belongs_to :company, :touch => :employees_last_updated_at
|
||||
def belongs_to(association_id, options = {})
|
||||
reflection = create_belongs_to_reflection(association_id, options)
|
||||
|
||||
if reflection.options[:polymorphic]
|
||||
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
||||
else
|
||||
association_accessor_methods(reflection, BelongsToAssociation)
|
||||
association_constructor_method(:build, reflection, BelongsToAssociation)
|
||||
association_constructor_method(:create, reflection, BelongsToAssociation)
|
||||
end
|
||||
|
||||
add_counter_cache_callbacks(reflection) if options[:counter_cache]
|
||||
add_touch_callbacks(reflection, options[:touch]) if options[:touch]
|
||||
|
||||
configure_dependency_for_belongs_to(reflection)
|
||||
end
|
||||
|
||||
# Specifies a many-to-many relationship with another class. This associates two classes via an
|
||||
# intermediate join table. Unless the join table is explicitly specified as an option, it is
|
||||
# guessed using the lexical order of the class names. So a join between Developer and Project
|
||||
# will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence
|
||||
# is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths,
|
||||
# and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
|
||||
# lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers"
|
||||
# to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes",
|
||||
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
|
||||
# custom <tt>:join_table</tt> option if you need to.
|
||||
#
|
||||
# The join table should not have a primary key or a model associated with it. You must manually generate the
|
||||
# join table with a migration such as this:
|
||||
#
|
||||
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :developers_projects, :id => false do |t|
|
||||
# t.integer :developer_id
|
||||
# t.integer :project_id
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# drop_table :developers_projects
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through
|
||||
# +has_and_belongs_to_many+ associations. Records returned from join tables with additional attributes will be marked as
|
||||
# readonly (because we can't save changes to the additional attributes). It's strongly recommended that you upgrade any
|
||||
# associations with attributes to a real join model (see introduction).
|
||||
#
|
||||
# Adds the following methods for retrieval and query:
|
||||
#
|
||||
# [collection(force_reload = false)]
|
||||
# Returns an array of all the associated objects.
|
||||
# An empty array is returned if none are found.
|
||||
# [collection<<(object, ...)]
|
||||
# Adds one or more objects to the collection by creating associations in the join table
|
||||
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
|
||||
# [collection.delete(object, ...)]
|
||||
# Removes one or more objects from the collection by removing their associations from the join table.
|
||||
# This does not destroy the objects.
|
||||
# [collection=objects]
|
||||
# Replaces the collection's content by deleting and adding objects as appropriate.
|
||||
# [collection_singular_ids]
|
||||
# Returns an array of the associated objects' ids.
|
||||
# [collection_singular_ids=ids]
|
||||
# Replace the collection by the objects identified by the primary keys in +ids+.
|
||||
# [collection.clear]
|
||||
# Removes every object from the collection. This does not destroy the objects.
|
||||
# [collection.empty?]
|
||||
# Returns +true+ if there are no associated objects.
|
||||
# [collection.size]
|
||||
# Returns the number of associated objects.
|
||||
# [collection.find(id)]
|
||||
# Finds an associated object responding to the +id+ and that
|
||||
# meets the condition that it has to be associated with this object.
|
||||
# Uses the same rules as ActiveRecord::Base.find.
|
||||
# [collection.exists?(...)]
|
||||
# Checks whether an associated object with the given conditions exists.
|
||||
# Uses the same rules as ActiveRecord::Base.exists?.
|
||||
# [collection.build(attributes = {})]
|
||||
# Returns a new object of the collection type that has been instantiated
|
||||
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
|
||||
# [collection.create(attributes = {})]
|
||||
# Returns a new object of the collection type that has been instantiated
|
||||
# with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation).
|
||||
#
|
||||
# (+collection+ is replaced with the symbol passed as the first argument, so
|
||||
# <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
|
||||
# * <tt>Developer#projects</tt>
|
||||
# * <tt>Developer#projects<<</tt>
|
||||
# * <tt>Developer#projects.delete</tt>
|
||||
# * <tt>Developer#projects=</tt>
|
||||
# * <tt>Developer#project_ids</tt>
|
||||
# * <tt>Developer#project_ids=</tt>
|
||||
# * <tt>Developer#projects.clear</tt>
|
||||
# * <tt>Developer#projects.empty?</tt>
|
||||
# * <tt>Developer#projects.size</tt>
|
||||
# * <tt>Developer#projects.find(id)</tt>
|
||||
# * <tt>Developer#clients.exists?(...)</tt>
|
||||
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
|
||||
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
|
||||
# The declaration may include an options hash to specialize the behavior of the association.
|
||||
#
|
||||
# === Options
|
||||
#
|
||||
# [:class_name]
|
||||
# Specify the class name of the association. Use it only if that name can't be inferred
|
||||
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
|
||||
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
|
||||
# [:join_table]
|
||||
# Specify the name of the join table if the default based on lexical order isn't what you want.
|
||||
# <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
|
||||
# MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
|
||||
# [:foreign_key]
|
||||
# Specify the foreign key used for the association. By default this is guessed to be the name
|
||||
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
|
||||
# to Project will use "person_id" as the default <tt>:foreign_key</tt>.
|
||||
# [:association_foreign_key]
|
||||
# Specify the foreign key used for the association on the receiving side of the association.
|
||||
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
|
||||
# So if a Person class makes a +has_and_belongs_to_many+ association to Project,
|
||||
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
|
||||
# [:conditions]
|
||||
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
||||
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
|
||||
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
||||
# or <tt>@blog.posts.build</tt>.
|
||||
# [:order]
|
||||
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
||||
# such as <tt>last_name, first_name DESC</tt>
|
||||
# [:uniq]
|
||||
# If true, duplicate associated objects will be ignored by accessors and query methods.
|
||||
# [:finder_sql]
|
||||
# Overwrite the default generated SQL statement used to fetch the association with a manual statement
|
||||
# [:counter_sql]
|
||||
# Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
|
||||
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
|
||||
# [:delete_sql]
|
||||
# Overwrite the default generated SQL statement used to remove links between the associated
|
||||
# classes with a manual statement.
|
||||
# [:insert_sql]
|
||||
# Overwrite the default generated SQL statement used to add links between the associated classes
|
||||
# with a manual statement.
|
||||
# [:extend]
|
||||
# Anonymous module for extending the proxy, see "Association extensions".
|
||||
# [:include]
|
||||
# Specify second-order associations that should be eager loaded when the collection is loaded.
|
||||
# [:group]
|
||||
# An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
||||
# [:having]
|
||||
# Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
|
||||
# [:limit]
|
||||
# An integer determining the limit on the number of rows that should be returned.
|
||||
# [:offset]
|
||||
# An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
||||
# [:select]
|
||||
# By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
||||
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
||||
# [:readonly]
|
||||
# If true, all the associated objects are readonly through the association.
|
||||
# [:validate]
|
||||
# If false, don't validate the associated objects when saving the parent object. +true+ by default.
|
||||
# [:autosave]
|
||||
# If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_and_belongs_to_many :projects
|
||||
# has_and_belongs_to_many :projects, :include => [ :milestones, :manager ]
|
||||
# has_and_belongs_to_many :nations, :class_name => "Country"
|
||||
# has_and_belongs_to_many :categories, :join_table => "prods_cats"
|
||||
# has_and_belongs_to_many :categories, :readonly => true
|
||||
# has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
|
||||
# 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
|
||||
def has_and_belongs_to_many(association_id, options = {}, &extension)
|
||||
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
||||
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
|
||||
|
||||
# Don't use a before_destroy callback since users' before_destroy
|
||||
# callbacks will be executed after the association is wiped out.
|
||||
old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
|
||||
class_eval <<-end_eval unless method_defined?(old_method)
|
||||
alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks
|
||||
def destroy_without_callbacks # def destroy_without_callbacks
|
||||
#{reflection.name}.clear # posts.clear
|
||||
#{old_method} # destroy_without_habtm_shim_for_posts
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
add_association_callbacks(reflection.name, options)
|
||||
end
|
||||
|
||||
private
|
||||
# Generates a join table name from two provided table names.
|
||||
# The names in the join table namesme end up in lexicographic order.
|
||||
#
|
||||
# join_table_name("members", "clubs") # => "clubs_members"
|
||||
# join_table_name("members", "special_clubs") # => "members_special_clubs"
|
||||
def join_table_name(first_table_name, second_table_name)
|
||||
if first_table_name < second_table_name
|
||||
join_table = "#{first_table_name}_#{second_table_name}"
|
||||
else
|
||||
join_table = "#{second_table_name}_#{first_table_name}"
|
||||
end
|
||||
|
||||
table_name_prefix + join_table + table_name_suffix
|
||||
end
|
||||
|
||||
def association_accessor_methods(reflection, association_proxy_class)
|
||||
define_method(reflection.name) do |*params|
|
||||
force_reload = params.first unless params.empty?
|
||||
association = association_instance_get(reflection.name)
|
||||
|
||||
if association.nil? || force_reload
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
retval = association.reload
|
||||
if retval.nil? and association_proxy_class == BelongsToAssociation
|
||||
association_instance_set(reflection.name, nil)
|
||||
return nil
|
||||
end
|
||||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
|
||||
association.target.nil? ? nil : association
|
||||
end
|
||||
|
||||
define_method("loaded_#{reflection.name}?") do
|
||||
association = association_instance_get(reflection.name)
|
||||
association && association.loaded?
|
||||
end
|
||||
|
||||
define_method("#{reflection.name}=") do |new_value|
|
||||
association = association_instance_get(reflection.name)
|
||||
|
||||
if association.nil? || association.target != new_value
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
end
|
||||
|
||||
if association_proxy_class == HasOneThroughAssociation
|
||||
association.create_through_record(new_value)
|
||||
if new_record?
|
||||
association_instance_set(reflection.name, new_value.nil? ? nil : association)
|
||||
else
|
||||
self.send(reflection.name, new_value)
|
||||
end
|
||||
else
|
||||
association.replace(new_value)
|
||||
association_instance_set(reflection.name, new_value.nil? ? nil : association)
|
||||
end
|
||||
end
|
||||
|
||||
define_method("set_#{reflection.name}_target") do |target|
|
||||
return if target.nil? and association_proxy_class == BelongsToAssociation
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
association.target = target
|
||||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
end
|
||||
|
||||
def collection_reader_method(reflection, association_proxy_class)
|
||||
define_method(reflection.name) do |*params|
|
||||
force_reload = params.first unless params.empty?
|
||||
association = association_instance_get(reflection.name)
|
||||
|
||||
unless association
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
|
||||
association.reload if force_reload
|
||||
|
||||
association
|
||||
end
|
||||
|
||||
define_method("#{reflection.name.to_s.singularize}_ids") do
|
||||
if send(reflection.name).loaded? || reflection.options[:finder_sql]
|
||||
send(reflection.name).map(&:id)
|
||||
else
|
||||
send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
|
||||
collection_reader_method(reflection, association_proxy_class)
|
||||
|
||||
if writer
|
||||
define_method("#{reflection.name}=") do |new_value|
|
||||
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
|
||||
association = send(reflection.name)
|
||||
association.replace(new_value)
|
||||
association
|
||||
end
|
||||
|
||||
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
||||
ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i)
|
||||
send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def association_constructor_method(constructor, reflection, association_proxy_class)
|
||||
define_method("#{constructor}_#{reflection.name}") do |*params|
|
||||
attributees = params.first unless params.empty?
|
||||
replace_existing = params[1].nil? ? true : params[1]
|
||||
association = association_instance_get(reflection.name)
|
||||
|
||||
unless association
|
||||
association = association_proxy_class.new(self, reflection)
|
||||
association_instance_set(reflection.name, association)
|
||||
end
|
||||
|
||||
if association_proxy_class == HasOneAssociation
|
||||
association.send(constructor, attributees, replace_existing)
|
||||
else
|
||||
association.send(constructor, attributees)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_counter_cache_callbacks(reflection)
|
||||
cache_column = reflection.counter_cache_column
|
||||
|
||||
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.class.increment_counter(cache_column, association.id) unless association.nil?
|
||||
end
|
||||
after_create(method_name)
|
||||
|
||||
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.class.decrement_counter(cache_column, association.id) unless association.nil?
|
||||
end
|
||||
before_destroy(method_name)
|
||||
|
||||
module_eval(
|
||||
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
||||
)
|
||||
end
|
||||
|
||||
def add_touch_callbacks(reflection, touch_attribute)
|
||||
method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
|
||||
if touch_attribute == true
|
||||
association.touch unless association.nil?
|
||||
else
|
||||
association.touch(touch_attribute) unless association.nil?
|
||||
end
|
||||
end
|
||||
after_save(method_name)
|
||||
after_destroy(method_name)
|
||||
end
|
||||
|
||||
def find_with_associations(options = {})
|
||||
catch :invalid_query do
|
||||
join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
|
||||
rows = select_all_rows(options, join_dependency)
|
||||
return join_dependency.instantiate(rows)
|
||||
end
|
||||
[]
|
||||
end
|
||||
|
||||
# Creates before_destroy callback methods that nullify, delete or destroy
|
||||
# has_many associated objects, according to the defined :dependent rule.
|
||||
#
|
||||
# See HasManyAssociation#delete_records. Dependent associations
|
||||
# delete children, otherwise foreign key is set to NULL.
|
||||
#
|
||||
# The +extra_conditions+ parameter, which is not used within the main
|
||||
# Active Record codebase, is meant to allow plugins to define extra
|
||||
# finder conditions.
|
||||
def configure_dependency_for_has_many(reflection, extra_conditions = nil)
|
||||
if reflection.options.include?(:dependent)
|
||||
# Add polymorphic type if the :as option is present
|
||||
dependent_conditions = []
|
||||
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
|
||||
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
|
||||
dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
|
||||
dependent_conditions << extra_conditions if extra_conditions
|
||||
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
|
||||
dependent_conditions = dependent_conditions.gsub('@', '\@')
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
send(reflection.name).each { |o| o.destroy }
|
||||
end
|
||||
before_destroy method_name
|
||||
when :delete_all
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
delete_all_has_many_dependencies(record, # delete_all_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
when :nullify
|
||||
module_eval %Q{
|
||||
before_destroy do |record| # before_destroy do |record|
|
||||
nullify_has_many_dependencies(record, # nullify_has_many_dependencies(record,
|
||||
"#{reflection.name}", # "posts",
|
||||
#{reflection.class_name}, # Post,
|
||||
"#{reflection.primary_key_name}", # "user_id",
|
||||
%@#{dependent_conditions}@) # %@...@) # this is a string literal like %(...)
|
||||
end # end
|
||||
}
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, or :nullify (#{reflection.options[:dependent].inspect})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Creates before_destroy callback methods that nullify, delete or destroy
|
||||
# has_one associated objects, according to the defined :dependent rule.
|
||||
def configure_dependency_for_has_one(reflection)
|
||||
if reflection.options.include?(:dependent)
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "has_one_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.destroy unless association.nil?
|
||||
end
|
||||
before_destroy method_name
|
||||
when :delete
|
||||
method_name = "has_one_dependent_delete_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
# Retrieve the associated object and delete it. The retrieval
|
||||
# is necessary because there may be multiple associated objects
|
||||
# with foreign keys pointing to this object, and we only want
|
||||
# to delete the correct one, not all of them.
|
||||
association = send(reflection.name)
|
||||
association.delete unless association.nil?
|
||||
end
|
||||
before_destroy method_name
|
||||
when :nullify
|
||||
method_name = "has_one_dependent_nullify_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.update_attribute(reflection.primary_key_name, nil) unless association.nil?
|
||||
end
|
||||
before_destroy method_name
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy, :delete or :nullify (#{reflection.options[:dependent].inspect})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def configure_dependency_for_belongs_to(reflection)
|
||||
if reflection.options.include?(:dependent)
|
||||
case reflection.options[:dependent]
|
||||
when :destroy
|
||||
method_name = "belongs_to_dependent_destroy_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.destroy unless association.nil?
|
||||
end
|
||||
after_destroy method_name
|
||||
when :delete
|
||||
method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = send(reflection.name)
|
||||
association.delete unless association.nil?
|
||||
end
|
||||
after_destroy method_name
|
||||
else
|
||||
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{reflection.options[:dependent].inspect})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions)
|
||||
association_class.delete_all(dependent_conditions)
|
||||
end
|
||||
|
||||
def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions)
|
||||
association_class.update_all("#{primary_key_name} = NULL", dependent_conditions)
|
||||
end
|
||||
|
||||
mattr_accessor :valid_keys_for_has_many_association
|
||||
@@valid_keys_for_has_many_association = [
|
||||
:class_name, :table_name, :foreign_key, :primary_key,
|
||||
:dependent,
|
||||
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
|
||||
:as, :through, :source, :source_type,
|
||||
:uniq,
|
||||
:finder_sql, :counter_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
]
|
||||
|
||||
def create_has_many_reflection(association_id, options, &extension)
|
||||
options.assert_valid_keys(valid_keys_for_has_many_association)
|
||||
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
||||
|
||||
create_reflection(:has_many, association_id, options, self)
|
||||
end
|
||||
|
||||
mattr_accessor :valid_keys_for_has_one_association
|
||||
@@valid_keys_for_has_one_association = [
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order,
|
||||
:include, :dependent, :counter_cache, :extend, :as, :readonly,
|
||||
:validate, :primary_key
|
||||
]
|
||||
|
||||
def create_has_one_reflection(association_id, options)
|
||||
options.assert_valid_keys(valid_keys_for_has_one_association)
|
||||
create_reflection(:has_one, association_id, options, self)
|
||||
end
|
||||
|
||||
def create_has_one_through_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
|
||||
)
|
||||
create_reflection(:has_one, association_id, options, self)
|
||||
end
|
||||
|
||||
mattr_accessor :valid_keys_for_belongs_to_association
|
||||
@@valid_keys_for_belongs_to_association = [
|
||||
:class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
|
||||
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
|
||||
:validate, :touch
|
||||
]
|
||||
|
||||
def create_belongs_to_reflection(association_id, options)
|
||||
options.assert_valid_keys(valid_keys_for_belongs_to_association)
|
||||
reflection = create_reflection(:belongs_to, association_id, options, self)
|
||||
|
||||
if options[:polymorphic]
|
||||
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
|
||||
end
|
||||
|
||||
reflection
|
||||
end
|
||||
|
||||
mattr_accessor :valid_keys_for_has_and_belongs_to_many_association
|
||||
@@valid_keys_for_has_and_belongs_to_many_association = [
|
||||
:class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
|
||||
:select, :conditions, :include, :order, :group, :having, :limit, :offset,
|
||||
:uniq,
|
||||
:finder_sql, :counter_sql, :delete_sql, :insert_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
]
|
||||
|
||||
def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
||||
options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association)
|
||||
|
||||
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
||||
|
||||
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
|
||||
|
||||
if reflection.association_foreign_key == reflection.primary_key_name
|
||||
raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
|
||||
end
|
||||
|
||||
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
|
||||
|
||||
reflection
|
||||
end
|
||||
|
||||
def reflect_on_included_associations(associations)
|
||||
[ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
|
||||
end
|
||||
|
||||
def guard_against_unlimitable_reflections(reflections, options)
|
||||
if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections)
|
||||
raise(
|
||||
ConfigurationError,
|
||||
"You can not use offset and limit together with has_many or has_and_belongs_to_many associations"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def select_all_rows(options, join_dependency)
|
||||
connection.select_all(
|
||||
construct_finder_sql_with_included_associations(options, join_dependency),
|
||||
"#{name} Load Including Associations"
|
||||
)
|
||||
end
|
||||
|
||||
def construct_finder_sql_with_included_associations(options, join_dependency)
|
||||
scope = scope(:find)
|
||||
sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
|
||||
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
||||
|
||||
add_joins!(sql, options[:joins], scope)
|
||||
add_conditions!(sql, options[:conditions], scope)
|
||||
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
|
||||
|
||||
add_group!(sql, options[:group], options[:having], scope)
|
||||
add_order!(sql, options[:order], scope)
|
||||
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
|
||||
add_lock!(sql, options, scope)
|
||||
|
||||
return sanitize_sql(sql)
|
||||
end
|
||||
|
||||
def add_limited_ids_condition!(sql, options, join_dependency)
|
||||
unless (id_list = select_limited_ids_list(options, join_dependency)).empty?
|
||||
sql << "#{condition_word(sql)} #{connection.quote_table_name table_name}.#{primary_key} IN (#{id_list}) "
|
||||
else
|
||||
throw :invalid_query
|
||||
end
|
||||
end
|
||||
|
||||
def select_limited_ids_list(options, join_dependency)
|
||||
pk = columns_hash[primary_key]
|
||||
|
||||
connection.select_all(
|
||||
construct_finder_sql_for_association_limiting(options, join_dependency),
|
||||
"#{name} Load IDs For Limited Eager Loading"
|
||||
).collect { |row| connection.quote(row[primary_key], pk) }.join(", ")
|
||||
end
|
||||
|
||||
def construct_finder_sql_for_association_limiting(options, join_dependency)
|
||||
scope = scope(:find)
|
||||
|
||||
# Only join tables referenced in order or conditions since this is particularly slow on the pre-query.
|
||||
tables_from_conditions = conditions_tables(options)
|
||||
tables_from_order = order_tables(options)
|
||||
all_tables = tables_from_conditions + tables_from_order
|
||||
distinct_join_associations = all_tables.uniq.map{|table|
|
||||
join_dependency.joins_for_table_name(table)
|
||||
}.flatten.compact.uniq
|
||||
|
||||
order = options[:order]
|
||||
if scoped_order = (scope && scope[:order])
|
||||
order = order ? "#{order}, #{scoped_order}" : scoped_order
|
||||
end
|
||||
|
||||
is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order)
|
||||
sql = "SELECT "
|
||||
if is_distinct
|
||||
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", order)
|
||||
else
|
||||
sql << primary_key
|
||||
end
|
||||
sql << " FROM #{connection.quote_table_name table_name} "
|
||||
|
||||
if is_distinct
|
||||
sql << distinct_join_associations.collect { |assoc| assoc.association_join }.join
|
||||
add_joins!(sql, options[:joins], scope)
|
||||
end
|
||||
|
||||
add_conditions!(sql, options[:conditions], scope)
|
||||
add_group!(sql, options[:group], options[:having], scope)
|
||||
|
||||
if order && is_distinct
|
||||
connection.add_order_by_for_association_limiting!(sql, :order => order)
|
||||
else
|
||||
add_order!(sql, options[:order], scope)
|
||||
end
|
||||
|
||||
add_limit!(sql, options, scope)
|
||||
|
||||
return sanitize_sql(sql)
|
||||
end
|
||||
|
||||
def tables_in_string(string)
|
||||
return [] if string.blank?
|
||||
string.scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
end
|
||||
|
||||
def tables_in_hash(hash)
|
||||
return [] if hash.blank?
|
||||
tables = hash.map do |key, value|
|
||||
if value.is_a?(Hash)
|
||||
key.to_s
|
||||
else
|
||||
tables_in_string(key) if key.is_a?(String)
|
||||
end
|
||||
end
|
||||
tables.flatten.compact
|
||||
end
|
||||
|
||||
def conditions_tables(options)
|
||||
# look in both sets of conditions
|
||||
conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
|
||||
case cond
|
||||
when nil then all
|
||||
when Array then all << tables_in_string(cond.first)
|
||||
when Hash then all << tables_in_hash(cond)
|
||||
else all << tables_in_string(cond)
|
||||
end
|
||||
end
|
||||
conditions.flatten
|
||||
end
|
||||
|
||||
def order_tables(options)
|
||||
order = [options[:order], scope(:find, :order) ].join(", ")
|
||||
return [] unless order && order.is_a?(String)
|
||||
tables_in_string(order)
|
||||
end
|
||||
|
||||
def selects_tables(options)
|
||||
select = options[:select]
|
||||
return [] unless select && select.is_a?(String)
|
||||
tables_in_string(select)
|
||||
end
|
||||
|
||||
def joined_tables(options)
|
||||
scope = scope(:find)
|
||||
joins = options[:joins]
|
||||
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
|
||||
[table_name] + case merged_joins
|
||||
when Symbol, Hash, Array
|
||||
if array_of_strings?(merged_joins)
|
||||
tables_in_string(merged_joins.join(' '))
|
||||
else
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
|
||||
join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact
|
||||
end
|
||||
else
|
||||
tables_in_string(merged_joins)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks if the conditions reference a table other than the current model table
|
||||
def include_eager_conditions?(options, tables = nil, joined_tables = nil)
|
||||
((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
# Checks if the query order references a table other than the current model's table.
|
||||
def include_eager_order?(options, tables = nil, joined_tables = nil)
|
||||
((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
def include_eager_select?(options, joined_tables = nil)
|
||||
(selects_tables(options) - (joined_tables || joined_tables(options))).any?
|
||||
end
|
||||
|
||||
def references_eager_loaded_tables?(options)
|
||||
joined_tables = joined_tables(options)
|
||||
include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables)
|
||||
end
|
||||
|
||||
def using_limitable_reflections?(reflections)
|
||||
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
|
||||
end
|
||||
|
||||
def column_aliases(join_dependency)
|
||||
join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
|
||||
"#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
|
||||
end
|
||||
|
||||
def add_association_callbacks(association_name, options)
|
||||
callbacks = %w(before_add after_add before_remove after_remove)
|
||||
callbacks.each do |callback_name|
|
||||
full_callback_name = "#{callback_name}_for_#{association_name}"
|
||||
defined_callbacks = options[callback_name.to_sym]
|
||||
if options.has_key?(callback_name.to_sym)
|
||||
class_inheritable_reader full_callback_name.to_sym
|
||||
write_inheritable_attribute(full_callback_name.to_sym, [defined_callbacks].flatten)
|
||||
else
|
||||
write_inheritable_attribute(full_callback_name.to_sym, [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def condition_word(sql)
|
||||
sql =~ /where/i ? " AND " : "WHERE "
|
||||
end
|
||||
|
||||
def create_extension_modules(association_id, block_extension, extensions)
|
||||
if block_extension
|
||||
extension_module_name = "#{self.to_s.demodulize}#{association_id.to_s.camelize}AssociationExtension"
|
||||
|
||||
silence_warnings do
|
||||
self.parent.const_set(extension_module_name, Module.new(&block_extension))
|
||||
end
|
||||
Array(extensions).push("#{self.parent}::#{extension_module_name}".constantize)
|
||||
else
|
||||
Array(extensions)
|
||||
end
|
||||
end
|
||||
|
||||
class JoinDependency # :nodoc:
|
||||
attr_reader :joins, :reflections, :table_aliases
|
||||
|
||||
def initialize(base, associations, joins)
|
||||
@joins = [JoinBase.new(base, joins)]
|
||||
@associations = associations
|
||||
@reflections = []
|
||||
@base_records_hash = {}
|
||||
@base_records_in_order = []
|
||||
@table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
|
||||
@table_aliases[base.table_name] = 1
|
||||
build(associations)
|
||||
end
|
||||
|
||||
def join_associations
|
||||
@joins[1..-1].to_a
|
||||
end
|
||||
|
||||
def join_base
|
||||
@joins[0]
|
||||
end
|
||||
|
||||
def instantiate(rows)
|
||||
rows.each_with_index do |row, i|
|
||||
primary_id = join_base.record_id(row)
|
||||
unless @base_records_hash[primary_id]
|
||||
@base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
|
||||
end
|
||||
construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
|
||||
end
|
||||
remove_duplicate_results!(join_base.active_record, @base_records_in_order, @associations)
|
||||
return @base_records_in_order
|
||||
end
|
||||
|
||||
def remove_duplicate_results!(base, records, associations)
|
||||
case associations
|
||||
when Symbol, String
|
||||
reflection = base.reflections[associations]
|
||||
if reflection && [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
records.each { |record| record.send(reflection.name).target.uniq! }
|
||||
end
|
||||
when Array
|
||||
associations.each do |association|
|
||||
remove_duplicate_results!(base, records, association)
|
||||
end
|
||||
when Hash
|
||||
associations.keys.each do |name|
|
||||
reflection = base.reflections[name]
|
||||
is_collection = [:has_many, :has_and_belongs_to_many].include?(reflection.macro)
|
||||
|
||||
parent_records = records.map do |record|
|
||||
descendant = record.send(reflection.name)
|
||||
next unless descendant
|
||||
descendant.target.uniq! if is_collection
|
||||
descendant
|
||||
end.flatten.compact
|
||||
|
||||
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def join_for_table_name(table_name)
|
||||
join = (@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first) rescue nil
|
||||
return join unless join.nil?
|
||||
@joins.select{|j|j.is_a?(JoinAssociation) && j.aliased_join_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
|
||||
end
|
||||
|
||||
def joins_for_table_name(table_name)
|
||||
join = join_for_table_name(table_name)
|
||||
result = nil
|
||||
if join && join.is_a?(JoinAssociation)
|
||||
result = [join]
|
||||
if join.parent && join.parent.is_a?(JoinAssociation)
|
||||
result = joins_for_table_name(join.parent.aliased_table_name) +
|
||||
result
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
protected
|
||||
def build(associations, parent = nil)
|
||||
parent ||= @joins.last
|
||||
case associations
|
||||
when Symbol, String
|
||||
reflection = parent.reflections[associations.to_s.intern] or
|
||||
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
|
||||
@reflections << reflection
|
||||
@joins << build_join_association(reflection, parent)
|
||||
when Array
|
||||
associations.each do |association|
|
||||
build(association, parent)
|
||||
end
|
||||
when Hash
|
||||
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
||||
build(name, parent)
|
||||
build(associations[name])
|
||||
end
|
||||
else
|
||||
raise ConfigurationError, associations.inspect
|
||||
end
|
||||
end
|
||||
|
||||
# overridden in InnerJoinDependency subclass
|
||||
def build_join_association(reflection, parent)
|
||||
JoinAssociation.new(reflection, self, parent)
|
||||
end
|
||||
|
||||
def construct(parent, associations, joins, row)
|
||||
case associations
|
||||
when Symbol, String
|
||||
join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
|
||||
raise(ConfigurationError, "No such association") if join.nil?
|
||||
|
||||
joins.delete(join)
|
||||
construct_association(parent, join, row)
|
||||
when Array
|
||||
associations.each do |association|
|
||||
construct(parent, association, joins, row)
|
||||
end
|
||||
when Hash
|
||||
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
||||
join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
|
||||
raise(ConfigurationError, "No such association") if join.nil?
|
||||
|
||||
association = construct_association(parent, join, row)
|
||||
joins.delete(join)
|
||||
construct(association, associations[name], joins, row) if association
|
||||
end
|
||||
else
|
||||
raise ConfigurationError, associations.inspect
|
||||
end
|
||||
end
|
||||
|
||||
def construct_association(record, join, row)
|
||||
case join.reflection.macro
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
collection = record.send(join.reflection.name)
|
||||
collection.loaded
|
||||
|
||||
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
collection.target.push(association)
|
||||
when :has_one
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s
|
||||
return if record.instance_variable_defined?("@#{join.reflection.name}")
|
||||
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
when :belongs_to
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
|
||||
association = join.instantiate(row)
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
else
|
||||
raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
|
||||
end
|
||||
return association
|
||||
end
|
||||
|
||||
class JoinBase # :nodoc:
|
||||
attr_reader :active_record, :table_joins
|
||||
delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
|
||||
|
||||
def initialize(active_record, joins = nil)
|
||||
@active_record = active_record
|
||||
@cached_record = {}
|
||||
@table_joins = joins
|
||||
end
|
||||
|
||||
def aliased_prefix
|
||||
"t0"
|
||||
end
|
||||
|
||||
def aliased_primary_key
|
||||
"#{aliased_prefix}_r0"
|
||||
end
|
||||
|
||||
def aliased_table_name
|
||||
active_record.table_name
|
||||
end
|
||||
|
||||
def column_names_with_alias
|
||||
unless defined?(@column_names_with_alias)
|
||||
@column_names_with_alias = []
|
||||
|
||||
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
|
||||
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
|
||||
end
|
||||
end
|
||||
|
||||
@column_names_with_alias
|
||||
end
|
||||
|
||||
def extract_record(row)
|
||||
column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record}
|
||||
end
|
||||
|
||||
def record_id(row)
|
||||
row[aliased_primary_key]
|
||||
end
|
||||
|
||||
def instantiate(row)
|
||||
@cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
|
||||
end
|
||||
end
|
||||
|
||||
class JoinAssociation < JoinBase # :nodoc:
|
||||
attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name
|
||||
delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
|
||||
|
||||
def initialize(reflection, join_dependency, parent = nil)
|
||||
reflection.check_validity!
|
||||
if reflection.options[:polymorphic]
|
||||
raise EagerLoadPolymorphicError.new(reflection)
|
||||
end
|
||||
|
||||
super(reflection.klass)
|
||||
@join_dependency = join_dependency
|
||||
@parent = parent
|
||||
@reflection = reflection
|
||||
@aliased_prefix = "t#{ join_dependency.joins.size }"
|
||||
@parent_table_name = parent.active_record.table_name
|
||||
@aliased_table_name = aliased_table_name_for(table_name)
|
||||
|
||||
if reflection.macro == :has_and_belongs_to_many
|
||||
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
|
||||
end
|
||||
|
||||
if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
|
||||
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
|
||||
end
|
||||
end
|
||||
|
||||
def association_join
|
||||
connection = reflection.active_record.connection
|
||||
join = case reflection.macro
|
||||
when :has_and_belongs_to_many
|
||||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_alias_for(options[:join_table], aliased_join_table_name),
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
options[:foreign_key] || reflection.active_record.to_s.foreign_key,
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
reflection.active_record.primary_key] +
|
||||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
klass.primary_key,
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
options[:association_foreign_key] || klass.to_s.foreign_key
|
||||
]
|
||||
when :has_many, :has_one
|
||||
case
|
||||
when reflection.options[:through]
|
||||
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
|
||||
|
||||
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
|
||||
first_key = second_key = as_extra = nil
|
||||
|
||||
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
||||
jt_foreign_key = through_reflection.options[:as].to_s + '_id'
|
||||
jt_as_extra = " AND %s.%s = %s" % [
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(through_reflection.options[:as].to_s + '_type'),
|
||||
klass.quote_value(parent.active_record.base_class.name)
|
||||
]
|
||||
else
|
||||
jt_foreign_key = through_reflection.primary_key_name
|
||||
end
|
||||
|
||||
case source_reflection.macro
|
||||
when :has_many
|
||||
if source_reflection.options[:as]
|
||||
first_key = "#{source_reflection.options[:as]}_id"
|
||||
second_key = options[:foreign_key] || primary_key
|
||||
as_extra = " AND %s.%s = %s" % [
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
connection.quote_column_name("#{source_reflection.options[:as]}_type"),
|
||||
klass.quote_value(source_reflection.active_record.base_class.name)
|
||||
]
|
||||
else
|
||||
first_key = through_reflection.klass.base_class.to_s.foreign_key
|
||||
second_key = options[:foreign_key] || primary_key
|
||||
end
|
||||
|
||||
unless through_reflection.klass.descends_from_active_record?
|
||||
jt_sti_extra = " AND %s.%s = %s" % [
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(through_reflection.active_record.inheritance_column),
|
||||
through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
|
||||
end
|
||||
when :belongs_to
|
||||
first_key = primary_key
|
||||
if reflection.options[:source_type]
|
||||
second_key = source_reflection.association_foreign_key
|
||||
jt_source_extra = " AND %s.%s = %s" % [
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(reflection.source_reflection.options[:foreign_type]),
|
||||
klass.quote_value(reflection.options[:source_type])
|
||||
]
|
||||
else
|
||||
second_key = source_reflection.primary_key_name
|
||||
end
|
||||
end
|
||||
|
||||
" #{join_type} %s ON (%s.%s = %s.%s%s%s%s) " % [
|
||||
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
connection.quote_column_name(parent.primary_key),
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(jt_foreign_key),
|
||||
jt_as_extra, jt_source_extra, jt_sti_extra
|
||||
] +
|
||||
" #{join_type} %s ON (%s.%s = %s.%s%s) " % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
connection.quote_column_name(first_key),
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(second_key),
|
||||
as_extra
|
||||
]
|
||||
|
||||
when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro)
|
||||
" #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %s" % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
"#{reflection.options[:as]}_id",
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
parent.primary_key,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
"#{reflection.options[:as]}_type",
|
||||
klass.quote_value(parent.active_record.base_class.name)
|
||||
]
|
||||
else
|
||||
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
|
||||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_name_and_alias,
|
||||
aliased_table_name,
|
||||
foreign_key,
|
||||
parent.aliased_table_name,
|
||||
reflection.options[:primary_key] || parent.primary_key
|
||||
]
|
||||
end
|
||||
when :belongs_to
|
||||
" #{join_type} %s ON %s.%s = %s.%s " % [
|
||||
table_name_and_alias,
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
reflection.klass.primary_key,
|
||||
connection.quote_table_name(parent.aliased_table_name),
|
||||
options[:foreign_key] || reflection.primary_key_name
|
||||
]
|
||||
else
|
||||
""
|
||||
end || ''
|
||||
join << %(AND %s) % [
|
||||
klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
|
||||
|
||||
[through_reflection, reflection].each do |ref|
|
||||
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name))} " if ref && ref.options[:conditions]
|
||||
end
|
||||
|
||||
join
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def aliased_table_name_for(name, suffix = nil)
|
||||
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
|
||||
@join_dependency.table_aliases[name] += 1
|
||||
end
|
||||
|
||||
unless @join_dependency.table_aliases[name].zero?
|
||||
# if the table name has been used, then use an alias
|
||||
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
||||
table_index = @join_dependency.table_aliases[name]
|
||||
@join_dependency.table_aliases[name] += 1
|
||||
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
|
||||
else
|
||||
@join_dependency.table_aliases[name] += 1
|
||||
end
|
||||
|
||||
name
|
||||
end
|
||||
|
||||
def pluralize(table_name)
|
||||
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
|
||||
end
|
||||
|
||||
def table_alias_for(table_name, table_alias)
|
||||
"#{reflection.active_record.connection.quote_table_name(table_name)} #{table_alias if table_name != table_alias}".strip
|
||||
end
|
||||
|
||||
def table_name_and_alias
|
||||
table_alias_for table_name, @aliased_table_name
|
||||
end
|
||||
|
||||
def interpolate_sql(sql)
|
||||
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
||||
end
|
||||
|
||||
private
|
||||
def join_type
|
||||
"LEFT OUTER JOIN"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class InnerJoinDependency < JoinDependency # :nodoc:
|
||||
protected
|
||||
def build_join_association(reflection, parent)
|
||||
InnerJoinAssociation.new(reflection, self, parent)
|
||||
end
|
||||
|
||||
class InnerJoinAssociation < JoinAssociation
|
||||
private
|
||||
def join_type
|
||||
"INNER JOIN"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,475 +0,0 @@
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# AssociationCollection is an abstract class that provides common stuff to
|
||||
# ease the implementation of association proxies that represent
|
||||
# collections. See the class hierarchy in AssociationProxy.
|
||||
#
|
||||
# You need to be careful with assumptions regarding the target: The proxy
|
||||
# does not fetch records from the database until it needs them, but new
|
||||
# ones created with +build+ are added to the target. So, the target may be
|
||||
# non-empty and still lack children waiting to be read from the database.
|
||||
# If you look directly to the database you cannot assume that's the entire
|
||||
# collection because new records may have beed added to the target, etc.
|
||||
#
|
||||
# If you need to work on all current children, new and existing records,
|
||||
# +load_target+ and the +loaded+ flag are your friends.
|
||||
class AssociationCollection < AssociationProxy #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
construct_sql
|
||||
end
|
||||
|
||||
def find(*args)
|
||||
options = args.extract_options!
|
||||
|
||||
# If using a custom finder_sql, scan the entire collection.
|
||||
if @reflection.options[:finder_sql]
|
||||
expects_array = args.first.kind_of?(Array)
|
||||
ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
|
||||
|
||||
if ids.size == 1
|
||||
id = ids.first
|
||||
record = load_target.detect { |r| id == r.id }
|
||||
expects_array ? [ record ] : record
|
||||
else
|
||||
load_target.select { |r| ids.include?(r.id) }
|
||||
end
|
||||
else
|
||||
conditions = "#{@finder_sql}"
|
||||
if sanitized_conditions = sanitize_sql(options[:conditions])
|
||||
conditions << " AND (#{sanitized_conditions})"
|
||||
end
|
||||
|
||||
options[:conditions] = conditions
|
||||
|
||||
if options[:order] && @reflection.options[:order]
|
||||
options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
|
||||
elsif @reflection.options[:order]
|
||||
options[:order] = @reflection.options[:order]
|
||||
end
|
||||
|
||||
# Build options specific to association
|
||||
construct_find_options!(options)
|
||||
|
||||
merge_options_from_reflection!(options)
|
||||
|
||||
# Pass through args exactly as we received them.
|
||||
args << options
|
||||
@reflection.klass.find(*args)
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the first one using SQL if possible.
|
||||
def first(*args)
|
||||
if fetch_first_or_last_using_find?(args)
|
||||
find(:first, *args)
|
||||
else
|
||||
load_target unless loaded?
|
||||
@target.first(*args)
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the last one using SQL if possible.
|
||||
def last(*args)
|
||||
if fetch_first_or_last_using_find?(args)
|
||||
find(:last, *args)
|
||||
else
|
||||
load_target unless loaded?
|
||||
@target.last(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def to_ary
|
||||
load_target
|
||||
if @target.is_a?(Array)
|
||||
@target.to_ary
|
||||
else
|
||||
Array(@target)
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
reset_target!
|
||||
@loaded = false
|
||||
end
|
||||
|
||||
def build(attributes = {}, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| build(attr, &block) }
|
||||
else
|
||||
build_record(attributes) do |record|
|
||||
block.call(record) if block_given?
|
||||
set_belongs_to_association_for(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
||||
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
||||
def <<(*records)
|
||||
result = true
|
||||
load_target if @owner.new_record?
|
||||
|
||||
transaction do
|
||||
flatten_deeper(records).each do |record|
|
||||
raise_on_type_mismatch(record)
|
||||
add_record_to_target_with_callbacks(record) do |r|
|
||||
result &&= insert_record(record) unless @owner.new_record?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
result && self
|
||||
end
|
||||
|
||||
alias_method :push, :<<
|
||||
alias_method :concat, :<<
|
||||
|
||||
# Starts a transaction in the association class's database connection.
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.find(:first).books.transaction do
|
||||
# # same effect as calling Book.transaction
|
||||
# end
|
||||
def transaction(*args)
|
||||
@reflection.klass.transaction(*args) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Remove all records from this association
|
||||
#
|
||||
# See delete for more info.
|
||||
def delete_all
|
||||
load_target
|
||||
delete(@target)
|
||||
reset_target!
|
||||
end
|
||||
|
||||
# Calculate sum using SQL, not Enumerable
|
||||
def sum(*args)
|
||||
if block_given?
|
||||
calculate(:sum, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
calculate(:sum, *args)
|
||||
end
|
||||
end
|
||||
|
||||
# Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
|
||||
# be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
|
||||
# descendant's +construct_sql+ method will have set :counter_sql automatically.
|
||||
# Otherwise, construct options and pass them with scope to the target class's +count+.
|
||||
def count(*args)
|
||||
if @reflection.options[:counter_sql]
|
||||
@reflection.klass.count_by_sql(@counter_sql)
|
||||
else
|
||||
column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
|
||||
if @reflection.options[:uniq]
|
||||
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
|
||||
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
|
||||
options.merge!(:distinct => true)
|
||||
end
|
||||
|
||||
value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
|
||||
|
||||
limit = @reflection.options[:limit]
|
||||
offset = @reflection.options[:offset]
|
||||
|
||||
if limit || offset
|
||||
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Removes +records+ from this association calling +before_remove+ and
|
||||
# +after_remove+ callbacks.
|
||||
#
|
||||
# This method is abstract in the sense that +delete_records+ has to be
|
||||
# provided by descendants. Note this method does not imply the records
|
||||
# are actually removed from the database, that depends precisely on
|
||||
# +delete_records+. They are in any case removed from the collection.
|
||||
def delete(*records)
|
||||
remove_records(records) do |records, old_records|
|
||||
delete_records(old_records) if old_records.any?
|
||||
records.each { |record| @target.delete(record) }
|
||||
end
|
||||
end
|
||||
|
||||
# Destroy +records+ and remove them from this association calling
|
||||
# +before_remove+ and +after_remove+ callbacks.
|
||||
#
|
||||
# Note that this method will _always_ remove records from the database
|
||||
# ignoring the +:dependent+ option.
|
||||
def destroy(*records)
|
||||
records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
|
||||
remove_records(records) do |records, old_records|
|
||||
old_records.each { |record| record.destroy }
|
||||
end
|
||||
|
||||
load_target
|
||||
end
|
||||
|
||||
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
||||
def clear
|
||||
return self if length.zero? # forces load_target if it hasn't happened already
|
||||
|
||||
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
|
||||
destroy_all
|
||||
else
|
||||
delete_all
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Destory all the records from this association.
|
||||
#
|
||||
# See destroy for more info.
|
||||
def destroy_all
|
||||
load_target
|
||||
destroy(@target)
|
||||
reset_target!
|
||||
end
|
||||
|
||||
def create(attrs = {})
|
||||
if attrs.is_a?(Array)
|
||||
attrs.collect { |attr| create(attr) }
|
||||
else
|
||||
create_record(attrs) do |record|
|
||||
yield(record) if block_given?
|
||||
record.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create!(attrs = {})
|
||||
create_record(attrs) do |record|
|
||||
yield(record) if block_given?
|
||||
record.save!
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection by executing a SELECT COUNT(*)
|
||||
# query if the collection hasn't been loaded, and calling
|
||||
# <tt>collection.size</tt> if it has.
|
||||
#
|
||||
# If the collection has been already loaded +size+ and +length+ are
|
||||
# equivalent. If not and you are going to need the records anyway
|
||||
# +length+ will take one less query. Otherwise +size+ is more efficient.
|
||||
#
|
||||
# This method is abstract in the sense that it relies on
|
||||
# +count_records+, which is a method descendants have to provide.
|
||||
def size
|
||||
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
||||
@target.size
|
||||
elsif !loaded? && @reflection.options[:group]
|
||||
load_target.size
|
||||
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
||||
unsaved_records = @target.select { |r| r.new_record? }
|
||||
unsaved_records.size + count_records
|
||||
else
|
||||
count_records
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection calling +size+ on the target.
|
||||
#
|
||||
# If the collection has been already loaded +length+ and +size+ are
|
||||
# equivalent. If not and you are going to need the records anyway this
|
||||
# method will take one less query. Otherwise +size+ is more efficient.
|
||||
def length
|
||||
load_target.size
|
||||
end
|
||||
|
||||
# Equivalent to <tt>collection.size.zero?</tt>. If the collection has
|
||||
# not been already loaded and you are going to fetch the records anyway
|
||||
# it is better to check <tt>collection.length.zero?</tt>.
|
||||
def empty?
|
||||
size.zero?
|
||||
end
|
||||
|
||||
def any?
|
||||
if block_given?
|
||||
method_missing(:any?) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
!empty?
|
||||
end
|
||||
end
|
||||
|
||||
def uniq(collection = self)
|
||||
seen = Set.new
|
||||
collection.inject([]) do |kept, record|
|
||||
unless seen.include?(record.id)
|
||||
kept << record
|
||||
seen << record.id
|
||||
end
|
||||
kept
|
||||
end
|
||||
end
|
||||
|
||||
# Replace this collection with +other_array+
|
||||
# This will perform a diff and delete/add only records that have changed.
|
||||
def replace(other_array)
|
||||
other_array.each { |val| raise_on_type_mismatch(val) }
|
||||
|
||||
load_target
|
||||
other = other_array.size < 100 ? other_array : other_array.to_set
|
||||
current = @target.size < 100 ? @target : @target.to_set
|
||||
|
||||
transaction do
|
||||
delete(@target.select { |v| !other.include?(v) })
|
||||
concat(other_array.select { |v| !current.include?(v) })
|
||||
end
|
||||
end
|
||||
|
||||
def include?(record)
|
||||
return false unless record.is_a?(@reflection.klass)
|
||||
load_target if @reflection.options[:finder_sql] && !loaded?
|
||||
return @target.include?(record) if loaded?
|
||||
exists?(record)
|
||||
end
|
||||
|
||||
def proxy_respond_to?(method, include_private = false)
|
||||
super || @reflection.klass.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
protected
|
||||
def construct_find_options!(options)
|
||||
end
|
||||
|
||||
def load_target
|
||||
if !@owner.new_record? || foreign_key_present
|
||||
begin
|
||||
if !loaded?
|
||||
if @target.is_a?(Array) && @target.any?
|
||||
@target = find_target + @target.find_all {|t| t.new_record? }
|
||||
else
|
||||
@target = find_target
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
reset
|
||||
end
|
||||
end
|
||||
|
||||
loaded if target
|
||||
target
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
||||
if block_given?
|
||||
super { |*block_args| yield(*block_args) }
|
||||
else
|
||||
super
|
||||
end
|
||||
elsif @reflection.klass.scopes.include?(method)
|
||||
@reflection.klass.scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope(construct_scope) do
|
||||
if block_given?
|
||||
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
@reflection.klass.send(method, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# overloaded in derived Association classes to provide useful scoping depending on association type.
|
||||
def construct_scope
|
||||
{}
|
||||
end
|
||||
|
||||
def reset_target!
|
||||
@target = Array.new
|
||||
end
|
||||
|
||||
def find_target
|
||||
records =
|
||||
if @reflection.options[:finder_sql]
|
||||
@reflection.klass.find_by_sql(@finder_sql)
|
||||
else
|
||||
find(:all)
|
||||
end
|
||||
|
||||
@reflection.options[:uniq] ? uniq(records) : records
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
ensure_owner_is_not_new
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
|
||||
@reflection.build_association(attrs)
|
||||
end
|
||||
if block_given?
|
||||
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
add_record_to_target_with_callbacks(record)
|
||||
end
|
||||
end
|
||||
|
||||
def build_record(attrs)
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
record = @reflection.build_association(attrs)
|
||||
if block_given?
|
||||
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
add_record_to_target_with_callbacks(record)
|
||||
end
|
||||
end
|
||||
|
||||
def add_record_to_target_with_callbacks(record)
|
||||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
record
|
||||
end
|
||||
|
||||
def remove_records(*records)
|
||||
records = flatten_deeper(records)
|
||||
records.each { |record| raise_on_type_mismatch(record) }
|
||||
|
||||
transaction do
|
||||
records.each { |record| callback(:before_remove, record) }
|
||||
old_records = records.reject { |r| r.new_record? }
|
||||
yield(records, old_records)
|
||||
records.each { |record| callback(:after_remove, record) }
|
||||
end
|
||||
end
|
||||
|
||||
def callback(method, record)
|
||||
callbacks_for(method).each do |callback|
|
||||
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
|
||||
end
|
||||
end
|
||||
|
||||
def callbacks_for(callback_name)
|
||||
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
||||
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
||||
end
|
||||
|
||||
def ensure_owner_is_not_new
|
||||
if @owner.new_record?
|
||||
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_first_or_last_using_find?(args)
|
||||
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
|
||||
@target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,278 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# This is the root class of all association proxies:
|
||||
#
|
||||
# AssociationProxy
|
||||
# BelongsToAssociation
|
||||
# HasOneAssociation
|
||||
# BelongsToPolymorphicAssociation
|
||||
# AssociationCollection
|
||||
# HasAndBelongsToManyAssociation
|
||||
# HasManyAssociation
|
||||
# HasManyThroughAssociation
|
||||
# HasOneThroughAssociation
|
||||
#
|
||||
# Association proxies in Active Record are middlemen between the object that
|
||||
# holds the association, known as the <tt>@owner</tt>, and the actual associated
|
||||
# object, known as the <tt>@target</tt>. The kind of association any proxy is
|
||||
# about is available in <tt>@reflection</tt>. That's an instance of the class
|
||||
# ActiveRecord::Reflection::AssociationReflection.
|
||||
#
|
||||
# For example, given
|
||||
#
|
||||
# class Blog < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# end
|
||||
#
|
||||
# blog = Blog.find(:first)
|
||||
#
|
||||
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
|
||||
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
|
||||
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
|
||||
#
|
||||
# This class has most of the basic instance methods removed, and delegates
|
||||
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
|
||||
# corner case, it even removes the +class+ method and that's why you get
|
||||
#
|
||||
# blog.posts.class # => Array
|
||||
#
|
||||
# though the object behind <tt>blog.posts</tt> is not an Array, but an
|
||||
# ActiveRecord::Associations::HasManyAssociation.
|
||||
#
|
||||
# The <tt>@target</tt> object is not \loaded until needed. For example,
|
||||
#
|
||||
# blog.posts.count
|
||||
#
|
||||
# is computed directly through SQL and does not trigger by itself the
|
||||
# instantiation of the actual post records.
|
||||
class AssociationProxy #:nodoc:
|
||||
alias_method :proxy_respond_to?, :respond_to?
|
||||
alias_method :proxy_extend, :extend
|
||||
delegate :to_param, :to => :proxy_target
|
||||
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
|
||||
|
||||
def initialize(owner, reflection)
|
||||
@owner, @reflection = owner, reflection
|
||||
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
||||
reset
|
||||
end
|
||||
|
||||
# Returns the owner of the proxy.
|
||||
def proxy_owner
|
||||
@owner
|
||||
end
|
||||
|
||||
# Returns the reflection object that represents the association handled
|
||||
# by the proxy.
|
||||
def proxy_reflection
|
||||
@reflection
|
||||
end
|
||||
|
||||
# Returns the \target of the proxy, same as +target+.
|
||||
def proxy_target
|
||||
@target
|
||||
end
|
||||
|
||||
# Does the proxy or its \target respond to +symbol+?
|
||||
def respond_to?(*args)
|
||||
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
||||
end
|
||||
|
||||
# Forwards <tt>===</tt> explicitly to the \target because the instance method
|
||||
# removal above doesn't catch it. Loads the \target if needed.
|
||||
def ===(other)
|
||||
load_target
|
||||
other === @target
|
||||
end
|
||||
|
||||
# Returns the name of the table of the related class:
|
||||
#
|
||||
# post.comments.aliased_table_name # => "comments"
|
||||
#
|
||||
def aliased_table_name
|
||||
@reflection.klass.table_name
|
||||
end
|
||||
|
||||
# Returns the SQL string that corresponds to the <tt>:conditions</tt>
|
||||
# option of the macro, if given, or +nil+ otherwise.
|
||||
def conditions
|
||||
@conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
|
||||
end
|
||||
alias :sql_conditions :conditions
|
||||
|
||||
# Resets the \loaded flag to +false+ and sets the \target to +nil+.
|
||||
def reset
|
||||
@loaded = false
|
||||
@target = nil
|
||||
end
|
||||
|
||||
# Reloads the \target and returns +self+ on success.
|
||||
def reload
|
||||
reset
|
||||
load_target
|
||||
self unless @target.nil?
|
||||
end
|
||||
|
||||
# Has the \target been already \loaded?
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
# Asserts the \target has been loaded setting the \loaded flag to +true+.
|
||||
def loaded
|
||||
@loaded = true
|
||||
end
|
||||
|
||||
# Returns the target of this proxy, same as +proxy_target+.
|
||||
def target
|
||||
@target
|
||||
end
|
||||
|
||||
# Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
|
||||
def target=(target)
|
||||
@target = target
|
||||
loaded
|
||||
end
|
||||
|
||||
# Forwards the call to the target. Loads the \target if needed.
|
||||
def inspect
|
||||
load_target
|
||||
@target.inspect
|
||||
end
|
||||
|
||||
def send(method, *args)
|
||||
if proxy_respond_to?(method)
|
||||
super
|
||||
else
|
||||
load_target
|
||||
@target.send(method, *args)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Does the association have a <tt>:dependent</tt> option?
|
||||
def dependent?
|
||||
@reflection.options[:dependent]
|
||||
end
|
||||
|
||||
# Returns a string with the IDs of +records+ joined with a comma, quoted
|
||||
# if needed. The result is ready to be inserted into a SQL IN clause.
|
||||
#
|
||||
# quoted_record_ids(records) # => "23,56,58,67"
|
||||
#
|
||||
def quoted_record_ids(records)
|
||||
records.map { |record| record.quoted_id }.join(',')
|
||||
end
|
||||
|
||||
def interpolate_sql(sql, record = nil)
|
||||
@owner.send(:interpolate_sql, sql, record)
|
||||
end
|
||||
|
||||
# Forwards the call to the reflection class.
|
||||
def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
|
||||
@reflection.klass.send(:sanitize_sql, sql, table_name)
|
||||
end
|
||||
|
||||
# Assigns the ID of the owner to the corresponding foreign key in +record+.
|
||||
# If the association is polymorphic the type of the owner is also set.
|
||||
def set_belongs_to_association_for(record)
|
||||
if @reflection.options[:as]
|
||||
record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
|
||||
record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
|
||||
else
|
||||
unless @owner.new_record?
|
||||
primary_key = @reflection.options[:primary_key] || :id
|
||||
record[@reflection.primary_key_name] = @owner.send(primary_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Merges into +options+ the ones coming from the reflection.
|
||||
def merge_options_from_reflection!(options)
|
||||
options.reverse_merge!(
|
||||
:group => @reflection.options[:group],
|
||||
:having => @reflection.options[:having],
|
||||
:limit => @reflection.options[:limit],
|
||||
:offset => @reflection.options[:offset],
|
||||
:joins => @reflection.options[:joins],
|
||||
:include => @reflection.options[:include],
|
||||
:select => @reflection.options[:select],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
end
|
||||
|
||||
# Forwards +with_scope+ to the reflection.
|
||||
def with_scope(*args, &block)
|
||||
@reflection.klass.send :with_scope, *args, &block
|
||||
end
|
||||
|
||||
private
|
||||
# Forwards any missing method call to the \target.
|
||||
def method_missing(method, *args)
|
||||
if load_target
|
||||
if @target.respond_to?(method)
|
||||
if block_given?
|
||||
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
@target.send(method, *args)
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Loads the \target if needed and returns it.
|
||||
#
|
||||
# This method is abstract in the sense that it relies on +find_target+,
|
||||
# which is expected to be provided by descendants.
|
||||
#
|
||||
# If the \target is already \loaded it is just returned. Thus, you can call
|
||||
# +load_target+ unconditionally to get the \target.
|
||||
#
|
||||
# ActiveRecord::RecordNotFound is rescued within the method, and it is
|
||||
# not reraised. The proxy is \reset and +nil+ is the return value.
|
||||
def load_target
|
||||
return nil unless defined?(@loaded)
|
||||
|
||||
if !loaded? and (!@owner.new_record? || foreign_key_present)
|
||||
@target = find_target
|
||||
end
|
||||
|
||||
@loaded = true
|
||||
@target
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
reset
|
||||
end
|
||||
|
||||
# Can be overwritten by associations that might have the foreign key
|
||||
# available for an association without having the object itself (and
|
||||
# still being a new record). Currently, only +belongs_to+ presents
|
||||
# this scenario (both vanilla and polymorphic).
|
||||
def foreign_key_present
|
||||
false
|
||||
end
|
||||
|
||||
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
|
||||
# the kind of the class of the associated objects. Meant to be used as
|
||||
# a sanity check when you are about to assign an associated record.
|
||||
def raise_on_type_mismatch(record)
|
||||
unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
|
||||
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
|
||||
raise ActiveRecord::AssociationTypeMismatch, message
|
||||
end
|
||||
end
|
||||
|
||||
# Array#flatten has problems with recursive arrays. Going one level
|
||||
# deeper solves the majority of the problems.
|
||||
def flatten_deeper(array)
|
||||
array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
|
||||
end
|
||||
|
||||
# Returns the ID of the owner, quoted if needed.
|
||||
def owner_quoted_id
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,76 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class BelongsToAssociation < AssociationProxy #:nodoc:
|
||||
def create(attributes = {})
|
||||
replace(@reflection.create_association(attributes))
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
replace(@reflection.build_association(attributes))
|
||||
end
|
||||
|
||||
def replace(record)
|
||||
counter_cache_name = @reflection.counter_cache_column
|
||||
|
||||
if record.nil?
|
||||
if counter_cache_name && !@owner.new_record?
|
||||
@reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
|
||||
end
|
||||
|
||||
@target = @owner[@reflection.primary_key_name] = nil
|
||||
else
|
||||
raise_on_type_mismatch(record)
|
||||
|
||||
if counter_cache_name && !@owner.new_record?
|
||||
@reflection.klass.increment_counter(counter_cache_name, record.id)
|
||||
@reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
|
||||
end
|
||||
|
||||
@target = (AssociationProxy === record ? record.target : record)
|
||||
@owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
|
||||
@updated = true
|
||||
end
|
||||
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
||||
def updated?
|
||||
@updated
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
find_method = if @reflection.options[:primary_key]
|
||||
"find_by_#{@reflection.options[:primary_key]}"
|
||||
else
|
||||
"find"
|
||||
end
|
||||
@reflection.klass.send(find_method,
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
) if @owner[@reflection.primary_key_name]
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
!@owner[@reflection.primary_key_name].nil?
|
||||
end
|
||||
|
||||
def record_id(record)
|
||||
record.send(@reflection.options[:primary_key] || :id)
|
||||
end
|
||||
|
||||
def previous_record_id
|
||||
@previous_record_id ||= if @reflection.options[:primary_key]
|
||||
previous_record = @owner.send(@reflection.name)
|
||||
previous_record.nil? ? nil : previous_record.id
|
||||
else
|
||||
@owner[@reflection.primary_key_name]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
|
||||
def replace(record)
|
||||
if record.nil?
|
||||
@target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
|
||||
else
|
||||
@target = (AssociationProxy === record ? record.target : record)
|
||||
|
||||
@owner[@reflection.primary_key_name] = record_id(record)
|
||||
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
|
||||
|
||||
@updated = true
|
||||
end
|
||||
|
||||
loaded
|
||||
record
|
||||
end
|
||||
|
||||
def updated?
|
||||
@updated
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
return nil if association_class.nil?
|
||||
|
||||
if @reflection.options[:conditions]
|
||||
association_class.find(
|
||||
@owner[@reflection.primary_key_name],
|
||||
:select => @reflection.options[:select],
|
||||
:conditions => conditions,
|
||||
:include => @reflection.options[:include]
|
||||
)
|
||||
else
|
||||
association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
|
||||
end
|
||||
end
|
||||
|
||||
def foreign_key_present
|
||||
!@owner[@reflection.primary_key_name].nil?
|
||||
end
|
||||
|
||||
def record_id(record)
|
||||
record.send(@reflection.options[:primary_key] || :id)
|
||||
end
|
||||
|
||||
def association_class
|
||||
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,143 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
@primary_key_list = {}
|
||||
end
|
||||
|
||||
def create(attributes = {})
|
||||
create_record(attributes) { |record| insert_record(record) }
|
||||
end
|
||||
|
||||
def create!(attributes = {})
|
||||
create_record(attributes) { |record| insert_record(record, true) }
|
||||
end
|
||||
|
||||
def columns
|
||||
@reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
|
||||
end
|
||||
|
||||
def reset_column_information
|
||||
@reflection.reset_column_information
|
||||
end
|
||||
|
||||
def has_primary_key?
|
||||
return @has_primary_key unless @has_primary_key.nil?
|
||||
@has_primary_key = (@owner.connection.supports_primary_key? &&
|
||||
@owner.connection.primary_key(@reflection.options[:join_table]))
|
||||
end
|
||||
|
||||
protected
|
||||
def construct_find_options!(options)
|
||||
options[:joins] = @join_sql
|
||||
options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
|
||||
options[:select] ||= (@reflection.options[:select] || '*')
|
||||
end
|
||||
|
||||
def count_records
|
||||
load_target.size
|
||||
end
|
||||
|
||||
def insert_record(record, force = true, validate = true)
|
||||
if has_primary_key?
|
||||
raise ActiveRecord::ConfigurationError,
|
||||
"Primary key is not allowed in a has_and_belongs_to_many join table (#{@reflection.options[:join_table]})."
|
||||
end
|
||||
|
||||
if record.new_record?
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
end
|
||||
end
|
||||
|
||||
if @reflection.options[:insert_sql]
|
||||
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
|
||||
else
|
||||
attributes = columns.inject({}) do |attrs, column|
|
||||
case column.name.to_s
|
||||
when @reflection.primary_key_name.to_s
|
||||
attrs[column.name] = owner_quoted_id
|
||||
when @reflection.association_foreign_key.to_s
|
||||
attrs[column.name] = record.quoted_id
|
||||
else
|
||||
if record.has_attribute?(column.name)
|
||||
value = @owner.send(:quote_value, record[column.name], column)
|
||||
attrs[column.name] = value unless value.nil?
|
||||
end
|
||||
end
|
||||
attrs
|
||||
end
|
||||
|
||||
sql =
|
||||
"INSERT INTO #{@owner.connection.quote_table_name @reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
|
||||
"VALUES (#{attributes.values.join(', ')})"
|
||||
|
||||
@owner.connection.insert(sql)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def delete_records(records)
|
||||
if sql = @reflection.options[:delete_sql]
|
||||
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
|
||||
else
|
||||
ids = quoted_record_ids(records)
|
||||
sql = "DELETE FROM #{@owner.connection.quote_table_name @reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
|
||||
@owner.connection.delete(sql)
|
||||
end
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
if @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
else
|
||||
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
||||
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
|
||||
|
||||
if @reflection.options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
elsif @reflection.options[:finder_sql]
|
||||
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
||||
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
else
|
||||
@counter_sql = @finder_sql
|
||||
end
|
||||
end
|
||||
|
||||
def construct_scope
|
||||
{ :find => { :conditions => @finder_sql,
|
||||
:joins => @join_sql,
|
||||
:readonly => false,
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:limit => @reflection.options[:limit] } }
|
||||
end
|
||||
|
||||
# Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
|
||||
# clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
|
||||
# an id column. This will then overwrite the id column of the records coming back.
|
||||
def finding_with_ambiguous_select?(select_clause)
|
||||
!select_clause && columns.size != 2
|
||||
end
|
||||
|
||||
private
|
||||
def create_record(attributes, &block)
|
||||
# Can't use Base.create because the foreign key may be a protected attribute.
|
||||
ensure_owner_is_not_new
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| create(attr) }
|
||||
else
|
||||
build_record(attributes, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
# This is the proxy that handles a has many association.
|
||||
#
|
||||
# If the association has a <tt>:through</tt> option further specialization
|
||||
# is provided by its child HasManyThroughAssociation.
|
||||
class HasManyAssociation < AssociationCollection #:nodoc:
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the number of records in this collection.
|
||||
#
|
||||
# If the association has a counter cache it gets that value. Otherwise
|
||||
# it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
|
||||
# there's one. Some configuration options like :group make it impossible
|
||||
# to do a SQL count, in those cases the array count will be used.
|
||||
#
|
||||
# That does not depend on whether the collection has already been loaded
|
||||
# or not. The +size+ method is the one that takes the loaded flag into
|
||||
# account and delegates to +count_records+ if needed.
|
||||
#
|
||||
# If the collection is empty the target is set to an empty array and
|
||||
# the loaded flag is set to true as well.
|
||||
def count_records
|
||||
count = if has_cached_counter?
|
||||
@owner.send(:read_attribute, cached_counter_attribute_name)
|
||||
elsif @reflection.options[:counter_sql]
|
||||
@reflection.klass.count_by_sql(@counter_sql)
|
||||
else
|
||||
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
|
||||
end
|
||||
|
||||
# If there's nothing in the database and @target has no new records
|
||||
# we are certain the current target is an empty array. This is a
|
||||
# documented side-effect of the method that may avoid an extra SELECT.
|
||||
@target ||= [] and loaded if count == 0
|
||||
|
||||
if @reflection.options[:limit]
|
||||
count = [ @reflection.options[:limit], count ].min
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
|
||||
def has_cached_counter?
|
||||
@owner.attribute_present?(cached_counter_attribute_name)
|
||||
end
|
||||
|
||||
def cached_counter_attribute_name
|
||||
"#{@reflection.name}_count"
|
||||
end
|
||||
|
||||
def insert_record(record, force = false, validate = true)
|
||||
set_belongs_to_association_for(record)
|
||||
force ? record.save! : record.save(validate)
|
||||
end
|
||||
|
||||
# Deletes the records according to the <tt>:dependent</tt> option.
|
||||
def delete_records(records)
|
||||
case @reflection.options[:dependent]
|
||||
when :destroy
|
||||
records.each { |r| r.destroy }
|
||||
when :delete_all
|
||||
@reflection.klass.delete(records.map { |record| record.id })
|
||||
else
|
||||
ids = quoted_record_ids(records)
|
||||
@reflection.klass.update_all(
|
||||
"#{@reflection.primary_key_name} = NULL",
|
||||
"#{@reflection.primary_key_name} = #{owner_quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
|
||||
)
|
||||
@owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter?
|
||||
end
|
||||
end
|
||||
|
||||
def target_obsolete?
|
||||
false
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
case
|
||||
when @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
|
||||
when @reflection.options[:as]
|
||||
@finder_sql =
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
|
||||
else
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
||||
if @reflection.options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
elsif @reflection.options[:finder_sql]
|
||||
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
||||
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
else
|
||||
@counter_sql = @finder_sql
|
||||
end
|
||||
end
|
||||
|
||||
def construct_scope
|
||||
create_scoping = {}
|
||||
set_belongs_to_association_for(create_scoping)
|
||||
{
|
||||
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit], :include => @reflection.options[:include]},
|
||||
:create => create_scoping
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,266 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
reflection.check_validity!
|
||||
super
|
||||
end
|
||||
|
||||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def create(attrs = nil)
|
||||
transaction do
|
||||
object = if attrs
|
||||
@reflection.klass.send(:with_scope, :create => attrs) {
|
||||
@reflection.create_association
|
||||
}
|
||||
else
|
||||
@reflection.create_association
|
||||
end
|
||||
raise_on_type_mismatch(object)
|
||||
add_record_to_target_with_callbacks(object) do |r|
|
||||
insert_record(object, false)
|
||||
end
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
|
||||
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero,
|
||||
# and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length.
|
||||
def size
|
||||
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
|
||||
return @target.size if loaded?
|
||||
return count
|
||||
end
|
||||
|
||||
protected
|
||||
def target_reflection_has_associated_record?
|
||||
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def construct_find_options!(options)
|
||||
options[:select] = construct_select(options[:select])
|
||||
options[:from] ||= construct_from
|
||||
options[:joins] = construct_joins(options[:joins])
|
||||
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include]
|
||||
end
|
||||
|
||||
def insert_record(record, force = true, validate = true)
|
||||
if record.new_record?
|
||||
if force
|
||||
record.save!
|
||||
else
|
||||
return false unless record.save(validate)
|
||||
end
|
||||
end
|
||||
through_reflection = @reflection.through_reflection
|
||||
klass = through_reflection.klass
|
||||
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
|
||||
end
|
||||
|
||||
# TODO - add dependent option support
|
||||
def delete_records(records)
|
||||
klass = @reflection.through_reflection.klass
|
||||
records.each do |associate|
|
||||
klass.delete_all(construct_join_attributes(associate))
|
||||
end
|
||||
end
|
||||
|
||||
def find_target
|
||||
return [] unless target_reflection_has_associated_record?
|
||||
@reflection.klass.find(:all,
|
||||
:select => construct_select,
|
||||
:conditions => construct_conditions,
|
||||
:from => construct_from,
|
||||
:joins => construct_joins,
|
||||
:order => @reflection.options[:order],
|
||||
:limit => @reflection.options[:limit],
|
||||
:group => @reflection.options[:group],
|
||||
:readonly => @reflection.options[:readonly],
|
||||
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
|
||||
)
|
||||
end
|
||||
|
||||
# Construct attributes for associate pointing to owner.
|
||||
def construct_owner_attributes(reflection)
|
||||
if as = reflection.options[:as]
|
||||
{ "#{as}_id" => @owner.id,
|
||||
"#{as}_type" => @owner.class.base_class.name.to_s }
|
||||
else
|
||||
{ reflection.primary_key_name => @owner.id }
|
||||
end
|
||||
end
|
||||
|
||||
# Construct attributes for :through pointing to owner and associate.
|
||||
def construct_join_attributes(associate)
|
||||
# TODO: revist this to allow it for deletion, supposing dependent option is supported
|
||||
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
|
||||
join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id)
|
||||
if @reflection.options[:source_type]
|
||||
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
|
||||
end
|
||||
join_attributes
|
||||
end
|
||||
|
||||
# Associate attributes pointing to owner, quoted.
|
||||
def construct_quoted_owner_attributes(reflection)
|
||||
if as = reflection.options[:as]
|
||||
{ "#{as}_id" => owner_quoted_id,
|
||||
"#{as}_type" => reflection.klass.quote_value(
|
||||
@owner.class.base_class.name.to_s,
|
||||
reflection.klass.columns_hash["#{as}_type"]) }
|
||||
elsif reflection.macro == :belongs_to
|
||||
{ reflection.klass.primary_key => @owner[reflection.primary_key_name] }
|
||||
else
|
||||
{ reflection.primary_key_name => owner_quoted_id }
|
||||
end
|
||||
end
|
||||
|
||||
# Build SQL conditions from attributes, qualified by table name.
|
||||
def construct_conditions
|
||||
table_name = @reflection.through_reflection.quoted_table_name
|
||||
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
|
||||
"#{table_name}.#{attr} = #{value}"
|
||||
end
|
||||
conditions << sql_conditions if sql_conditions
|
||||
"(" + conditions.join(') AND (') + ")"
|
||||
end
|
||||
|
||||
def construct_from
|
||||
@reflection.quoted_table_name
|
||||
end
|
||||
|
||||
def construct_select(custom_select = nil)
|
||||
distinct = "DISTINCT " if @reflection.options[:uniq]
|
||||
selected = custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
|
||||
end
|
||||
|
||||
def construct_joins(custom_joins = nil)
|
||||
polymorphic_join = nil
|
||||
if @reflection.source_reflection.macro == :belongs_to
|
||||
reflection_primary_key = @reflection.klass.primary_key
|
||||
source_primary_key = @reflection.source_reflection.primary_key_name
|
||||
if @reflection.options[:source_type]
|
||||
polymorphic_join = "AND %s.%s = %s" % [
|
||||
@reflection.through_reflection.quoted_table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
|
||||
@owner.class.quote_value(@reflection.options[:source_type])
|
||||
]
|
||||
end
|
||||
else
|
||||
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
||||
source_primary_key = @reflection.through_reflection.klass.primary_key
|
||||
if @reflection.source_reflection.options[:as]
|
||||
polymorphic_join = "AND %s.%s = %s" % [
|
||||
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
|
||||
@owner.class.quote_value(@reflection.through_reflection.klass.name)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
"INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
|
||||
@reflection.through_reflection.quoted_table_name,
|
||||
@reflection.quoted_table_name, reflection_primary_key,
|
||||
@reflection.through_reflection.quoted_table_name, source_primary_key,
|
||||
polymorphic_join
|
||||
]
|
||||
end
|
||||
|
||||
def construct_scope
|
||||
{ :create => construct_owner_attributes(@reflection),
|
||||
:find => { :from => construct_from,
|
||||
:conditions => construct_conditions,
|
||||
:joins => construct_joins,
|
||||
:include => @reflection.options[:include],
|
||||
:select => construct_select,
|
||||
:order => @reflection.options[:order],
|
||||
:limit => @reflection.options[:limit],
|
||||
:readonly => @reflection.options[:readonly],
|
||||
} }
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
case
|
||||
when @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
else
|
||||
@finder_sql = construct_conditions
|
||||
end
|
||||
|
||||
if @reflection.options[:counter_sql]
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
elsif @reflection.options[:finder_sql]
|
||||
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
||||
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
||||
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
||||
else
|
||||
@counter_sql = @finder_sql
|
||||
end
|
||||
end
|
||||
|
||||
def conditions
|
||||
@conditions = build_conditions unless defined?(@conditions)
|
||||
@conditions
|
||||
end
|
||||
|
||||
def build_conditions
|
||||
association_conditions = @reflection.options[:conditions]
|
||||
through_conditions = build_through_conditions
|
||||
source_conditions = @reflection.source_reflection.options[:conditions]
|
||||
uses_sti = !@reflection.through_reflection.klass.descends_from_active_record?
|
||||
|
||||
if association_conditions || through_conditions || source_conditions || uses_sti
|
||||
all = []
|
||||
|
||||
[association_conditions, source_conditions].each do |conditions|
|
||||
all << interpolate_sql(sanitize_sql(conditions)) if conditions
|
||||
end
|
||||
|
||||
all << through_conditions if through_conditions
|
||||
all << build_sti_condition if uses_sti
|
||||
|
||||
all.map { |sql| "(#{sql})" } * ' AND '
|
||||
end
|
||||
end
|
||||
|
||||
def build_through_conditions
|
||||
conditions = @reflection.through_reflection.options[:conditions]
|
||||
if conditions.is_a?(Hash)
|
||||
interpolate_sql(sanitize_sql(conditions)).gsub(
|
||||
@reflection.quoted_table_name,
|
||||
@reflection.through_reflection.quoted_table_name)
|
||||
elsif conditions
|
||||
interpolate_sql(sanitize_sql(conditions))
|
||||
end
|
||||
end
|
||||
|
||||
def build_sti_condition
|
||||
@reflection.through_reflection.klass.send(:type_condition)
|
||||
end
|
||||
|
||||
alias_method :sql_conditions, :conditions
|
||||
|
||||
def has_cached_counter?
|
||||
@owner.attribute_present?(cached_counter_attribute_name)
|
||||
end
|
||||
|
||||
def cached_counter_attribute_name
|
||||
"#{@reflection.name}_count"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,133 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasOneAssociation < BelongsToAssociation #:nodoc:
|
||||
def initialize(owner, reflection)
|
||||
super
|
||||
construct_sql
|
||||
end
|
||||
|
||||
def create(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.create_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def create!(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.create_association!(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def build(attrs = {}, replace_existing = true)
|
||||
new_record(replace_existing) do |reflection|
|
||||
attrs = merge_with_conditions(attrs)
|
||||
reflection.build_association(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
def replace(obj, dont_save = false)
|
||||
load_target
|
||||
|
||||
unless @target.nil? || @target == obj
|
||||
if dependent? && !dont_save
|
||||
case @reflection.options[:dependent]
|
||||
when :delete
|
||||
@target.delete unless @target.new_record?
|
||||
@owner.clear_association_cache
|
||||
when :destroy
|
||||
@target.destroy unless @target.new_record?
|
||||
@owner.clear_association_cache
|
||||
when :nullify
|
||||
@target[@reflection.primary_key_name] = nil
|
||||
@target.save unless @owner.new_record? || @target.new_record?
|
||||
end
|
||||
else
|
||||
@target[@reflection.primary_key_name] = nil
|
||||
@target.save unless @owner.new_record? || @target.new_record?
|
||||
end
|
||||
end
|
||||
|
||||
if obj.nil?
|
||||
@target = nil
|
||||
else
|
||||
raise_on_type_mismatch(obj)
|
||||
set_belongs_to_association_for(obj)
|
||||
@target = (AssociationProxy === obj ? obj.target : obj)
|
||||
end
|
||||
|
||||
@loaded = true
|
||||
|
||||
unless @owner.new_record? or obj.nil? or dont_save
|
||||
return (obj.save ? self : false)
|
||||
else
|
||||
return (obj.nil? ? nil : self)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def owner_quoted_id
|
||||
if @reflection.options[:primary_key]
|
||||
@owner.class.quote_value(@owner.send(@reflection.options[:primary_key]))
|
||||
else
|
||||
@owner.quoted_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find_target
|
||||
@reflection.klass.find(:first,
|
||||
:conditions => @finder_sql,
|
||||
:select => @reflection.options[:select],
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:readonly => @reflection.options[:readonly]
|
||||
)
|
||||
end
|
||||
|
||||
def construct_sql
|
||||
case
|
||||
when @reflection.options[:as]
|
||||
@finder_sql =
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
|
||||
"#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
|
||||
else
|
||||
@finder_sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
|
||||
end
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
end
|
||||
|
||||
def construct_scope
|
||||
create_scoping = {}
|
||||
set_belongs_to_association_for(create_scoping)
|
||||
{ :create => create_scoping }
|
||||
end
|
||||
|
||||
def new_record(replace_existing)
|
||||
# Make sure we load the target first, if we plan on replacing the existing
|
||||
# instance. Otherwise, if the target has not previously been loaded
|
||||
# elsewhere, the instance we create will get orphaned.
|
||||
load_target if replace_existing
|
||||
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
|
||||
yield @reflection
|
||||
end
|
||||
|
||||
if replace_existing
|
||||
replace(record, true)
|
||||
else
|
||||
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
||||
self.target = record
|
||||
end
|
||||
|
||||
record
|
||||
end
|
||||
|
||||
def merge_with_conditions(attrs={})
|
||||
attrs ||= {}
|
||||
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
||||
attrs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class HasOneThroughAssociation < HasManyThroughAssociation
|
||||
|
||||
def create_through_record(new_value) #nodoc:
|
||||
klass = @reflection.through_reflection.klass
|
||||
|
||||
current_object = @owner.send(@reflection.through_reflection.name)
|
||||
|
||||
if current_object
|
||||
new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
|
||||
elsif new_value
|
||||
if @owner.new_record?
|
||||
self.target = new_value
|
||||
through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
|
||||
through_association.build(construct_join_attributes(new_value))
|
||||
else
|
||||
@owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def find(*args)
|
||||
super(args.merge(:limit => 1))
|
||||
end
|
||||
|
||||
def find_target
|
||||
super.first
|
||||
end
|
||||
|
||||
def reset_target!
|
||||
@target = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,388 +0,0 @@
|
||||
module ActiveRecord
|
||||
module AttributeMethods #:nodoc:
|
||||
DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
|
||||
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
base.attribute_method_suffix(*DEFAULT_SUFFIXES)
|
||||
base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
|
||||
base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
||||
base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
||||
base.time_zone_aware_attributes = false
|
||||
base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
||||
base.skip_time_zone_conversion_for_attributes = []
|
||||
end
|
||||
|
||||
# Declare and check for suffixed attribute methods.
|
||||
module ClassMethods
|
||||
# Declares a method available for all attributes with the given suffix.
|
||||
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
|
||||
#
|
||||
# #{attr}#{suffix}(*args, &block)
|
||||
#
|
||||
# to
|
||||
#
|
||||
# attribute#{suffix}(#{attr}, *args, &block)
|
||||
#
|
||||
# An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
|
||||
# the +attr+ argument.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# attribute_method_suffix '_changed?'
|
||||
#
|
||||
# private
|
||||
# def attribute_changed?(attr)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.find(1)
|
||||
# person.name_changed? # => false
|
||||
# person.name = 'Hubert'
|
||||
# person.name_changed? # => true
|
||||
def attribute_method_suffix(*suffixes)
|
||||
attribute_method_suffixes.concat suffixes
|
||||
rebuild_attribute_method_regexp
|
||||
end
|
||||
|
||||
# Returns MatchData if method_name is an attribute method.
|
||||
def match_attribute_method?(method_name)
|
||||
rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
|
||||
@@attribute_method_regexp.match(method_name)
|
||||
end
|
||||
|
||||
|
||||
# Contains the names of the generated attribute methods.
|
||||
def generated_methods #:nodoc:
|
||||
@generated_methods ||= Set.new
|
||||
end
|
||||
|
||||
def generated_methods?
|
||||
!generated_methods.empty?
|
||||
end
|
||||
|
||||
# Generates all the attribute related methods for columns in the database
|
||||
# accessors, mutators and query methods.
|
||||
def define_attribute_methods
|
||||
return if generated_methods?
|
||||
columns_hash.each do |name, column|
|
||||
unless instance_method_already_implemented?(name)
|
||||
if self.serialized_attributes[name]
|
||||
define_read_method_for_serialized_attribute(name)
|
||||
elsif create_time_zone_conversion_attribute?(name, column)
|
||||
define_read_method_for_time_zone_conversion(name)
|
||||
else
|
||||
define_read_method(name.to_sym, name, column)
|
||||
end
|
||||
end
|
||||
|
||||
unless instance_method_already_implemented?("#{name}=")
|
||||
if create_time_zone_conversion_attribute?(name, column)
|
||||
define_write_method_for_time_zone_conversion(name)
|
||||
else
|
||||
define_write_method(name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
unless instance_method_already_implemented?("#{name}?")
|
||||
define_question_method(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Checks whether the method is defined in the model or any of its subclasses
|
||||
# that also derive from Active Record. Raises DangerousAttributeError if the
|
||||
# method is defined by Active Record though.
|
||||
def instance_method_already_implemented?(method_name)
|
||||
method_name = method_name.to_s
|
||||
return true if method_name =~ /^id(=$|\?$|$)/
|
||||
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map(&:to_s).to_set
|
||||
@@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map(&:to_s).to_set
|
||||
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
|
||||
@_defined_class_methods.include?(method_name)
|
||||
end
|
||||
|
||||
alias :define_read_methods :define_attribute_methods
|
||||
|
||||
# +cache_attributes+ allows you to declare which converted attribute values should
|
||||
# be cached. Usually caching only pays off for attributes with expensive conversion
|
||||
# methods, like time related columns (e.g. +created_at+, +updated_at+).
|
||||
def cache_attributes(*attribute_names)
|
||||
attribute_names.each {|attr| cached_attributes << attr.to_s}
|
||||
end
|
||||
|
||||
# Returns the attributes which are cached. By default time related columns
|
||||
# with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
|
||||
def cached_attributes
|
||||
@cached_attributes ||=
|
||||
columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
|
||||
end
|
||||
|
||||
# Returns +true+ if the provided attribute is being cached.
|
||||
def cache_attribute?(attr_name)
|
||||
cached_attributes.include?(attr_name)
|
||||
end
|
||||
|
||||
private
|
||||
# Suffixes a, ?, c become regexp /(a|\?|c)$/
|
||||
def rebuild_attribute_method_regexp
|
||||
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
|
||||
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
|
||||
end
|
||||
|
||||
# Default to =, ?, _before_type_cast
|
||||
def attribute_method_suffixes
|
||||
@@attribute_method_suffixes ||= []
|
||||
end
|
||||
|
||||
def create_time_zone_conversion_attribute?(name, column)
|
||||
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
|
||||
end
|
||||
|
||||
# Define an attribute reader method. Cope with nil column.
|
||||
def define_read_method(symbol, attr_name, column)
|
||||
cast_code = column.type_cast_code('v') if column
|
||||
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
||||
|
||||
unless attr_name.to_s == self.primary_key.to_s
|
||||
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
||||
end
|
||||
|
||||
if cache_attribute?(attr_name)
|
||||
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
||||
end
|
||||
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
|
||||
end
|
||||
|
||||
# Define read method for serialized attribute.
|
||||
def define_read_method_for_serialized_attribute(attr_name)
|
||||
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
|
||||
end
|
||||
|
||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
||||
def define_read_method_for_time_zone_conversion(attr_name)
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}(reload = false)
|
||||
cached = @attributes_cache['#{attr_name}']
|
||||
return cached if cached && !reload
|
||||
time = read_attribute('#{attr_name}')
|
||||
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method attr_name, method_body
|
||||
end
|
||||
|
||||
# Defines a predicate method <tt>attr_name?</tt>.
|
||||
def define_question_method(attr_name)
|
||||
evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
|
||||
end
|
||||
|
||||
def define_write_method(attr_name)
|
||||
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
|
||||
end
|
||||
|
||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
||||
def define_write_method_for_time_zone_conversion(attr_name)
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}=(time)
|
||||
unless time.acts_like?(:time)
|
||||
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
||||
end
|
||||
time = time.in_time_zone rescue nil if time
|
||||
write_attribute(:#{attr_name}, time)
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
|
||||
end
|
||||
|
||||
# Evaluate the definition for an attribute related method
|
||||
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
|
||||
|
||||
unless method_name.to_s == primary_key.to_s
|
||||
generated_methods << method_name
|
||||
end
|
||||
|
||||
begin
|
||||
class_eval(method_definition, __FILE__, __LINE__)
|
||||
rescue SyntaxError => err
|
||||
generated_methods.delete(attr_name)
|
||||
if logger
|
||||
logger.warn "Exception occurred during reader method compilation."
|
||||
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
|
||||
logger.warn err.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end # ClassMethods
|
||||
|
||||
|
||||
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
|
||||
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
||||
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
|
||||
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
|
||||
# the completed attribute is not +nil+ or 0.
|
||||
#
|
||||
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
||||
# table with a +master_id+ foreign key can instantiate master through Client#master.
|
||||
def method_missing(method_id, *args, &block)
|
||||
method_name = method_id.to_s
|
||||
|
||||
if self.class.private_method_defined?(method_name)
|
||||
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
||||
end
|
||||
|
||||
# If we haven't generated any methods yet, generate them, then
|
||||
# see if we've created the method we're looking for.
|
||||
if !self.class.generated_methods?
|
||||
self.class.define_attribute_methods
|
||||
if self.class.generated_methods.include?(method_name)
|
||||
return self.send(method_id, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
if self.class.primary_key.to_s == method_name
|
||||
id
|
||||
elsif md = self.class.match_attribute_method?(method_name)
|
||||
attribute_name, method_type = md.pre_match, md.to_s
|
||||
if @attributes.include?(attribute_name)
|
||||
__send__("attribute#{method_type}", attribute_name, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
elsif @attributes.include?(method_name)
|
||||
read_attribute(method_name)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
||||
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
||||
def read_attribute(attr_name)
|
||||
attr_name = attr_name.to_s
|
||||
if !(value = @attributes[attr_name]).nil?
|
||||
if column = column_for_attribute(attr_name)
|
||||
if unserializable_attribute?(attr_name, column)
|
||||
unserialize_attribute(attr_name)
|
||||
else
|
||||
column.type_cast(value)
|
||||
end
|
||||
else
|
||||
value
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def read_attribute_before_type_cast(attr_name)
|
||||
@attributes[attr_name]
|
||||
end
|
||||
|
||||
# Returns true if the attribute is of a text column and marked for serialization.
|
||||
def unserializable_attribute?(attr_name, column)
|
||||
column.text? && self.class.serialized_attributes[attr_name]
|
||||
end
|
||||
|
||||
# Returns the unserialized object of the attribute.
|
||||
def unserialize_attribute(attr_name)
|
||||
unserialized_object = object_from_yaml(@attributes[attr_name])
|
||||
|
||||
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
|
||||
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
|
||||
else
|
||||
raise SerializationTypeMismatch,
|
||||
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
|
||||
# columns are turned into +nil+.
|
||||
def write_attribute(attr_name, value)
|
||||
attr_name = attr_name.to_s
|
||||
@attributes_cache.delete(attr_name)
|
||||
if (column = column_for_attribute(attr_name)) && column.number?
|
||||
@attributes[attr_name] = convert_number_column_value(value)
|
||||
else
|
||||
@attributes[attr_name] = value
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def query_attribute(attr_name)
|
||||
unless value = read_attribute(attr_name)
|
||||
false
|
||||
else
|
||||
column = self.class.columns_hash[attr_name]
|
||||
if column.nil?
|
||||
if Numeric === value || value !~ /[^0-9]/
|
||||
!value.to_i.zero?
|
||||
else
|
||||
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
||||
!value.blank?
|
||||
end
|
||||
elsif column.number?
|
||||
!value.zero?
|
||||
else
|
||||
!value.blank?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
||||
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
||||
# which will all return +true+.
|
||||
alias :respond_to_without_attributes? :respond_to?
|
||||
def respond_to?(method, include_private_methods = false)
|
||||
method_name = method.to_s
|
||||
if super
|
||||
return true
|
||||
elsif !include_private_methods && super(method, true)
|
||||
# If we're here than we haven't found among non-private methods
|
||||
# but found among all methods. Which means that given method is private.
|
||||
return false
|
||||
elsif !self.class.generated_methods?
|
||||
self.class.define_attribute_methods
|
||||
if self.class.generated_methods.include?(method_name)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if @attributes.nil?
|
||||
return super
|
||||
elsif @attributes.include?(method_name)
|
||||
return true
|
||||
elsif md = self.class.match_attribute_method?(method_name)
|
||||
return true if @attributes.include?(md.pre_match)
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def missing_attribute(attr_name, stack)
|
||||
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
||||
end
|
||||
|
||||
# Handle *? for method_missing.
|
||||
def attribute?(attribute_name)
|
||||
query_attribute(attribute_name)
|
||||
end
|
||||
|
||||
# Handle *= for method_missing.
|
||||
def attribute=(attribute_name, value)
|
||||
write_attribute(attribute_name, value)
|
||||
end
|
||||
|
||||
# Handle *_before_type_cast for method_missing.
|
||||
def attribute_before_type_cast(attribute_name)
|
||||
read_attribute_before_type_cast(attribute_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,364 +0,0 @@
|
||||
module ActiveRecord
|
||||
# AutosaveAssociation is a module that takes care of automatically saving
|
||||
# your associations when the parent is saved. In addition to saving, it
|
||||
# also destroys any associations that were marked for destruction.
|
||||
# (See mark_for_destruction and marked_for_destruction?)
|
||||
#
|
||||
# Saving of the parent, its associations, and the destruction of marked
|
||||
# associations, all happen inside 1 transaction. This should never leave the
|
||||
# database in an inconsistent state after, for instance, mass assigning
|
||||
# attributes and saving them.
|
||||
#
|
||||
# If validations for any of the associations fail, their error messages will
|
||||
# be applied to the parent.
|
||||
#
|
||||
# Note that it also means that associations marked for destruction won't
|
||||
# be destroyed directly. They will however still be marked for destruction.
|
||||
#
|
||||
# === One-to-one Example
|
||||
#
|
||||
# Consider a Post model with one Author:
|
||||
#
|
||||
# class Post
|
||||
# has_one :author, :autosave => true
|
||||
# end
|
||||
#
|
||||
# Saving changes to the parent and its associated model can now be performed
|
||||
# automatically _and_ atomically:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.title # => "The current global position of migrating ducks"
|
||||
# post.author.name # => "alloy"
|
||||
#
|
||||
# post.title = "On the migration of ducks"
|
||||
# post.author.name = "Eloy Duran"
|
||||
#
|
||||
# post.save
|
||||
# post.reload
|
||||
# post.title # => "On the migration of ducks"
|
||||
# post.author.name # => "Eloy Duran"
|
||||
#
|
||||
# Destroying an associated model, as part of the parent's save action, is as
|
||||
# simple as marking it for destruction:
|
||||
#
|
||||
# post.author.mark_for_destruction
|
||||
# post.author.marked_for_destruction? # => true
|
||||
#
|
||||
# Note that the model is _not_ yet removed from the database:
|
||||
# id = post.author.id
|
||||
# Author.find_by_id(id).nil? # => false
|
||||
#
|
||||
# post.save
|
||||
# post.reload.author # => nil
|
||||
#
|
||||
# Now it _is_ removed from the database:
|
||||
# Author.find_by_id(id).nil? # => true
|
||||
#
|
||||
# === One-to-many Example
|
||||
#
|
||||
# Consider a Post model with many Comments:
|
||||
#
|
||||
# class Post
|
||||
# has_many :comments, :autosave => true
|
||||
# end
|
||||
#
|
||||
# Saving changes to the parent and its associated model can now be performed
|
||||
# automatically _and_ atomically:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.title # => "The current global position of migrating ducks"
|
||||
# post.comments.first.body # => "Wow, awesome info thanks!"
|
||||
# post.comments.last.body # => "Actually, your article should be named differently."
|
||||
#
|
||||
# post.title = "On the migration of ducks"
|
||||
# post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
|
||||
#
|
||||
# post.save
|
||||
# post.reload
|
||||
# post.title # => "On the migration of ducks"
|
||||
# post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
|
||||
#
|
||||
# Destroying one of the associated models members, as part of the parent's
|
||||
# save action, is as simple as marking it for destruction:
|
||||
#
|
||||
# post.comments.last.mark_for_destruction
|
||||
# post.comments.last.marked_for_destruction? # => true
|
||||
# post.comments.length # => 2
|
||||
#
|
||||
# Note that the model is _not_ yet removed from the database:
|
||||
# id = post.comments.last.id
|
||||
# Comment.find_by_id(id).nil? # => false
|
||||
#
|
||||
# post.save
|
||||
# post.reload.comments.length # => 1
|
||||
#
|
||||
# Now it _is_ removed from the database:
|
||||
# Comment.find_by_id(id).nil? # => true
|
||||
#
|
||||
# === Validation
|
||||
#
|
||||
# Validation is performed on the parent as usual, but also on all autosave
|
||||
# enabled associations. If any of the associations fail validation, its
|
||||
# error messages will be applied on the parents errors object and validation
|
||||
# of the parent will fail.
|
||||
#
|
||||
# Consider a Post model with Author which validates the presence of its name
|
||||
# attribute:
|
||||
#
|
||||
# class Post
|
||||
# has_one :author, :autosave => true
|
||||
# end
|
||||
#
|
||||
# class Author
|
||||
# validates_presence_of :name
|
||||
# end
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save # => false
|
||||
# post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
|
||||
#
|
||||
# No validations will be performed on the associated models when validations
|
||||
# are skipped for the parent:
|
||||
#
|
||||
# post = Post.find(1)
|
||||
# post.author.name = ''
|
||||
# post.save(false) # => true
|
||||
module AutosaveAssociation
|
||||
ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
base.extend(ClassMethods)
|
||||
alias_method_chain :reload, :autosave_associations
|
||||
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
base.send("valid_keys_for_#{type}_association") << :autosave
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
private
|
||||
|
||||
# def belongs_to(name, options = {})
|
||||
# super
|
||||
# add_autosave_association_callbacks(reflect_on_association(name))
|
||||
# end
|
||||
ASSOCIATION_TYPES.each do |type|
|
||||
module_eval %{
|
||||
def #{type}(name, options = {})
|
||||
super
|
||||
add_autosave_association_callbacks(reflect_on_association(name))
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# Adds a validate and save callback for the association as specified by
|
||||
# the +reflection+.
|
||||
def add_autosave_association_callbacks(reflection)
|
||||
save_method = "autosave_associated_records_for_#{reflection.name}"
|
||||
validation_method = "validate_associated_records_for_#{reflection.name}"
|
||||
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
|
||||
|
||||
case reflection.macro
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
before_save :before_save_collection_association
|
||||
|
||||
define_method(save_method) { save_collection_association(reflection) }
|
||||
# Doesn't use after_save as that would save associations added in after_create/after_update twice
|
||||
after_create save_method
|
||||
after_update save_method
|
||||
|
||||
if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
|
||||
define_method(validation_method) { validate_collection_association(reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
else
|
||||
case reflection.macro
|
||||
when :has_one
|
||||
define_method(save_method) { save_has_one_association(reflection) }
|
||||
after_save save_method
|
||||
when :belongs_to
|
||||
define_method(save_method) { save_belongs_to_association(reflection) }
|
||||
before_save save_method
|
||||
end
|
||||
|
||||
if force_validation
|
||||
define_method(validation_method) { validate_single_association(reflection) }
|
||||
validate validation_method
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reloads the attributes of the object as usual and removes a mark for destruction.
|
||||
def reload_with_autosave_associations(options = nil)
|
||||
@marked_for_destruction = false
|
||||
reload_without_autosave_associations(options)
|
||||
end
|
||||
|
||||
# Marks this record to be destroyed as part of the parents save transaction.
|
||||
# This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
|
||||
#
|
||||
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
||||
def mark_for_destruction
|
||||
@marked_for_destruction = true
|
||||
end
|
||||
|
||||
# Returns whether or not this record will be destroyed as part of the parents save transaction.
|
||||
#
|
||||
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
||||
def marked_for_destruction?
|
||||
@marked_for_destruction
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns the record for an association collection that should be validated
|
||||
# or saved. If +autosave+ is +false+ only new records will be returned,
|
||||
# unless the parent is/was a new record itself.
|
||||
def associated_records_to_validate_or_save(association, new_record, autosave)
|
||||
if new_record
|
||||
association
|
||||
elsif association.loaded?
|
||||
autosave ? association : association.select { |record| record.new_record? }
|
||||
else
|
||||
autosave ? association.target : association.target.select { |record| record.new_record? }
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
|
||||
# turned on for the association specified by +reflection+.
|
||||
def validate_single_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.target.nil?
|
||||
association_valid?(reflection, association)
|
||||
end
|
||||
end
|
||||
|
||||
# Validate the associated records if <tt>:validate</tt> or
|
||||
# <tt>:autosave</tt> is turned on for the association specified by
|
||||
# +reflection+.
|
||||
def validate_collection_association(reflection)
|
||||
if association = association_instance_get(reflection.name)
|
||||
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
|
||||
records.each { |record| association_valid?(reflection, record) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether or not the association is valid and applies any errors to
|
||||
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
|
||||
# enabled records if they're marked_for_destruction? or destroyed.
|
||||
def association_valid?(reflection, association)
|
||||
return true if association.destroyed? || association.marked_for_destruction?
|
||||
|
||||
unless valid = association.valid?
|
||||
if reflection.options[:autosave]
|
||||
association.errors.each_error do |attribute, error|
|
||||
attribute = "#{reflection.name}.#{attribute}"
|
||||
errors.add(attribute, error.dup) unless errors.on(attribute)
|
||||
end
|
||||
else
|
||||
errors.add(reflection.name)
|
||||
end
|
||||
end
|
||||
valid
|
||||
end
|
||||
|
||||
# Is used as a before_save callback to check while saving a collection
|
||||
# association whether or not the parent was a new record before saving.
|
||||
def before_save_collection_association
|
||||
@new_record_before_save = new_record?
|
||||
true
|
||||
end
|
||||
|
||||
# Saves any new associated records, or all loaded autosave associations if
|
||||
# <tt>:autosave</tt> is enabled on the association.
|
||||
#
|
||||
# In addition, it destroys all children that were marked for destruction
|
||||
# with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_collection_association(reflection)
|
||||
if association = association_instance_get(reflection.name)
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
|
||||
records.each do |record|
|
||||
next if record.destroyed?
|
||||
|
||||
if autosave && record.marked_for_destruction?
|
||||
association.destroy(record)
|
||||
elsif autosave != false && (@new_record_before_save || record.new_record?)
|
||||
if autosave
|
||||
association.send(:insert_record, record, false, false)
|
||||
else
|
||||
association.send(:insert_record, record)
|
||||
end
|
||||
elsif autosave
|
||||
record.save(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# reconstruct the SQL queries now that we know the owner's id
|
||||
association.send(:construct_sql) if association.respond_to?(:construct_sql)
|
||||
end
|
||||
end
|
||||
|
||||
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
|
||||
# on the association.
|
||||
#
|
||||
# In addition, it will destroy the association if it was marked for
|
||||
# destruction with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_has_one_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
else
|
||||
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
|
||||
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
|
||||
association[reflection.primary_key_name] = key
|
||||
association.save(!autosave)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled
|
||||
# on the association.
|
||||
#
|
||||
# In addition, it will destroy the association if it was marked for
|
||||
# destruction with mark_for_destruction.
|
||||
#
|
||||
# This all happens inside a transaction, _if_ the Transactions module is included into
|
||||
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
|
||||
def save_belongs_to_association(reflection)
|
||||
if (association = association_instance_get(reflection.name)) && !association.destroyed?
|
||||
autosave = reflection.options[:autosave]
|
||||
|
||||
if autosave && association.marked_for_destruction?
|
||||
association.destroy
|
||||
elsif autosave != false
|
||||
association.save(!autosave) if association.new_record? || autosave
|
||||
|
||||
if association.updated?
|
||||
association_id = association.send(reflection.options[:primary_key] || :id)
|
||||
self[reflection.primary_key_name] = association_id
|
||||
# TODO: Removing this code doesn't seem to matter…
|
||||
if reflection.options[:polymorphic]
|
||||
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3165 +0,0 @@
|
||||
require 'yaml'
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
# Generic Active Record exception class.
|
||||
class ActiveRecordError < StandardError
|
||||
end
|
||||
|
||||
# Raised when the single-table inheritance mechanism fails to locate the subclass
|
||||
# (for example due to improper usage of column that +inheritance_column+ points to).
|
||||
class SubclassNotFound < ActiveRecordError #:nodoc:
|
||||
end
|
||||
|
||||
# Raised when an object assigned to an association has an incorrect type.
|
||||
#
|
||||
# class Ticket < ActiveRecord::Base
|
||||
# has_many :patches
|
||||
# end
|
||||
#
|
||||
# class Patch < ActiveRecord::Base
|
||||
# belongs_to :ticket
|
||||
# end
|
||||
#
|
||||
# # Comments are not patches, this assignment raises AssociationTypeMismatch.
|
||||
# @ticket.patches << Comment.new(:content => "Please attach tests to your patch.")
|
||||
class AssociationTypeMismatch < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when unserialized object's type mismatches one specified for serializable field.
|
||||
class SerializationTypeMismatch < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt> misses adapter field).
|
||||
class AdapterNotSpecified < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
|
||||
class AdapterNotFound < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when connection to the database could not been established (for example when <tt>connection=</tt> is given a nil object).
|
||||
class ConnectionNotEstablished < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when Active Record cannot find record by given id or set of ids.
|
||||
class RecordNotFound < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
|
||||
# saved because record is invalid.
|
||||
class RecordNotSaved < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when SQL statement cannot be executed by the database (for example, it's often the case for MySQL when Ruby driver used is too old).
|
||||
class StatementInvalid < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example, when using +find+ method)
|
||||
# does not match number of expected variables.
|
||||
#
|
||||
# For example, in
|
||||
#
|
||||
# Location.find :all, :conditions => ["lat = ? AND lng = ?", 53.7362]
|
||||
#
|
||||
# two placeholders are given but only one variable to fill them.
|
||||
class PreparedStatementInvalid < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
|
||||
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
|
||||
# the page before the other.
|
||||
#
|
||||
# Read more about optimistic locking in ActiveRecord::Locking module RDoc.
|
||||
class StaleObjectError < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when association is being configured improperly or
|
||||
# user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
|
||||
class ConfigurationError < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised on attempt to update record that is instantiated as read only.
|
||||
class ReadOnlyRecord < ActiveRecordError
|
||||
end
|
||||
|
||||
# ActiveRecord::Transactions::ClassMethods.transaction uses this exception
|
||||
# to distinguish a deliberate rollback from other exceptional situations.
|
||||
# Normally, raising an exception will cause the +transaction+ method to rollback
|
||||
# the database transaction *and* pass on the exception. But if you raise an
|
||||
# ActiveRecord::Rollback exception, then the database transaction will be rolled back,
|
||||
# without passing on the exception.
|
||||
#
|
||||
# For example, you could do this in your controller to rollback a transaction:
|
||||
#
|
||||
# class BooksController < ActionController::Base
|
||||
# def create
|
||||
# Book.transaction do
|
||||
# book = Book.new(params[:book])
|
||||
# book.save!
|
||||
# if today_is_friday?
|
||||
# # The system must fail on Friday so that our support department
|
||||
# # won't be out of job. We silently rollback this transaction
|
||||
# # without telling the user.
|
||||
# raise ActiveRecord::Rollback, "Call tech support!"
|
||||
# end
|
||||
# end
|
||||
# # ActiveRecord::Rollback is the only exception that won't be passed on
|
||||
# # by ActiveRecord::Base.transaction, so this line will still be reached
|
||||
# # even on Friday.
|
||||
# redirect_to root_url
|
||||
# end
|
||||
# end
|
||||
class Rollback < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
|
||||
class DangerousAttributeError < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when you've tried to access a column which wasn't loaded by your finder.
|
||||
# Typically this is because <tt>:select</tt> has been specified.
|
||||
class MissingAttributeError < NoMethodError
|
||||
end
|
||||
|
||||
# Raised when unknown attributes are supplied via mass assignment.
|
||||
class UnknownAttributeError < NoMethodError
|
||||
end
|
||||
|
||||
# Raised when an error occurred while doing a mass assignment to an attribute through the
|
||||
# <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
|
||||
# offending attribute.
|
||||
class AttributeAssignmentError < ActiveRecordError
|
||||
attr_reader :exception, :attribute
|
||||
def initialize(message, exception, attribute)
|
||||
@exception = exception
|
||||
@attribute = attribute
|
||||
@message = message
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when there are multiple errors while doing a mass assignment through the +attributes+
|
||||
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
|
||||
# objects, each corresponding to the error while assigning to an attribute.
|
||||
class MultiparameterAssignmentErrors < ActiveRecordError
|
||||
attr_reader :errors
|
||||
def initialize(errors)
|
||||
@errors = errors
|
||||
end
|
||||
end
|
||||
|
||||
# Active Record objects don't specify their attributes directly, but rather infer them from the table definition with
|
||||
# which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
|
||||
# is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
|
||||
# database table will happen automatically in most common cases, but can be overwritten for the uncommon ones.
|
||||
#
|
||||
# See the mapping rules in table_name and the full example in link:files/README.html for more insight.
|
||||
#
|
||||
# == Creation
|
||||
#
|
||||
# Active Records accept constructor parameters either in a hash or as a block. The hash method is especially useful when
|
||||
# you're receiving the data from somewhere else, like an HTTP request. It works like this:
|
||||
#
|
||||
# user = User.new(:name => "David", :occupation => "Code Artist")
|
||||
# user.name # => "David"
|
||||
#
|
||||
# You can also use block initialization:
|
||||
#
|
||||
# user = User.new do |u|
|
||||
# u.name = "David"
|
||||
# u.occupation = "Code Artist"
|
||||
# end
|
||||
#
|
||||
# And of course you can just create a bare object and specify the attributes after the fact:
|
||||
#
|
||||
# user = User.new
|
||||
# user.name = "David"
|
||||
# user.occupation = "Code Artist"
|
||||
#
|
||||
# == Conditions
|
||||
#
|
||||
# Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement.
|
||||
# The array form is to be used when the condition input is tainted and requires sanitization. The string form can
|
||||
# be used for statements that don't involve tainted data. The hash form works much like the array form, except
|
||||
# only equality and range is possible. Examples:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# def self.authenticate_unsafely(user_name, password)
|
||||
# find(:first, :conditions => "user_name = '#{user_name}' AND password = '#{password}'")
|
||||
# end
|
||||
#
|
||||
# def self.authenticate_safely(user_name, password)
|
||||
# find(:first, :conditions => [ "user_name = ? AND password = ?", user_name, password ])
|
||||
# end
|
||||
#
|
||||
# def self.authenticate_safely_simply(user_name, password)
|
||||
# find(:first, :conditions => { :user_name => user_name, :password => password })
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection
|
||||
# attacks if the <tt>user_name</tt> and +password+ parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and
|
||||
# <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query,
|
||||
# which will ensure that an attacker can't escape the query and fake the login (or worse).
|
||||
#
|
||||
# When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth
|
||||
# question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing
|
||||
# the question marks with symbols and supplying a hash with values for the matching symbol keys:
|
||||
#
|
||||
# Company.find(:first, :conditions => [
|
||||
# "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
|
||||
# { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
|
||||
# ])
|
||||
#
|
||||
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
|
||||
# operator. For instance:
|
||||
#
|
||||
# Student.find(:all, :conditions => { :first_name => "Harvey", :status => 1 })
|
||||
# Student.find(:all, :conditions => params[:student])
|
||||
#
|
||||
# A range may be used in the hash to use the SQL BETWEEN operator:
|
||||
#
|
||||
# Student.find(:all, :conditions => { :grade => 9..12 })
|
||||
#
|
||||
# An array may be used in the hash to use the SQL IN operator:
|
||||
#
|
||||
# Student.find(:all, :conditions => { :grade => [9,11,12] })
|
||||
#
|
||||
# == Overwriting default accessors
|
||||
#
|
||||
# All column values are automatically available through basic accessors on the Active Record object, but sometimes you
|
||||
# want to specialize this behavior. This can be done by overwriting the default accessors (using the same
|
||||
# name as the attribute) and calling <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually change things.
|
||||
# Example:
|
||||
#
|
||||
# class Song < ActiveRecord::Base
|
||||
# # Uses an integer of seconds to hold the length of the song
|
||||
#
|
||||
# def length=(minutes)
|
||||
# write_attribute(:length, minutes.to_i * 60)
|
||||
# end
|
||||
#
|
||||
# def length
|
||||
# read_attribute(:length) / 60
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> instead of <tt>write_attribute(:attribute, value)</tt> and
|
||||
# <tt>read_attribute(:attribute)</tt> as a shorter form.
|
||||
#
|
||||
# == Attribute query methods
|
||||
#
|
||||
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
|
||||
# Query methods allow you to test whether an attribute value is present.
|
||||
#
|
||||
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
|
||||
# to determine whether the user has a name:
|
||||
#
|
||||
# user = User.new(:name => "David")
|
||||
# user.name? # => true
|
||||
#
|
||||
# anonymous = User.new(:name => "")
|
||||
# anonymous.name? # => false
|
||||
#
|
||||
# == Accessing attributes before they have been typecasted
|
||||
#
|
||||
# Sometimes you want to be able to read the raw attribute data without having the column-determined typecast run its course first.
|
||||
# That can be done by using the <tt><attribute>_before_type_cast</tt> accessors that all attributes have. For example, if your Account model
|
||||
# has a <tt>balance</tt> attribute, you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
|
||||
#
|
||||
# This is especially useful in validation situations where the user might supply a string for an integer field and you want to display
|
||||
# the original string back in an error message. Accessing the attribute normally would typecast the string to 0, which isn't what you
|
||||
# want.
|
||||
#
|
||||
# == Dynamic attribute-based finders
|
||||
#
|
||||
# Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries without turning to SQL. They work by
|
||||
# appending the name of an attribute to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt>, so you get finders like <tt>Person.find_by_user_name</tt>,
|
||||
# <tt>Person.find_all_by_last_name</tt>, and <tt>Payment.find_by_transaction_id</tt>. So instead of writing
|
||||
# <tt>Person.find(:first, :conditions => ["user_name = ?", user_name])</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
|
||||
# And instead of writing <tt>Person.find(:all, :conditions => ["last_name = ?", last_name])</tt>, you just do <tt>Person.find_all_by_last_name(last_name)</tt>.
|
||||
#
|
||||
# It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
|
||||
# <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
|
||||
# <tt>Person.find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt>, you just do
|
||||
# <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
|
||||
#
|
||||
# It's even possible to use all the additional parameters to find. For example, the full interface for <tt>Payment.find_all_by_amount</tt>
|
||||
# is actually <tt>Payment.find_all_by_amount(amount, options)</tt>. And the full interface to <tt>Person.find_by_user_name</tt> is
|
||||
# actually <tt>Person.find_by_user_name(user_name, options)</tt>. So you could call <tt>Payment.find_all_by_amount(50, :order => "created_on")</tt>.
|
||||
# Also you may call <tt>Payment.find_last_by_amount(amount, options)</tt> returning the last record matching that amount and options.
|
||||
#
|
||||
# The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with
|
||||
# <tt>find_or_create_by_</tt> and will return the object if it already exists and otherwise creates it, then returns it. Protected attributes won't be set unless they are given in a block. For example:
|
||||
#
|
||||
# # No 'Summer' tag exists
|
||||
# Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
|
||||
#
|
||||
# # Now the 'Summer' tag does exist
|
||||
# Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
|
||||
#
|
||||
# # Now 'Bob' exist and is an 'admin'
|
||||
# User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
|
||||
#
|
||||
# Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be set unless they are given in a block. For example:
|
||||
#
|
||||
# # No 'Winter' tag exists
|
||||
# winter = Tag.find_or_initialize_by_name("Winter")
|
||||
# winter.new_record? # true
|
||||
#
|
||||
# To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
|
||||
# a list of parameters. For example:
|
||||
#
|
||||
# Tag.find_or_create_by_name(:name => "rails", :creator => current_user)
|
||||
#
|
||||
# That will either find an existing tag named "rails", or create a new one while setting the user that created it.
|
||||
#
|
||||
# == Saving arrays, hashes, and other non-mappable objects in text columns
|
||||
#
|
||||
# Active Record can serialize any object in text columns using YAML. To do so, you must specify this with a call to the class method +serialize+.
|
||||
# This makes it possible to store arrays, hashes, and other non-mappable objects without doing any additional work. Example:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# serialize :preferences
|
||||
# end
|
||||
#
|
||||
# user = User.create(:preferences => { "background" => "black", "display" => large })
|
||||
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
|
||||
#
|
||||
# You can also specify a class option as the second parameter that'll raise an exception if a serialized object is retrieved as a
|
||||
# descendant of a class not in the hierarchy. Example:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# serialize :preferences, Hash
|
||||
# end
|
||||
#
|
||||
# user = User.create(:preferences => %w( one two three ))
|
||||
# User.find(user.id).preferences # raises SerializationTypeMismatch
|
||||
#
|
||||
# == Single table inheritance
|
||||
#
|
||||
# Active Record allows inheritance by storing the name of the class in a column that by default is named "type" (can be changed
|
||||
# by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this:
|
||||
#
|
||||
# class Company < ActiveRecord::Base; end
|
||||
# class Firm < Company; end
|
||||
# class Client < Company; end
|
||||
# class PriorityClient < Client; end
|
||||
#
|
||||
# When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in the companies table with type = "Firm". You can then
|
||||
# fetch this row again using <tt>Company.find(:first, "name = '37signals'")</tt> and it will return a Firm object.
|
||||
#
|
||||
# If you don't have a type column defined in your table, single-table inheritance won't be triggered. In that case, it'll work just
|
||||
# like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
|
||||
#
|
||||
# Note, all the attributes for all the cases are kept in the same table. Read more:
|
||||
# http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
|
||||
#
|
||||
# == Connection to multiple databases in different models
|
||||
#
|
||||
# Connections are usually created through ActiveRecord::Base.establish_connection and retrieved by ActiveRecord::Base.connection.
|
||||
# All classes inheriting from ActiveRecord::Base will use this connection. But you can also set a class-specific connection.
|
||||
# For example, if Course is an ActiveRecord::Base, but resides in a different database, you can just say <tt>Course.establish_connection</tt>
|
||||
# and Course and all of its subclasses will use this connection instead.
|
||||
#
|
||||
# This feature is implemented by keeping a connection pool in ActiveRecord::Base that is a Hash indexed by the class. If a connection is
|
||||
# requested, the retrieve_connection method will go up the class-hierarchy until a connection is found in the connection pool.
|
||||
#
|
||||
# == Exceptions
|
||||
#
|
||||
# * ActiveRecordError - Generic error class and superclass of all other errors raised by Active Record.
|
||||
# * AdapterNotSpecified - The configuration hash used in <tt>establish_connection</tt> didn't include an
|
||||
# <tt>:adapter</tt> key.
|
||||
# * AdapterNotFound - The <tt>:adapter</tt> key used in <tt>establish_connection</tt> specified a non-existent adapter
|
||||
# (or a bad spelling of an existing one).
|
||||
# * AssociationTypeMismatch - The object assigned to the association wasn't of the type specified in the association definition.
|
||||
# * SerializationTypeMismatch - The serialized object wasn't of the class specified as the second parameter.
|
||||
# * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt> before querying.
|
||||
# * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
|
||||
# or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
|
||||
# nothing was found, please check its documentation for further details.
|
||||
# * StatementInvalid - The database server rejected the SQL statement. The precise error is added in the message.
|
||||
# * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
|
||||
# <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of AttributeAssignmentError
|
||||
# objects that should be inspected to determine which attributes triggered the errors.
|
||||
# * AttributeAssignmentError - An error occurred while doing a mass assignment through the <tt>attributes=</tt> method.
|
||||
# You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error.
|
||||
#
|
||||
# *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
|
||||
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
|
||||
# instances in the current object space.
|
||||
class Base
|
||||
##
|
||||
# :singleton-method:
|
||||
# Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class, which is then passed
|
||||
# on to any new database connections made and which can be retrieved on both a class and instance level by calling +logger+.
|
||||
cattr_accessor :logger, :instance_writer => false
|
||||
|
||||
def self.inherited(child) #:nodoc:
|
||||
@@subclasses[self] ||= []
|
||||
@@subclasses[self] << child
|
||||
super
|
||||
end
|
||||
|
||||
def self.reset_subclasses #:nodoc:
|
||||
nonreloadables = []
|
||||
subclasses.each do |klass|
|
||||
unless ActiveSupport::Dependencies.autoloaded? klass
|
||||
nonreloadables << klass
|
||||
next
|
||||
end
|
||||
klass.instance_variables.each { |var| klass.send(:remove_instance_variable, var) }
|
||||
klass.instance_methods(false).each { |m| klass.send :undef_method, m }
|
||||
end
|
||||
@@subclasses = {}
|
||||
nonreloadables.each { |klass| (@@subclasses[klass.superclass] ||= []) << klass }
|
||||
end
|
||||
|
||||
@@subclasses = {}
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Contains the database configuration - as is typically stored in config/database.yml -
|
||||
# as a Hash.
|
||||
#
|
||||
# For example, the following database.yml...
|
||||
#
|
||||
# development:
|
||||
# adapter: sqlite3
|
||||
# database: db/development.sqlite3
|
||||
#
|
||||
# production:
|
||||
# adapter: sqlite3
|
||||
# database: db/production.sqlite3
|
||||
#
|
||||
# ...would result in ActiveRecord::Base.configurations to look like this:
|
||||
#
|
||||
# {
|
||||
# 'development' => {
|
||||
# 'adapter' => 'sqlite3',
|
||||
# 'database' => 'db/development.sqlite3'
|
||||
# },
|
||||
# 'production' => {
|
||||
# 'adapter' => 'sqlite3',
|
||||
# 'database' => 'db/production.sqlite3'
|
||||
# }
|
||||
# }
|
||||
cattr_accessor :configurations, :instance_writer => false
|
||||
@@configurations = {}
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Accessor for the prefix type that will be prepended to every primary key column name. The options are :table_name and
|
||||
# :table_name_with_underscore. If the first is specified, the Product class will look for "productid" instead of "id" as
|
||||
# the primary column. If the latter is specified, the Product class will look for "product_id" instead of "id". Remember
|
||||
# that this is a global setting for all Active Records.
|
||||
cattr_accessor :primary_key_prefix_type, :instance_writer => false
|
||||
@@primary_key_prefix_type = nil
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Accessor for the name of the prefix string to prepend to every table name. So if set to "basecamp_", all
|
||||
# table names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient way of creating a namespace
|
||||
# for tables in a shared database. By default, the prefix is the empty string.
|
||||
cattr_accessor :table_name_prefix, :instance_writer => false
|
||||
@@table_name_prefix = ""
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
|
||||
# "people_basecamp"). By default, the suffix is the empty string.
|
||||
cattr_accessor :table_name_suffix, :instance_writer => false
|
||||
@@table_name_suffix = ""
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Indicates whether table names should be the pluralized versions of the corresponding class names.
|
||||
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
|
||||
# See table_name for the full rules on table/class naming. This is true, by default.
|
||||
cattr_accessor :pluralize_table_names, :instance_writer => false
|
||||
@@pluralize_table_names = true
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Determines whether to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
|
||||
# make it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
|
||||
# may complicate matters if you use software like syslog. This is true, by default.
|
||||
cattr_accessor :colorize_logging, :instance_writer => false
|
||||
@@colorize_logging = true
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
|
||||
# This is set to :local by default.
|
||||
cattr_accessor :default_timezone, :instance_writer => false
|
||||
@@default_timezone = :local
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Specifies the format to use when dumping the database schema with Rails'
|
||||
# Rakefile. If :sql, the schema is dumped as (potentially database-
|
||||
# specific) SQL statements. If :ruby, the schema is dumped as an
|
||||
# ActiveRecord::Schema file which can be loaded into any database that
|
||||
# supports migrations. Use :ruby if you want to have different database
|
||||
# adapters for, e.g., your development and test environments.
|
||||
cattr_accessor :schema_format , :instance_writer => false
|
||||
@@schema_format = :ruby
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Specify whether or not to use timestamps for migration numbers
|
||||
cattr_accessor :timestamped_migrations , :instance_writer => false
|
||||
@@timestamped_migrations = true
|
||||
|
||||
# Determine whether to store the full constant name including namespace when using STI
|
||||
superclass_delegating_accessor :store_full_sti_class
|
||||
self.store_full_sti_class = false
|
||||
|
||||
# Stores the default scope for the class
|
||||
class_inheritable_accessor :default_scoping, :instance_writer => false
|
||||
self.default_scoping = []
|
||||
|
||||
class << self # Class methods
|
||||
# Find operates with four different retrieval approaches:
|
||||
#
|
||||
# * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
|
||||
# If no record can be found for all of the listed ids, then RecordNotFound will be raised.
|
||||
# * Find first - This will return the first record matched by the options used. These options can either be specific
|
||||
# conditions or merely an order. If no record can be matched, +nil+ is returned. Use
|
||||
# <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>.
|
||||
# * Find last - This will return the last record matched by the options used. These options can either be specific
|
||||
# conditions or merely an order. If no record can be matched, +nil+ is returned. Use
|
||||
# <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>.
|
||||
# * Find all - This will return all the records matched by the options used.
|
||||
# If no records are found, an empty array is returned. Use
|
||||
# <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>.
|
||||
#
|
||||
# All approaches accept an options hash as their last parameter.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>[ "user_name = ?", username ]</tt>, or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
|
||||
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name".
|
||||
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
||||
# * <tt>:having</tt> - Combined with +:group+ this can be used to filter the records that a <tt>GROUP BY</tt> returns. Uses the <tt>HAVING</tt> SQL-clause.
|
||||
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
|
||||
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip rows 0 through 4.
|
||||
# * <tt>:joins</tt> - Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed),
|
||||
# named associations in the same form used for the <tt>:include</tt> option, which will perform an <tt>INNER JOIN</tt> on the associated table(s),
|
||||
# or an array containing a mixture of both strings and named associations.
|
||||
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
|
||||
# Pass <tt>:readonly => false</tt> to override.
|
||||
# * <tt>:include</tt> - Names associations that should be loaded alongside. The symbols named refer
|
||||
# to already defined associations. See eager loading under Associations.
|
||||
# * <tt>:select</tt> - By default, this is "*" as in "SELECT * FROM", but can be changed if you, for example, want to do a join but not
|
||||
# include the joined columns. Takes a string with the SELECT SQL fragment (e.g. "id, name").
|
||||
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
|
||||
# of a database view).
|
||||
# * <tt>:readonly</tt> - Mark the returned records read-only so they cannot be saved or updated.
|
||||
# * <tt>:lock</tt> - An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE".
|
||||
# <tt>:lock => true</tt> gives connection's default exclusive lock, usually "FOR UPDATE".
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # find by id
|
||||
# Person.find(1) # returns the object for ID = 1
|
||||
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
|
||||
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
|
||||
# Person.find([1]) # returns an array for the object with ID = 1
|
||||
# Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
|
||||
#
|
||||
# Note that returned records may not be in the same order as the ids you
|
||||
# provide since database rows are unordered. Give an explicit <tt>:order</tt>
|
||||
# to ensure the results are sorted.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # find first
|
||||
# Person.find(:first) # returns the first object fetched by SELECT * FROM people
|
||||
# Person.find(:first, :conditions => [ "user_name = ?", user_name])
|
||||
# Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }])
|
||||
# Person.find(:first, :order => "created_on DESC", :offset => 5)
|
||||
#
|
||||
# # find last
|
||||
# Person.find(:last) # returns the last object fetched by SELECT * FROM people
|
||||
# Person.find(:last, :conditions => [ "user_name = ?", user_name])
|
||||
# Person.find(:last, :order => "created_on DESC", :offset => 5)
|
||||
#
|
||||
# # find all
|
||||
# Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
|
||||
# Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
|
||||
# Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] }
|
||||
# Person.find(:all, :offset => 10, :limit => 10)
|
||||
# Person.find(:all, :include => [ :account, :friends ])
|
||||
# Person.find(:all, :group => "category")
|
||||
#
|
||||
# Example for find with a lock: Imagine two concurrent transactions:
|
||||
# each will read <tt>person.visits == 2</tt>, add 1 to it, and save, resulting
|
||||
# in two saves of <tt>person.visits = 3</tt>. By locking the row, the second
|
||||
# transaction has to wait until the first is finished; we get the
|
||||
# expected <tt>person.visits == 4</tt>.
|
||||
#
|
||||
# Person.transaction do
|
||||
# person = Person.find(1, :lock => true)
|
||||
# person.visits += 1
|
||||
# person.save!
|
||||
# end
|
||||
def find(*args)
|
||||
options = args.extract_options!
|
||||
validate_find_options(options)
|
||||
set_readonly_option!(options)
|
||||
|
||||
case args.first
|
||||
when :first then find_initial(options)
|
||||
when :last then find_last(options)
|
||||
when :all then find_every(options)
|
||||
else find_from_ids(args, options)
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
|
||||
# same arguments to this method as you can to <tt>find(:first)</tt>.
|
||||
def first(*args)
|
||||
find(:first, *args)
|
||||
end
|
||||
|
||||
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
|
||||
# same arguments to this method as you can to <tt>find(:last)</tt>.
|
||||
def last(*args)
|
||||
find(:last, *args)
|
||||
end
|
||||
|
||||
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
|
||||
# to find(:all)
|
||||
def all(*args)
|
||||
find(:all, *args)
|
||||
end
|
||||
|
||||
# Executes a custom SQL query against your database and returns all the results. The results will
|
||||
# be returned as an array with columns requested encapsulated as attributes of the model you call
|
||||
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
|
||||
# a Product object with the attributes you specified in the SQL query.
|
||||
#
|
||||
# If you call a complicated SQL query which spans multiple tables the columns specified by the
|
||||
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
|
||||
# table.
|
||||
#
|
||||
# The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
|
||||
# no database agnostic conversions performed. This should be a last resort because using, for example,
|
||||
# MySQL specific terms will lock you to using that particular database engine or require you to
|
||||
# change your call if you switch engines.
|
||||
#
|
||||
# ==== Examples
|
||||
# # A simple SQL query spanning multiple tables
|
||||
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
|
||||
# > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
|
||||
#
|
||||
# # You can use the same string replacement techniques as you can with ActiveRecord#find
|
||||
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
|
||||
# > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...]
|
||||
def find_by_sql(sql)
|
||||
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
|
||||
end
|
||||
|
||||
# Returns true if a record exists in the table that matches the +id+ or
|
||||
# conditions given, or false otherwise. The argument can take five forms:
|
||||
#
|
||||
# * Integer - Finds the record with this primary key.
|
||||
# * String - Finds the record with a primary key corresponding to this
|
||||
# string (such as <tt>'5'</tt>).
|
||||
# * Array - Finds the record that matches these +find+-style conditions
|
||||
# (such as <tt>['color = ?', 'red']</tt>).
|
||||
# * Hash - Finds the record that matches these +find+-style conditions
|
||||
# (such as <tt>{:color => 'red'}</tt>).
|
||||
# * No args - Returns false if the table is empty, true otherwise.
|
||||
#
|
||||
# For more information about specifying conditions as a Hash or Array,
|
||||
# see the Conditions section in the introduction to ActiveRecord::Base.
|
||||
#
|
||||
# Note: You can't pass in a condition as a string (like <tt>name =
|
||||
# 'Jamie'</tt>), since it would be sanitized and then queried against
|
||||
# the primary key column, like <tt>id = 'name = \'Jamie\''</tt>.
|
||||
#
|
||||
# ==== Examples
|
||||
# Person.exists?(5)
|
||||
# Person.exists?('5')
|
||||
# Person.exists?(:name => "David")
|
||||
# Person.exists?(['name LIKE ?', "%#{query}%"])
|
||||
# Person.exists?
|
||||
def exists?(id_or_conditions = {})
|
||||
find_initial(
|
||||
:select => "#{quoted_table_name}.#{primary_key}",
|
||||
:conditions => expand_id_conditions(id_or_conditions)) ? true : false
|
||||
end
|
||||
|
||||
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
|
||||
# The resulting object is returned whether the object was saved successfully to the database or not.
|
||||
#
|
||||
# The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
|
||||
# attributes on the objects that are to be created.
|
||||
#
|
||||
# ==== Examples
|
||||
# # Create a single new object
|
||||
# User.create(:first_name => 'Jamie')
|
||||
#
|
||||
# # Create an Array of new objects
|
||||
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
|
||||
#
|
||||
# # Create a single object and pass it into a block to set other attributes.
|
||||
# User.create(:first_name => 'Jamie') do |u|
|
||||
# u.is_admin = false
|
||||
# end
|
||||
#
|
||||
# # Creating an Array of new objects using a block, where the block is executed for each object:
|
||||
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
|
||||
# u.is_admin = false
|
||||
# end
|
||||
def create(attributes = nil, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| create(attr, &block) }
|
||||
else
|
||||
object = new(attributes)
|
||||
yield(object) if block_given?
|
||||
object.save
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
|
||||
# The resulting object is returned whether the object was saved successfully to the database or not.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - This should be the id or an array of ids to be updated.
|
||||
# * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Updating one record:
|
||||
# Person.update(15, :user_name => 'Samuel', :group => 'expert')
|
||||
#
|
||||
# # Updating multiple records:
|
||||
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
|
||||
# Person.update(people.keys, people.values)
|
||||
def update(id, attributes)
|
||||
if id.is_a?(Array)
|
||||
idx = -1
|
||||
id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
|
||||
else
|
||||
object = find(id)
|
||||
object.update_attributes(attributes)
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes the row with a primary key matching the +id+ argument, using a
|
||||
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
|
||||
# Record objects are not instantiated, so the object's callbacks are not
|
||||
# executed, including any <tt>:dependent</tt> association options or
|
||||
# Observer methods.
|
||||
#
|
||||
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
|
||||
#
|
||||
# Note: Although it is often much faster than the alternative,
|
||||
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
|
||||
# your application that ensures referential integrity or performs other
|
||||
# essential jobs.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Delete a single row
|
||||
# Todo.delete(1)
|
||||
#
|
||||
# # Delete multiple rows
|
||||
# Todo.delete([2,3,4])
|
||||
def delete(id)
|
||||
delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
|
||||
end
|
||||
|
||||
# Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
|
||||
# therefore all callbacks and filters are fired off before the object is deleted. This method is
|
||||
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
|
||||
#
|
||||
# This essentially finds the object (or multiple objects) with the given id, creates a new object
|
||||
# from the attributes, and then calls destroy on it.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - Can be either an Integer or an Array of Integers.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Destroy a single object
|
||||
# Todo.destroy(1)
|
||||
#
|
||||
# # Destroy multiple objects
|
||||
# todos = [1,2,3]
|
||||
# Todo.destroy(todos)
|
||||
def destroy(id)
|
||||
if id.is_a?(Array)
|
||||
id.map { |one_id| destroy(one_id) }
|
||||
else
|
||||
find(id).destroy
|
||||
end
|
||||
end
|
||||
|
||||
# Updates all records with details given if they match a set of conditions supplied, limits and order can
|
||||
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
|
||||
# database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
|
||||
# * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
|
||||
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Update all billing objects with the 3 different attributes given
|
||||
# Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" )
|
||||
#
|
||||
# # Update records that match our conditions
|
||||
# Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" )
|
||||
#
|
||||
# # Update records that match our conditions but limit it to 5 ordered by date
|
||||
# Billing.update_all( "author = 'David'", "title LIKE '%Rails%'",
|
||||
# :order => 'created_at', :limit => 5 )
|
||||
def update_all(updates, conditions = nil, options = {})
|
||||
sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} "
|
||||
|
||||
scope = scope(:find)
|
||||
|
||||
select_sql = ""
|
||||
add_conditions!(select_sql, conditions, scope)
|
||||
|
||||
if options.has_key?(:limit) || (scope && scope[:limit])
|
||||
# Only take order from scope if limit is also provided by scope, this
|
||||
# is useful for updating a has_many association with a limit.
|
||||
add_order!(select_sql, options[:order], scope)
|
||||
|
||||
add_limit!(select_sql, options, scope)
|
||||
sql.concat(connection.limited_update_conditions(select_sql, quoted_table_name, connection.quote_column_name(primary_key)))
|
||||
else
|
||||
add_order!(select_sql, options[:order], nil)
|
||||
sql.concat(select_sql)
|
||||
end
|
||||
|
||||
connection.update(sql, "#{name} Update")
|
||||
end
|
||||
|
||||
# Destroys the records matching +conditions+ by instantiating each
|
||||
# record and calling its +destroy+ method. Each object's callbacks are
|
||||
# executed (including <tt>:dependent</tt> association options and
|
||||
# +before_destroy+/+after_destroy+ Observer methods). Returns the
|
||||
# collection of objects that were destroyed; each will be frozen, to
|
||||
# reflect that no changes should be made (since they can't be
|
||||
# persisted).
|
||||
#
|
||||
# Note: Instantiation, callback execution, and deletion of each
|
||||
# record can be time consuming when you're removing many records at
|
||||
# once. It generates at least one SQL +DELETE+ query per record (or
|
||||
# possibly more, to enforce your callbacks). If you want to delete many
|
||||
# rows quickly, without concern for their associations or callbacks, use
|
||||
# +delete_all+ instead.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +conditions+ - A string, array, or hash that specifies which records
|
||||
# to destroy. If omitted, all records are destroyed. See the
|
||||
# Conditions section in the introduction to ActiveRecord::Base for
|
||||
# more information.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Person.destroy_all("last_login < '2004-04-04'")
|
||||
# Person.destroy_all(:status => "inactive")
|
||||
def destroy_all(conditions = nil)
|
||||
find(:all, :conditions => conditions).each { |object| object.destroy }
|
||||
end
|
||||
|
||||
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
|
||||
# calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
|
||||
# goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
|
||||
# though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
|
||||
# the number of rows affected.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +conditions+ - Conditions are specified the same way as with +find+ method.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
|
||||
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
|
||||
#
|
||||
# Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent
|
||||
# associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead.
|
||||
def delete_all(conditions = nil)
|
||||
sql = "DELETE FROM #{quoted_table_name} "
|
||||
add_conditions!(sql, conditions, scope(:find))
|
||||
connection.delete(sql, "#{name} Delete all")
|
||||
end
|
||||
|
||||
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
|
||||
# The use of this method should be restricted to complicated SQL queries that can't be executed
|
||||
# using the ActiveRecord::Calculations class methods. Look into those before using this.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +sql+ - An SQL statement which should return a count query from the database, see the example below.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
|
||||
def count_by_sql(sql)
|
||||
sql = sanitize_conditions(sql)
|
||||
connection.select_value(sql, "#{name} Count").to_i
|
||||
end
|
||||
|
||||
# A generic "counter updater" implementation, intended primarily to be
|
||||
# used by increment_counter and decrement_counter, but which may also
|
||||
# be useful on its own. It simply does a direct SQL update for the record
|
||||
# with the given ID, altering the given hash of counters by the amount
|
||||
# given by the corresponding value:
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
|
||||
# * +counters+ - An Array of Hashes containing the names of the fields
|
||||
# to update as keys and the amount to update the field by as values.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # For the Post with id of 5, decrement the comment_count by 1, and
|
||||
# # increment the action_count by 1
|
||||
# Post.update_counters 5, :comment_count => -1, :action_count => 1
|
||||
# # Executes the following SQL:
|
||||
# # UPDATE posts
|
||||
# # SET comment_count = comment_count - 1,
|
||||
# # action_count = action_count + 1
|
||||
# # WHERE id = 5
|
||||
#
|
||||
# # For the Posts with id of 10 and 15, increment the comment_count by 1
|
||||
# Post.update_counters [10, 15], :comment_count => 1
|
||||
# # Executes the following SQL:
|
||||
# # UPDATE posts
|
||||
# # SET comment_count = comment_count + 1,
|
||||
# # WHERE id IN (10, 15)
|
||||
def update_counters(id, counters)
|
||||
updates = counters.inject([]) { |list, (counter_name, increment)|
|
||||
sign = increment < 0 ? "-" : "+"
|
||||
list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
|
||||
}.join(", ")
|
||||
|
||||
if id.is_a?(Array)
|
||||
ids_list = id.map {|i| quote_value(i)}.join(', ')
|
||||
condition = "IN (#{ids_list})"
|
||||
else
|
||||
condition = "= #{quote_value(id)}"
|
||||
end
|
||||
|
||||
update_all(updates, "#{connection.quote_column_name(primary_key)} #{condition}")
|
||||
end
|
||||
|
||||
# Increment a number field by one, usually representing a count.
|
||||
#
|
||||
# This is used for caching aggregate values, so that they don't need to be computed every time.
|
||||
# For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
|
||||
# shown it would have to run an SQL query to find how many posts and comments there are.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +counter_name+ - The name of the field that should be incremented.
|
||||
# * +id+ - The id of the object that should be incremented.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Increment the post_count column for the record with an id of 5
|
||||
# DiscussionBoard.increment_counter(:post_count, 5)
|
||||
def increment_counter(counter_name, id)
|
||||
update_counters(id, counter_name => 1)
|
||||
end
|
||||
|
||||
# Decrement a number field by one, usually representing a count.
|
||||
#
|
||||
# This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +counter_name+ - The name of the field that should be decremented.
|
||||
# * +id+ - The id of the object that should be decremented.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # Decrement the post_count column for the record with an id of 5
|
||||
# DiscussionBoard.decrement_counter(:post_count, 5)
|
||||
def decrement_counter(counter_name, id)
|
||||
update_counters(id, counter_name => -1)
|
||||
end
|
||||
|
||||
# Attributes named in this macro are protected from mass-assignment,
|
||||
# such as <tt>new(attributes)</tt>,
|
||||
# <tt>update_attributes(attributes)</tt>, or
|
||||
# <tt>attributes=(attributes)</tt>.
|
||||
#
|
||||
# Mass-assignment to these attributes will simply be ignored, to assign
|
||||
# to them you can use direct writer methods. This is meant to protect
|
||||
# sensitive attributes from being overwritten by malicious users
|
||||
# tampering with URLs or forms.
|
||||
#
|
||||
# class Customer < ActiveRecord::Base
|
||||
# attr_protected :credit_rating
|
||||
# end
|
||||
#
|
||||
# customer = Customer.new("name" => David, "credit_rating" => "Excellent")
|
||||
# customer.credit_rating # => nil
|
||||
# customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
|
||||
# customer.credit_rating # => nil
|
||||
#
|
||||
# customer.credit_rating = "Average"
|
||||
# customer.credit_rating # => "Average"
|
||||
#
|
||||
# To start from an all-closed default and enable attributes as needed,
|
||||
# have a look at +attr_accessible+.
|
||||
def attr_protected(*attributes)
|
||||
write_inheritable_attribute(:attr_protected, Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
|
||||
end
|
||||
|
||||
# Returns an array of all the attributes that have been protected from mass-assignment.
|
||||
def protected_attributes # :nodoc:
|
||||
read_inheritable_attribute(:attr_protected)
|
||||
end
|
||||
|
||||
# Specifies a white list of model attributes that can be set via
|
||||
# mass-assignment, such as <tt>new(attributes)</tt>,
|
||||
# <tt>update_attributes(attributes)</tt>, or
|
||||
# <tt>attributes=(attributes)</tt>
|
||||
#
|
||||
# This is the opposite of the +attr_protected+ macro: Mass-assignment
|
||||
# will only set attributes in this list, to assign to the rest of
|
||||
# attributes you can use direct writer methods. This is meant to protect
|
||||
# sensitive attributes from being overwritten by malicious users
|
||||
# tampering with URLs or forms. If you'd rather start from an all-open
|
||||
# default and restrict attributes as needed, have a look at
|
||||
# +attr_protected+.
|
||||
#
|
||||
# class Customer < ActiveRecord::Base
|
||||
# attr_accessible :name, :nickname
|
||||
# end
|
||||
#
|
||||
# customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
|
||||
# customer.credit_rating # => nil
|
||||
# customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
|
||||
# customer.credit_rating # => nil
|
||||
#
|
||||
# customer.credit_rating = "Average"
|
||||
# customer.credit_rating # => "Average"
|
||||
def attr_accessible(*attributes)
|
||||
write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
|
||||
end
|
||||
|
||||
# Returns an array of all the attributes that have been made accessible to mass-assignment.
|
||||
def accessible_attributes # :nodoc:
|
||||
read_inheritable_attribute(:attr_accessible)
|
||||
end
|
||||
|
||||
# Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
|
||||
def attr_readonly(*attributes)
|
||||
write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
|
||||
end
|
||||
|
||||
# Returns an array of all the attributes that have been specified as readonly.
|
||||
def readonly_attributes
|
||||
read_inheritable_attribute(:attr_readonly)
|
||||
end
|
||||
|
||||
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
|
||||
# then specify the name of that attribute using this method and it will be handled automatically.
|
||||
# The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
|
||||
# class on retrieval or SerializationTypeMismatch will be raised.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +attr_name+ - The field name that should be serialized.
|
||||
# * +class_name+ - Optional, class name that the object type should be equal to.
|
||||
#
|
||||
# ==== Example
|
||||
# # Serialize a preferences attribute
|
||||
# class User
|
||||
# serialize :preferences
|
||||
# end
|
||||
def serialize(attr_name, class_name = Object)
|
||||
serialized_attributes[attr_name.to_s] = class_name
|
||||
end
|
||||
|
||||
# Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
|
||||
def serialized_attributes
|
||||
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
|
||||
end
|
||||
|
||||
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
|
||||
# directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used
|
||||
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
|
||||
# in Active Support, which knows almost all common English inflections. You can add new inflections in config/initializers/inflections.rb.
|
||||
#
|
||||
# Nested classes are given table names prefixed by the singular form of
|
||||
# the parent's table name. Enclosing modules are not considered.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# class Invoice < ActiveRecord::Base; end;
|
||||
# file class table_name
|
||||
# invoice.rb Invoice invoices
|
||||
#
|
||||
# class Invoice < ActiveRecord::Base; class Lineitem < ActiveRecord::Base; end; end;
|
||||
# file class table_name
|
||||
# invoice.rb Invoice::Lineitem invoice_lineitems
|
||||
#
|
||||
# module Invoice; class Lineitem < ActiveRecord::Base; end; end;
|
||||
# file class table_name
|
||||
# invoice/lineitem.rb Invoice::Lineitem lineitems
|
||||
#
|
||||
# Additionally, the class-level +table_name_prefix+ is prepended and the
|
||||
# +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
|
||||
# the table name guess for an Invoice class becomes "myapp_invoices".
|
||||
# Invoice::Lineitem becomes "myapp_invoice_lineitems".
|
||||
#
|
||||
# You can also overwrite this class method to allow for unguessable
|
||||
# links, such as a Mouse class with a link to a "mice" table. Example:
|
||||
#
|
||||
# class Mouse < ActiveRecord::Base
|
||||
# set_table_name "mice"
|
||||
# end
|
||||
def table_name
|
||||
reset_table_name
|
||||
end
|
||||
|
||||
def reset_table_name #:nodoc:
|
||||
base = base_class
|
||||
|
||||
name =
|
||||
# STI subclasses always use their superclass' table.
|
||||
unless self == base
|
||||
base.table_name
|
||||
else
|
||||
# Nested classes are prefixed with singular parent table name.
|
||||
if parent < ActiveRecord::Base && !parent.abstract_class?
|
||||
contained = parent.table_name
|
||||
contained = contained.singularize if parent.pluralize_table_names
|
||||
contained << '_'
|
||||
end
|
||||
name = "#{table_name_prefix}#{contained}#{undecorated_table_name(base.name)}#{table_name_suffix}"
|
||||
end
|
||||
|
||||
set_table_name(name)
|
||||
name
|
||||
end
|
||||
|
||||
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
|
||||
# primary_key_prefix_type setting, though.
|
||||
def primary_key
|
||||
reset_primary_key
|
||||
end
|
||||
|
||||
def reset_primary_key #:nodoc:
|
||||
key = get_primary_key(base_class.name)
|
||||
set_primary_key(key)
|
||||
key
|
||||
end
|
||||
|
||||
def get_primary_key(base_name) #:nodoc:
|
||||
key = 'id'
|
||||
case primary_key_prefix_type
|
||||
when :table_name
|
||||
key = base_name.to_s.foreign_key(false)
|
||||
when :table_name_with_underscore
|
||||
key = base_name.to_s.foreign_key
|
||||
end
|
||||
key
|
||||
end
|
||||
|
||||
# Defines the column name for use with single table inheritance
|
||||
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
|
||||
def inheritance_column
|
||||
@inheritance_column ||= "type".freeze
|
||||
end
|
||||
|
||||
# Lazy-set the sequence name to the connection's default. This method
|
||||
# is only ever called once since set_sequence_name overrides it.
|
||||
def sequence_name #:nodoc:
|
||||
reset_sequence_name
|
||||
end
|
||||
|
||||
def reset_sequence_name #:nodoc:
|
||||
default = connection.default_sequence_name(table_name, primary_key)
|
||||
set_sequence_name(default)
|
||||
default
|
||||
end
|
||||
|
||||
# Sets the table name to use to the given value, or (if the value
|
||||
# is nil or false) to the value returned by the given block.
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# set_table_name "project"
|
||||
# end
|
||||
def set_table_name(value = nil, &block)
|
||||
define_attr_method :table_name, value, &block
|
||||
end
|
||||
alias :table_name= :set_table_name
|
||||
|
||||
# Sets the name of the primary key column to use to the given value,
|
||||
# or (if the value is nil or false) to the value returned by the given
|
||||
# block.
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# set_primary_key "sysid"
|
||||
# end
|
||||
def set_primary_key(value = nil, &block)
|
||||
define_attr_method :primary_key, value, &block
|
||||
end
|
||||
alias :primary_key= :set_primary_key
|
||||
|
||||
# Sets the name of the inheritance column to use to the given value,
|
||||
# or (if the value # is nil or false) to the value returned by the
|
||||
# given block.
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# set_inheritance_column do
|
||||
# original_inheritance_column + "_id"
|
||||
# end
|
||||
# end
|
||||
def set_inheritance_column(value = nil, &block)
|
||||
define_attr_method :inheritance_column, value, &block
|
||||
end
|
||||
alias :inheritance_column= :set_inheritance_column
|
||||
|
||||
# Sets the name of the sequence to use when generating ids to the given
|
||||
# value, or (if the value is nil or false) to the value returned by the
|
||||
# given block. This is required for Oracle and is useful for any
|
||||
# database which relies on sequences for primary key generation.
|
||||
#
|
||||
# If a sequence name is not explicitly set when using Oracle or Firebird,
|
||||
# it will default to the commonly used pattern of: #{table_name}_seq
|
||||
#
|
||||
# If a sequence name is not explicitly set when using PostgreSQL, it
|
||||
# will discover the sequence corresponding to your primary key for you.
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# set_sequence_name "projectseq" # default would have been "project_seq"
|
||||
# end
|
||||
def set_sequence_name(value = nil, &block)
|
||||
define_attr_method :sequence_name, value, &block
|
||||
end
|
||||
alias :sequence_name= :set_sequence_name
|
||||
|
||||
# Turns the +table_name+ back into a class name following the reverse rules of +table_name+.
|
||||
def class_name(table_name = table_name) # :nodoc:
|
||||
# remove any prefix and/or suffix from the table name
|
||||
class_name = table_name[table_name_prefix.length..-(table_name_suffix.length + 1)].camelize
|
||||
class_name = class_name.singularize if pluralize_table_names
|
||||
class_name
|
||||
end
|
||||
|
||||
# Indicates whether the table associated with this class exists
|
||||
def table_exists?
|
||||
connection.table_exists?(table_name)
|
||||
end
|
||||
|
||||
# Returns an array of column objects for the table associated with this class.
|
||||
def columns
|
||||
unless defined?(@columns) && @columns
|
||||
@columns = connection.columns(table_name, "#{name} Columns")
|
||||
@columns.each { |column| column.primary = column.name == primary_key }
|
||||
end
|
||||
@columns
|
||||
end
|
||||
|
||||
# Returns a hash of column objects for the table associated with this class.
|
||||
def columns_hash
|
||||
@columns_hash ||= columns.inject({}) { |hash, column| hash[column.name] = column; hash }
|
||||
end
|
||||
|
||||
# Returns an array of column names as strings.
|
||||
def column_names
|
||||
@column_names ||= columns.map { |column| column.name }
|
||||
end
|
||||
|
||||
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
|
||||
# and columns used for single table inheritance have been removed.
|
||||
def content_columns
|
||||
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
|
||||
end
|
||||
|
||||
# Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
|
||||
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
|
||||
# is available.
|
||||
def column_methods_hash #:nodoc:
|
||||
@dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
|
||||
attr_name = attr.to_s
|
||||
methods[attr.to_sym] = attr_name
|
||||
methods["#{attr}=".to_sym] = attr_name
|
||||
methods["#{attr}?".to_sym] = attr_name
|
||||
methods["#{attr}_before_type_cast".to_sym] = attr_name
|
||||
methods
|
||||
end
|
||||
end
|
||||
|
||||
# Resets all the cached information about columns, which will cause them
|
||||
# to be reloaded on the next request.
|
||||
#
|
||||
# The most common usage pattern for this method is probably in a migration,
|
||||
# when just after creating a table you want to populate it with some default
|
||||
# values, eg:
|
||||
#
|
||||
# class CreateJobLevels < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :job_levels do |t|
|
||||
# t.integer :id
|
||||
# t.string :name
|
||||
#
|
||||
# t.timestamps
|
||||
# end
|
||||
#
|
||||
# JobLevel.reset_column_information
|
||||
# %w{assistant executive manager director}.each do |type|
|
||||
# JobLevel.create(:name => type)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# drop_table :job_levels
|
||||
# end
|
||||
# end
|
||||
def reset_column_information
|
||||
generated_methods.each { |name| undef_method(name) }
|
||||
@column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @generated_methods = @inheritance_column = nil
|
||||
end
|
||||
|
||||
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
|
||||
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
|
||||
end
|
||||
|
||||
def self_and_descendants_from_active_record#nodoc:
|
||||
klass = self
|
||||
classes = [klass]
|
||||
while klass != klass.base_class
|
||||
classes << klass = klass.superclass
|
||||
end
|
||||
classes
|
||||
rescue
|
||||
# OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
|
||||
# Appearantly the method base_class causes some trouble.
|
||||
# It now works for sure.
|
||||
[self]
|
||||
end
|
||||
|
||||
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
|
||||
# Person.human_attribute_name("first_name") # => "First name"
|
||||
# This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n
|
||||
# module now.
|
||||
# Specify +options+ with additional translating options.
|
||||
def human_attribute_name(attribute_key_name, options = {})
|
||||
defaults = self_and_descendants_from_active_record.map do |klass|
|
||||
:"#{klass.name.underscore}.#{attribute_key_name}"
|
||||
end
|
||||
defaults << options[:default] if options[:default]
|
||||
defaults.flatten!
|
||||
defaults << attribute_key_name.to_s.humanize
|
||||
options[:count] ||= 1
|
||||
I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
|
||||
end
|
||||
|
||||
# Transform the modelname into a more humane format, using I18n.
|
||||
# Defaults to the basic humanize method.
|
||||
# Default scope of the translation is activerecord.models
|
||||
# Specify +options+ with additional translating options.
|
||||
def human_name(options = {})
|
||||
defaults = self_and_descendants_from_active_record.map do |klass|
|
||||
:"#{klass.name.underscore}"
|
||||
end
|
||||
defaults << self.name.humanize
|
||||
I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
|
||||
end
|
||||
|
||||
# True if this isn't a concrete subclass needing a STI type condition.
|
||||
def descends_from_active_record?
|
||||
if superclass.abstract_class?
|
||||
superclass.descends_from_active_record?
|
||||
else
|
||||
superclass == Base || !columns_hash.include?(inheritance_column)
|
||||
end
|
||||
end
|
||||
|
||||
def finder_needs_type_condition? #:nodoc:
|
||||
# This is like this because benchmarking justifies the strange :false stuff
|
||||
:true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
|
||||
end
|
||||
|
||||
# Returns a string like 'Post id:integer, title:string, body:text'
|
||||
def inspect
|
||||
if self == Base
|
||||
super
|
||||
elsif abstract_class?
|
||||
"#{super}(abstract)"
|
||||
elsif table_exists?
|
||||
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
|
||||
"#{super}(#{attr_list})"
|
||||
else
|
||||
"#{super}(Table doesn't exist)"
|
||||
end
|
||||
end
|
||||
|
||||
def quote_value(value, column = nil) #:nodoc:
|
||||
connection.quote(value,column)
|
||||
end
|
||||
|
||||
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
|
||||
def sanitize(object) #:nodoc:
|
||||
connection.quote(object)
|
||||
end
|
||||
|
||||
# Log and benchmark multiple statements in a single block. Example:
|
||||
#
|
||||
# Project.benchmark("Creating project") do
|
||||
# project = Project.create("name" => "stuff")
|
||||
# project.create_manager("name" => "David")
|
||||
# project.milestones << Milestone.find(:all)
|
||||
# end
|
||||
#
|
||||
# The benchmark is only recorded if the current level of the logger is less than or equal to the <tt>log_level</tt>,
|
||||
# which makes it easy to include benchmarking statements in production software that will remain inexpensive because
|
||||
# the benchmark will only be conducted if the log level is low enough.
|
||||
#
|
||||
# The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false.
|
||||
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
|
||||
if logger && logger.level <= log_level
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
|
||||
logger.add(log_level, '%s (%.1fms)' % [title, ms])
|
||||
result
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Silences the logger for the duration of the block.
|
||||
def silence
|
||||
old_logger_level, logger.level = logger.level, Logger::ERROR if logger
|
||||
yield
|
||||
ensure
|
||||
logger.level = old_logger_level if logger
|
||||
end
|
||||
|
||||
# Overwrite the default class equality method to provide support for association proxies.
|
||||
def ===(object)
|
||||
object.is_a?(self)
|
||||
end
|
||||
|
||||
# Returns the base AR subclass that this class descends from. If A
|
||||
# extends AR::Base, A.base_class will return A. If B descends from A
|
||||
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
||||
def base_class
|
||||
class_of_active_record_descendant(self)
|
||||
end
|
||||
|
||||
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
|
||||
attr_accessor :abstract_class
|
||||
|
||||
# Returns whether this class is a base AR class. If A is a base class and
|
||||
# B descends from A, then B.base_class will return B.
|
||||
def abstract_class?
|
||||
defined?(@abstract_class) && @abstract_class == true
|
||||
end
|
||||
|
||||
def respond_to?(method_id, include_private = false)
|
||||
if match = DynamicFinderMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def sti_name
|
||||
store_full_sti_class ? name : name.demodulize
|
||||
end
|
||||
|
||||
# Merges conditions so that the result is a valid +condition+
|
||||
def merge_conditions(*conditions)
|
||||
segments = []
|
||||
|
||||
conditions.each do |condition|
|
||||
unless condition.blank?
|
||||
sql = sanitize_sql(condition)
|
||||
segments << sql unless sql.blank?
|
||||
end
|
||||
end
|
||||
|
||||
"(#{segments.join(') AND (')})" unless segments.empty?
|
||||
end
|
||||
|
||||
private
|
||||
def find_initial(options)
|
||||
options.update(:limit => 1)
|
||||
find_every(options).first
|
||||
end
|
||||
|
||||
def find_last(options)
|
||||
order = options[:order]
|
||||
|
||||
if order
|
||||
order = reverse_sql_order(order)
|
||||
elsif !scoped?(:find, :order)
|
||||
order = "#{table_name}.#{primary_key} DESC"
|
||||
end
|
||||
|
||||
if scoped?(:find, :order)
|
||||
scope = scope(:find)
|
||||
original_scoped_order = scope[:order]
|
||||
scope[:order] = reverse_sql_order(original_scoped_order)
|
||||
end
|
||||
|
||||
begin
|
||||
find_initial(options.merge({ :order => order }))
|
||||
ensure
|
||||
scope[:order] = original_scoped_order if original_scoped_order
|
||||
end
|
||||
end
|
||||
|
||||
def reverse_sql_order(order_query)
|
||||
reversed_query = order_query.to_s.split(/,/).each { |s|
|
||||
if s.match(/\s(asc|ASC)$/)
|
||||
s.gsub!(/\s(asc|ASC)$/, ' DESC')
|
||||
elsif s.match(/\s(desc|DESC)$/)
|
||||
s.gsub!(/\s(desc|DESC)$/, ' ASC')
|
||||
elsif !s.match(/\s(asc|ASC|desc|DESC)$/)
|
||||
s.concat(' DESC')
|
||||
end
|
||||
}.join(',')
|
||||
end
|
||||
|
||||
def find_every(options)
|
||||
include_associations = merge_includes(scope(:find, :include), options[:include])
|
||||
|
||||
if include_associations.any? && references_eager_loaded_tables?(options)
|
||||
records = find_with_associations(options)
|
||||
else
|
||||
records = find_by_sql(construct_finder_sql(options))
|
||||
if include_associations.any?
|
||||
preload_associations(records, include_associations)
|
||||
end
|
||||
end
|
||||
|
||||
records.each { |record| record.readonly! } if options[:readonly]
|
||||
|
||||
records
|
||||
end
|
||||
|
||||
def find_from_ids(ids, options)
|
||||
expects_array = ids.first.kind_of?(Array)
|
||||
return ids.first if expects_array && ids.first.empty?
|
||||
|
||||
ids = ids.flatten.compact.uniq
|
||||
|
||||
case ids.size
|
||||
when 0
|
||||
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
||||
when 1
|
||||
result = find_one(ids.first, options)
|
||||
expects_array ? [ result ] : result
|
||||
else
|
||||
find_some(ids, options)
|
||||
end
|
||||
end
|
||||
|
||||
def find_one(id, options)
|
||||
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
||||
options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"
|
||||
|
||||
# Use find_every(options).first since the primary key condition
|
||||
# already ensures we have a single record. Using find_initial adds
|
||||
# a superfluous :limit => 1.
|
||||
if result = find_every(options).first
|
||||
result
|
||||
else
|
||||
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
|
||||
end
|
||||
end
|
||||
|
||||
def find_some(ids, options)
|
||||
conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
|
||||
ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
|
||||
options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IN (#{ids_list})#{conditions}"
|
||||
|
||||
result = find_every(options)
|
||||
|
||||
# Determine expected size from limit and offset, not just ids.size.
|
||||
expected_size =
|
||||
if options[:limit] && ids.size > options[:limit]
|
||||
options[:limit]
|
||||
else
|
||||
ids.size
|
||||
end
|
||||
|
||||
# 11 ids with limit 3, offset 9 should give 2 results.
|
||||
if options[:offset] && (ids.size - options[:offset] < expected_size)
|
||||
expected_size = ids.size - options[:offset]
|
||||
end
|
||||
|
||||
if result.size == expected_size
|
||||
result
|
||||
else
|
||||
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
||||
end
|
||||
end
|
||||
|
||||
# Finder methods must instantiate through this method to work with the
|
||||
# single-table inheritance model that makes it possible to create
|
||||
# objects of different types from the same table.
|
||||
def instantiate(record)
|
||||
object =
|
||||
if subclass_name = record[inheritance_column]
|
||||
# No type given.
|
||||
if subclass_name.empty?
|
||||
allocate
|
||||
|
||||
else
|
||||
# Ignore type if no column is present since it was probably
|
||||
# pulled in from a sloppy join.
|
||||
unless columns_hash.include?(inheritance_column)
|
||||
allocate
|
||||
|
||||
else
|
||||
begin
|
||||
compute_type(subclass_name).allocate
|
||||
rescue NameError
|
||||
raise SubclassNotFound,
|
||||
"The single-table inheritance mechanism failed to locate the subclass: '#{record[inheritance_column]}'. " +
|
||||
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
|
||||
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
|
||||
"or overwrite #{self.to_s}.inheritance_column to use another column for that information."
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
allocate
|
||||
end
|
||||
|
||||
object.instance_variable_set("@attributes", record)
|
||||
object.instance_variable_set("@attributes_cache", Hash.new)
|
||||
|
||||
if object.respond_to_without_attributes?(:after_find)
|
||||
object.send(:callback, :after_find)
|
||||
end
|
||||
|
||||
if object.respond_to_without_attributes?(:after_initialize)
|
||||
object.send(:callback, :after_initialize)
|
||||
end
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
# Nest the type name in the same module as this class.
|
||||
# Bar is "MyApp::Business::Bar" relative to MyApp::Business::Foo
|
||||
def type_name_with_module(type_name)
|
||||
if store_full_sti_class
|
||||
type_name
|
||||
else
|
||||
(/^::/ =~ type_name) ? type_name : "#{parent.name}::#{type_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def default_select(qualified)
|
||||
if qualified
|
||||
quoted_table_name + '.*'
|
||||
else
|
||||
'*'
|
||||
end
|
||||
end
|
||||
|
||||
def construct_finder_sql(options)
|
||||
scope = scope(:find)
|
||||
sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
|
||||
sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
|
||||
|
||||
add_joins!(sql, options[:joins], scope)
|
||||
add_conditions!(sql, options[:conditions], scope)
|
||||
|
||||
add_group!(sql, options[:group], options[:having], scope)
|
||||
add_order!(sql, options[:order], scope)
|
||||
add_limit!(sql, options, scope)
|
||||
add_lock!(sql, options, scope)
|
||||
|
||||
sql
|
||||
end
|
||||
|
||||
# Merges includes so that the result is a valid +include+
|
||||
def merge_includes(first, second)
|
||||
(safe_to_array(first) + safe_to_array(second)).uniq
|
||||
end
|
||||
|
||||
def merge_joins(*joins)
|
||||
if joins.any?{|j| j.is_a?(String) || array_of_strings?(j) }
|
||||
joins = joins.collect do |join|
|
||||
join = [join] if join.is_a?(String)
|
||||
unless array_of_strings?(join)
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil)
|
||||
join = join_dependency.join_associations.collect { |assoc| assoc.association_join }
|
||||
end
|
||||
join
|
||||
end
|
||||
joins.flatten.map{|j| j.strip}.uniq
|
||||
else
|
||||
joins.collect{|j| safe_to_array(j)}.flatten.uniq
|
||||
end
|
||||
end
|
||||
|
||||
# Object#to_a is deprecated, though it does have the desired behavior
|
||||
def safe_to_array(o)
|
||||
case o
|
||||
when NilClass
|
||||
[]
|
||||
when Array
|
||||
o
|
||||
else
|
||||
[o]
|
||||
end
|
||||
end
|
||||
|
||||
def array_of_strings?(o)
|
||||
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
|
||||
end
|
||||
|
||||
def add_order!(sql, order, scope = :auto)
|
||||
scope = scope(:find) if :auto == scope
|
||||
scoped_order = scope[:order] if scope
|
||||
if order
|
||||
sql << " ORDER BY #{order}"
|
||||
if scoped_order && scoped_order != order
|
||||
sql << ", #{scoped_order}"
|
||||
end
|
||||
else
|
||||
sql << " ORDER BY #{scoped_order}" if scoped_order
|
||||
end
|
||||
end
|
||||
|
||||
def add_group!(sql, group, having, scope = :auto)
|
||||
if group
|
||||
sql << " GROUP BY #{group}"
|
||||
sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
|
||||
else
|
||||
scope = scope(:find) if :auto == scope
|
||||
if scope && (scoped_group = scope[:group])
|
||||
sql << " GROUP BY #{scoped_group}"
|
||||
sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The optional scope argument is for the current <tt>:find</tt> scope.
|
||||
def add_limit!(sql, options, scope = :auto)
|
||||
scope = scope(:find) if :auto == scope
|
||||
|
||||
if scope
|
||||
options[:limit] ||= scope[:limit]
|
||||
options[:offset] ||= scope[:offset]
|
||||
end
|
||||
|
||||
connection.add_limit_offset!(sql, options)
|
||||
end
|
||||
|
||||
# The optional scope argument is for the current <tt>:find</tt> scope.
|
||||
# The <tt>:lock</tt> option has precedence over a scoped <tt>:lock</tt>.
|
||||
def add_lock!(sql, options, scope = :auto)
|
||||
scope = scope(:find) if :auto == scope
|
||||
options = options.reverse_merge(:lock => scope[:lock]) if scope
|
||||
connection.add_lock!(sql, options)
|
||||
end
|
||||
|
||||
# The optional scope argument is for the current <tt>:find</tt> scope.
|
||||
def add_joins!(sql, joins, scope = :auto)
|
||||
scope = scope(:find) if :auto == scope
|
||||
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
|
||||
case merged_joins
|
||||
when Symbol, Hash, Array
|
||||
if array_of_strings?(merged_joins)
|
||||
sql << merged_joins.join(' ') + " "
|
||||
else
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
|
||||
sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
|
||||
end
|
||||
when String
|
||||
sql << " #{merged_joins} "
|
||||
end
|
||||
end
|
||||
|
||||
# Adds a sanitized version of +conditions+ to the +sql+ string. Note that the passed-in +sql+ string is changed.
|
||||
# The optional scope argument is for the current <tt>:find</tt> scope.
|
||||
def add_conditions!(sql, conditions, scope = :auto)
|
||||
scope = scope(:find) if :auto == scope
|
||||
conditions = [conditions]
|
||||
conditions << scope[:conditions] if scope
|
||||
conditions << type_condition if finder_needs_type_condition?
|
||||
merged_conditions = merge_conditions(*conditions)
|
||||
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
|
||||
end
|
||||
|
||||
def type_condition(table_alias=nil)
|
||||
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
|
||||
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
|
||||
type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
|
||||
condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
|
||||
end
|
||||
|
||||
" (#{type_condition}) "
|
||||
end
|
||||
|
||||
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
||||
def undecorated_table_name(class_name = base_class.name)
|
||||
table_name = class_name.to_s.demodulize.underscore
|
||||
table_name = table_name.pluralize if pluralize_table_names
|
||||
table_name
|
||||
end
|
||||
|
||||
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
|
||||
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
|
||||
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
|
||||
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
|
||||
#
|
||||
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
|
||||
# is actually <tt>find_all_by_amount(amount, options)</tt>.
|
||||
#
|
||||
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
||||
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
||||
# respectively.
|
||||
#
|
||||
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
|
||||
# attempts to use it do not run through method_missing.
|
||||
def method_missing(method_id, *arguments, &block)
|
||||
if match = DynamicFinderMatch.match(method_id)
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if match.finder?
|
||||
finder = match.finder
|
||||
bang = match.bang?
|
||||
# def self.find_by_login_and_activated(*args)
|
||||
# options = args.extract_options!
|
||||
# attributes = construct_attributes_from_arguments(
|
||||
# [:login,:activated],
|
||||
# args
|
||||
# )
|
||||
# finder_options = { :conditions => attributes }
|
||||
# validate_find_options(options)
|
||||
# set_readonly_option!(options)
|
||||
#
|
||||
# if options[:conditions]
|
||||
# with_scope(:find => finder_options) do
|
||||
# find(:first, options)
|
||||
# end
|
||||
# else
|
||||
# find(:first, options.merge(finder_options))
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args)
|
||||
options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments(
|
||||
[:#{attribute_names.join(',:')}],
|
||||
args
|
||||
)
|
||||
finder_options = { :conditions => attributes }
|
||||
validate_find_options(options)
|
||||
set_readonly_option!(options)
|
||||
|
||||
#{'result = ' if bang}if options[:conditions]
|
||||
with_scope(:find => finder_options) do
|
||||
find(:#{finder}, options)
|
||||
end
|
||||
else
|
||||
find(:#{finder}, options.merge(finder_options))
|
||||
end
|
||||
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments)
|
||||
elsif match.instantiator?
|
||||
instantiator = match.instantiator
|
||||
# def self.find_or_create_by_user_id(*args)
|
||||
# guard_protected_attributes = false
|
||||
#
|
||||
# if args[0].is_a?(Hash)
|
||||
# guard_protected_attributes = true
|
||||
# attributes = args[0].with_indifferent_access
|
||||
# find_attributes = attributes.slice(*[:user_id])
|
||||
# else
|
||||
# find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
|
||||
# end
|
||||
#
|
||||
# options = { :conditions => find_attributes }
|
||||
# set_readonly_option!(options)
|
||||
#
|
||||
# record = find(:first, options)
|
||||
#
|
||||
# if record.nil?
|
||||
# record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
||||
# yield(record) if block_given?
|
||||
# record.save
|
||||
# record
|
||||
# else
|
||||
# record
|
||||
# end
|
||||
# end
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args)
|
||||
guard_protected_attributes = false
|
||||
|
||||
if args[0].is_a?(Hash)
|
||||
guard_protected_attributes = true
|
||||
attributes = args[0].with_indifferent_access
|
||||
find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
|
||||
else
|
||||
find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
|
||||
end
|
||||
|
||||
options = { :conditions => find_attributes }
|
||||
set_readonly_option!(options)
|
||||
|
||||
record = find(:first, options)
|
||||
|
||||
if record.nil?
|
||||
record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
||||
#{'yield(record) if block_given?'}
|
||||
#{'record.save' if instantiator == :create}
|
||||
record
|
||||
else
|
||||
record
|
||||
end
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments, &block)
|
||||
end
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if match.scope?
|
||||
self.class_eval %{
|
||||
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments(
|
||||
[:#{attribute_names.join(',:')}], args # [:user_name, :password], args
|
||||
) # )
|
||||
#
|
||||
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
||||
end # end
|
||||
}, __FILE__, __LINE__
|
||||
send(method_id, *arguments)
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def construct_attributes_from_arguments(attribute_names, arguments)
|
||||
attributes = {}
|
||||
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
||||
attributes
|
||||
end
|
||||
|
||||
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
|
||||
def expand_attribute_names_for_aggregates(attribute_names)
|
||||
expanded_attribute_names = []
|
||||
attribute_names.each do |attribute_name|
|
||||
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
|
||||
aggregate_mapping(aggregation).each do |field_attr, aggregate_attr|
|
||||
expanded_attribute_names << field_attr
|
||||
end
|
||||
else
|
||||
expanded_attribute_names << attribute_name
|
||||
end
|
||||
end
|
||||
expanded_attribute_names
|
||||
end
|
||||
|
||||
def all_attributes_exists?(attribute_names)
|
||||
attribute_names = expand_attribute_names_for_aggregates(attribute_names)
|
||||
attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
|
||||
end
|
||||
|
||||
def attribute_condition(quoted_column_name, argument)
|
||||
case argument
|
||||
when nil then "#{quoted_column_name} IS ?"
|
||||
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "#{quoted_column_name} IN (?)"
|
||||
when Range then if argument.exclude_end?
|
||||
"#{quoted_column_name} >= ? AND #{quoted_column_name} < ?"
|
||||
else
|
||||
"#{quoted_column_name} BETWEEN ? AND ?"
|
||||
end
|
||||
else "#{quoted_column_name} = ?"
|
||||
end
|
||||
end
|
||||
|
||||
# Interpret Array and Hash as conditions and anything else as an id.
|
||||
def expand_id_conditions(id_or_conditions)
|
||||
case id_or_conditions
|
||||
when Array, Hash then id_or_conditions
|
||||
else sanitize_sql(primary_key => id_or_conditions)
|
||||
end
|
||||
end
|
||||
|
||||
# Defines an "attribute" method (like +inheritance_column+ or
|
||||
# +table_name+). A new (class) method will be created with the
|
||||
# given name. If a value is specified, the new method will
|
||||
# return that value (as a string). Otherwise, the given block
|
||||
# will be used to compute the value of the method.
|
||||
#
|
||||
# The original method will be aliased, with the new name being
|
||||
# prefixed with "original_". This allows the new method to
|
||||
# access the original value.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class A < ActiveRecord::Base
|
||||
# define_attr_method :primary_key, "sysid"
|
||||
# define_attr_method( :inheritance_column ) do
|
||||
# original_inheritance_column + "_id"
|
||||
# end
|
||||
# end
|
||||
def define_attr_method(name, value=nil, &block)
|
||||
sing = class << self; self; end
|
||||
sing.send :alias_method, "original_#{name}", name
|
||||
if block_given?
|
||||
sing.send :define_method, name, &block
|
||||
else
|
||||
# use eval instead of a block to work around a memory leak in dev
|
||||
# mode in fcgi
|
||||
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
|
||||
# method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
|
||||
# <tt>:include</tt>, <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options. <tt>:create</tt> parameters are an attributes hash.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.create_with_scope
|
||||
# with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do
|
||||
# find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
|
||||
# a = create(1)
|
||||
# a.blog_id # => 1
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
|
||||
# <tt>:conditions</tt>, <tt>:include</tt>, and <tt>:joins</tt> options in <tt>:find</tt>, which are merged.
|
||||
#
|
||||
# <tt>:joins</tt> options are uniqued so multiple scopes can join in the same table without table aliasing
|
||||
# problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
|
||||
# array of strings format for your joins.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.find_with_scope
|
||||
# with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do
|
||||
# with_scope(:find => { :limit => 10 })
|
||||
# find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
|
||||
# end
|
||||
# with_scope(:find => { :conditions => "author_id = 3" })
|
||||
# find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.find_with_exclusive_scope
|
||||
# with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do
|
||||
# with_exclusive_scope(:find => { :limit => 10 })
|
||||
# find(:all) # => SELECT * from articles LIMIT 10
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# *Note*: the +:find+ scope also has effect on update and deletion methods,
|
||||
# like +update_all+ and +delete_all+.
|
||||
def with_scope(method_scoping = {}, action = :merge, &block)
|
||||
method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
|
||||
|
||||
# Dup first and second level of hash (method and params).
|
||||
method_scoping = method_scoping.inject({}) do |hash, (method, params)|
|
||||
hash[method] = (params == true) ? params : params.dup
|
||||
hash
|
||||
end
|
||||
|
||||
method_scoping.assert_valid_keys([ :find, :create ])
|
||||
|
||||
if f = method_scoping[:find]
|
||||
f.assert_valid_keys(VALID_FIND_OPTIONS)
|
||||
set_readonly_option! f
|
||||
end
|
||||
|
||||
# Merge scopings
|
||||
if [:merge, :reverse_merge].include?(action) && current_scoped_methods
|
||||
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
|
||||
case hash[method]
|
||||
when Hash
|
||||
if method == :find
|
||||
(hash[method].keys + params.keys).uniq.each do |key|
|
||||
merge = hash[method][key] && params[key] # merge if both scopes have the same key
|
||||
if key == :conditions && merge
|
||||
if params[key].is_a?(Hash) && hash[method][key].is_a?(Hash)
|
||||
hash[method][key] = merge_conditions(hash[method][key].deep_merge(params[key]))
|
||||
else
|
||||
hash[method][key] = merge_conditions(params[key], hash[method][key])
|
||||
end
|
||||
elsif key == :include && merge
|
||||
hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
|
||||
elsif key == :joins && merge
|
||||
hash[method][key] = merge_joins(params[key], hash[method][key])
|
||||
else
|
||||
hash[method][key] = hash[method][key] || params[key]
|
||||
end
|
||||
end
|
||||
else
|
||||
if action == :reverse_merge
|
||||
hash[method] = hash[method].merge(params)
|
||||
else
|
||||
hash[method] = params.merge(hash[method])
|
||||
end
|
||||
end
|
||||
else
|
||||
hash[method] = params
|
||||
end
|
||||
hash
|
||||
end
|
||||
end
|
||||
|
||||
self.scoped_methods << method_scoping
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
self.scoped_methods.pop
|
||||
end
|
||||
end
|
||||
|
||||
# Works like with_scope, but discards any nested properties.
|
||||
def with_exclusive_scope(method_scoping = {}, &block)
|
||||
with_scope(method_scoping, :overwrite, &block)
|
||||
end
|
||||
|
||||
def subclasses #:nodoc:
|
||||
@@subclasses[self] ||= []
|
||||
@@subclasses[self] + extra = @@subclasses[self].inject([]) {|list, subclass| list + subclass.subclasses }
|
||||
end
|
||||
|
||||
# Sets the default options for the model. The format of the
|
||||
# <tt>options</tt> argument is the same as in find.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# default_scope :order => 'last_name, first_name'
|
||||
# end
|
||||
def default_scope(options = {})
|
||||
self.default_scoping << { :find => options, :create => options[:conditions].is_a?(Hash) ? options[:conditions] : {} }
|
||||
end
|
||||
|
||||
# Test whether the given method and optional key are scoped.
|
||||
def scoped?(method, key = nil) #:nodoc:
|
||||
if current_scoped_methods && (scope = current_scoped_methods[method])
|
||||
!key || !scope[key].nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Retrieve the scope for the given method and optional key.
|
||||
def scope(method, key = nil) #:nodoc:
|
||||
if current_scoped_methods && (scope = current_scoped_methods[method])
|
||||
key ? scope[key] : scope
|
||||
end
|
||||
end
|
||||
|
||||
def scoped_methods #:nodoc:
|
||||
Thread.current[:"#{self}_scoped_methods"] ||= self.default_scoping.dup
|
||||
end
|
||||
|
||||
def current_scoped_methods #:nodoc:
|
||||
scoped_methods.last
|
||||
end
|
||||
|
||||
# Returns the class type of the record using the current module as a prefix. So descendants of
|
||||
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
||||
def compute_type(type_name)
|
||||
modularized_name = type_name_with_module(type_name)
|
||||
silence_warnings do
|
||||
begin
|
||||
class_eval(modularized_name, __FILE__, __LINE__)
|
||||
rescue NameError
|
||||
class_eval(type_name, __FILE__, __LINE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the class descending directly from ActiveRecord::Base or an
|
||||
# abstract class, if any, in the inheritance hierarchy.
|
||||
def class_of_active_record_descendant(klass)
|
||||
if klass.superclass == Base || klass.superclass.abstract_class?
|
||||
klass
|
||||
elsif klass.superclass.nil?
|
||||
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
|
||||
else
|
||||
class_of_active_record_descendant(klass.superclass)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the name of the class descending directly from Active Record in the inheritance hierarchy.
|
||||
def class_name_of_active_record_descendant(klass) #:nodoc:
|
||||
klass.base_class.name
|
||||
end
|
||||
|
||||
# Accepts an array, hash, or string of SQL conditions and sanitizes
|
||||
# them into a valid SQL fragment for a WHERE clause.
|
||||
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
|
||||
# { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
|
||||
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
|
||||
def sanitize_sql_for_conditions(condition, table_name = quoted_table_name)
|
||||
return nil if condition.blank?
|
||||
|
||||
case condition
|
||||
when Array; sanitize_sql_array(condition)
|
||||
when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
|
||||
else condition
|
||||
end
|
||||
end
|
||||
alias_method :sanitize_sql, :sanitize_sql_for_conditions
|
||||
|
||||
# Accepts an array, hash, or string of SQL conditions and sanitizes
|
||||
# them into a valid SQL fragment for a SET clause.
|
||||
# { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
|
||||
def sanitize_sql_for_assignment(assignments)
|
||||
case assignments
|
||||
when Array; sanitize_sql_array(assignments)
|
||||
when Hash; sanitize_sql_hash_for_assignment(assignments)
|
||||
else assignments
|
||||
end
|
||||
end
|
||||
|
||||
def aggregate_mapping(reflection)
|
||||
mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
|
||||
mapping.first.is_a?(Array) ? mapping : [mapping]
|
||||
end
|
||||
|
||||
# Accepts a hash of SQL conditions and replaces those attributes
|
||||
# that correspond to a +composed_of+ relationship with their expanded
|
||||
# aggregate attribute values.
|
||||
# Given:
|
||||
# class Person < ActiveRecord::Base
|
||||
# composed_of :address, :class_name => "Address",
|
||||
# :mapping => [%w(address_street street), %w(address_city city)]
|
||||
# end
|
||||
# Then:
|
||||
# { :address => Address.new("813 abc st.", "chicago") }
|
||||
# # => { :address_street => "813 abc st.", :address_city => "chicago" }
|
||||
def expand_hash_conditions_for_aggregates(attrs)
|
||||
expanded_attrs = {}
|
||||
attrs.each do |attr, value|
|
||||
unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
|
||||
mapping = aggregate_mapping(aggregation)
|
||||
mapping.each do |field_attr, aggregate_attr|
|
||||
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
|
||||
expanded_attrs[field_attr] = value
|
||||
else
|
||||
expanded_attrs[field_attr] = value.send(aggregate_attr)
|
||||
end
|
||||
end
|
||||
else
|
||||
expanded_attrs[attr] = value
|
||||
end
|
||||
end
|
||||
expanded_attrs
|
||||
end
|
||||
|
||||
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
|
||||
# { :name => "foo'bar", :group_id => 4 }
|
||||
# # => "name='foo''bar' and group_id= 4"
|
||||
# { :status => nil, :group_id => [1,2,3] }
|
||||
# # => "status IS NULL and group_id IN (1,2,3)"
|
||||
# { :age => 13..18 }
|
||||
# # => "age BETWEEN 13 AND 18"
|
||||
# { 'other_records.id' => 7 }
|
||||
# # => "`other_records`.`id` = 7"
|
||||
# { :other_records => { :id => 7 } }
|
||||
# # => "`other_records`.`id` = 7"
|
||||
# And for value objects on a composed_of relationship:
|
||||
# { :address => Address.new("123 abc st.", "chicago") }
|
||||
# # => "address_street='123 abc st.' and address_city='chicago'"
|
||||
def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name)
|
||||
attrs = expand_hash_conditions_for_aggregates(attrs)
|
||||
|
||||
conditions = attrs.map do |attr, value|
|
||||
table_name = default_table_name
|
||||
|
||||
unless value.is_a?(Hash)
|
||||
attr = attr.to_s
|
||||
|
||||
# Extract table name from qualified attribute names.
|
||||
if attr.include?('.')
|
||||
attr_table_name, attr = attr.split('.', 2)
|
||||
attr_table_name = connection.quote_table_name(attr_table_name)
|
||||
else
|
||||
attr_table_name = table_name
|
||||
end
|
||||
|
||||
attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value)
|
||||
else
|
||||
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
|
||||
end
|
||||
end.join(' AND ')
|
||||
|
||||
replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
|
||||
end
|
||||
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
|
||||
|
||||
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
|
||||
# { :status => nil, :group_id => 1 }
|
||||
# # => "status = NULL , group_id = 1"
|
||||
def sanitize_sql_hash_for_assignment(attrs)
|
||||
attrs.map do |attr, value|
|
||||
"#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
# Accepts an array of conditions. The array has each value
|
||||
# sanitized and interpolated into the SQL statement.
|
||||
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
|
||||
def sanitize_sql_array(ary)
|
||||
statement, *values = ary
|
||||
if values.first.is_a?(Hash) and statement =~ /:\w+/
|
||||
replace_named_bind_variables(statement, values.first)
|
||||
elsif statement.include?('?')
|
||||
replace_bind_variables(statement, values)
|
||||
else
|
||||
statement % values.collect { |value| connection.quote_string(value.to_s) }
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :sanitize_conditions, :sanitize_sql
|
||||
|
||||
def replace_bind_variables(statement, values) #:nodoc:
|
||||
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
|
||||
bound = values.dup
|
||||
statement.gsub('?') { quote_bound_value(bound.shift) }
|
||||
end
|
||||
|
||||
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
|
||||
statement.gsub(/(:?):([a-zA-Z]\w*)/) do
|
||||
if $1 == ':' # skip postgresql casts
|
||||
$& # return the whole match
|
||||
elsif bind_vars.include?(match = $2.to_sym)
|
||||
quote_bound_value(bind_vars[match])
|
||||
else
|
||||
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expand_range_bind_variables(bind_vars) #:nodoc:
|
||||
expanded = []
|
||||
|
||||
bind_vars.each do |var|
|
||||
next if var.is_a?(Hash)
|
||||
|
||||
if var.is_a?(Range)
|
||||
expanded << var.first
|
||||
expanded << var.last
|
||||
else
|
||||
expanded << var
|
||||
end
|
||||
end
|
||||
|
||||
expanded
|
||||
end
|
||||
|
||||
def quote_bound_value(value) #:nodoc:
|
||||
if value.respond_to?(:map) && !value.acts_like?(:string)
|
||||
if value.respond_to?(:empty?) && value.empty?
|
||||
connection.quote(nil)
|
||||
else
|
||||
value.map { |v| connection.quote(v) }.join(',')
|
||||
end
|
||||
else
|
||||
connection.quote(value)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
|
||||
unless expected == provided
|
||||
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
|
||||
end
|
||||
end
|
||||
|
||||
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
||||
:order, :select, :readonly, :group, :having, :from, :lock ]
|
||||
|
||||
def validate_find_options(options) #:nodoc:
|
||||
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
||||
end
|
||||
|
||||
def set_readonly_option!(options) #:nodoc:
|
||||
# Inherit :readonly from finder scope if set. Otherwise,
|
||||
# if :joins is not blank then :readonly defaults to true.
|
||||
unless options.has_key?(:readonly)
|
||||
if scoped_readonly = scope(:find, :readonly)
|
||||
options[:readonly] = scoped_readonly
|
||||
elsif !options[:joins].blank? && !options[:select]
|
||||
options[:readonly] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def encode_quoted_value(value) #:nodoc:
|
||||
quoted_value = connection.quote(value)
|
||||
quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) "
|
||||
quoted_value
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
# New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
|
||||
# attributes but not yet saved (pass a hash with key names matching the associated table column names).
|
||||
# In both instances, valid attribute keys are determined by the column names of the associated table --
|
||||
# hence you can't have attributes that aren't part of the table columns.
|
||||
def initialize(attributes = nil)
|
||||
@attributes = attributes_from_column_definition
|
||||
@attributes_cache = {}
|
||||
@new_record = true
|
||||
ensure_proper_type
|
||||
self.attributes = attributes unless attributes.nil?
|
||||
self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
|
||||
result = yield self if block_given?
|
||||
callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
|
||||
result
|
||||
end
|
||||
|
||||
# A model instance's primary key is always available as model.id
|
||||
# whether you name it the default 'id' or set it to something else.
|
||||
def id
|
||||
attr_name = self.class.primary_key
|
||||
column = column_for_attribute(attr_name)
|
||||
|
||||
self.class.send(:define_read_method, :id, attr_name, column)
|
||||
# now that the method exists, call it
|
||||
self.send attr_name.to_sym
|
||||
|
||||
end
|
||||
|
||||
# Returns a String, which Action Pack uses for constructing an URL to this
|
||||
# object. The default implementation returns this record's id as a String,
|
||||
# or nil if this record's unsaved.
|
||||
#
|
||||
# For example, suppose that you have a User model, and that you have a
|
||||
# <tt>map.resources :users</tt> route. Normally, +user_path+ will
|
||||
# construct a path with the user object's 'id' in it:
|
||||
#
|
||||
# user = User.find_by_name('Phusion')
|
||||
# user_path(user) # => "/users/1"
|
||||
#
|
||||
# You can override +to_param+ in your model to make +user_path+ construct
|
||||
# a path using the user's name instead of the user's id:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# def to_param # overridden
|
||||
# name
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# user = User.find_by_name('Phusion')
|
||||
# user_path(user) # => "/users/Phusion"
|
||||
def to_param
|
||||
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
||||
(id = self.id) ? id.to_s : nil # Be sure to stringify the id for routes
|
||||
end
|
||||
|
||||
# Returns a cache key that can be used to identify this record.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Product.new.cache_key # => "products/new"
|
||||
# Product.find(5).cache_key # => "products/5" (updated_at not available)
|
||||
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
|
||||
def cache_key
|
||||
case
|
||||
when new_record?
|
||||
"#{self.class.model_name.cache_key}/new"
|
||||
when timestamp = self[:updated_at]
|
||||
"#{self.class.model_name.cache_key}/#{id}-#{timestamp.to_s(:number)}"
|
||||
else
|
||||
"#{self.class.model_name.cache_key}/#{id}"
|
||||
end
|
||||
end
|
||||
|
||||
def id_before_type_cast #:nodoc:
|
||||
read_attribute_before_type_cast(self.class.primary_key)
|
||||
end
|
||||
|
||||
def quoted_id #:nodoc:
|
||||
quote_value(id, column_for_attribute(self.class.primary_key))
|
||||
end
|
||||
|
||||
# Sets the primary ID.
|
||||
def id=(value)
|
||||
write_attribute(self.class.primary_key, value)
|
||||
end
|
||||
|
||||
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
|
||||
def new_record?
|
||||
@new_record || false
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# save(perform_validation = true)
|
||||
#
|
||||
# Saves the model.
|
||||
#
|
||||
# If the model is new a record gets created in the database, otherwise
|
||||
# the existing record gets updated.
|
||||
#
|
||||
# If +perform_validation+ is true validations run. If any of them fail
|
||||
# the action is cancelled and +save+ returns +false+. If the flag is
|
||||
# false validations are bypassed altogether. See
|
||||
# ActiveRecord::Validations for more information.
|
||||
#
|
||||
# There's a series of callbacks associated with +save+. If any of the
|
||||
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
|
||||
# +save+ returns +false+. See ActiveRecord::Callbacks for further
|
||||
# details.
|
||||
def save
|
||||
create_or_update
|
||||
end
|
||||
|
||||
# Saves the model.
|
||||
#
|
||||
# If the model is new a record gets created in the database, otherwise
|
||||
# the existing record gets updated.
|
||||
#
|
||||
# With <tt>save!</tt> validations always run. If any of them fail
|
||||
# ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
|
||||
# for more information.
|
||||
#
|
||||
# There's a series of callbacks associated with <tt>save!</tt>. If any of
|
||||
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
|
||||
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
|
||||
# ActiveRecord::Callbacks for further details.
|
||||
def save!
|
||||
create_or_update || raise(RecordNotSaved)
|
||||
end
|
||||
|
||||
# Deletes the record in the database and freezes this instance to
|
||||
# reflect that no changes should be made (since they can't be
|
||||
# persisted). Returns the frozen instance.
|
||||
#
|
||||
# The row is simply removed with a SQL +DELETE+ statement on the
|
||||
# record's primary key, and no callbacks are executed.
|
||||
#
|
||||
# To enforce the object's +before_destroy+ and +after_destroy+
|
||||
# callbacks, Observer methods, or any <tt>:dependent</tt> association
|
||||
# options, use <tt>#destroy</tt>.
|
||||
def delete
|
||||
self.class.delete(id) unless new_record?
|
||||
@destroyed = true
|
||||
freeze
|
||||
end
|
||||
|
||||
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
||||
# be made (since they can't be persisted).
|
||||
def destroy
|
||||
unless new_record?
|
||||
connection.delete(
|
||||
"DELETE FROM #{self.class.quoted_table_name} " +
|
||||
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id}",
|
||||
"#{self.class.name} Destroy"
|
||||
)
|
||||
end
|
||||
|
||||
@destroyed = true
|
||||
freeze
|
||||
end
|
||||
|
||||
# Returns a clone of the record that hasn't been assigned an id yet and
|
||||
# is treated as a new record. Note that this is a "shallow" clone:
|
||||
# it copies the object's attributes only, not its associations.
|
||||
# The extent of a "deep" clone is application-specific and is therefore
|
||||
# left to the application to implement according to its need.
|
||||
def clone
|
||||
attrs = clone_attributes(:read_attribute_before_type_cast)
|
||||
attrs.delete(self.class.primary_key)
|
||||
record = self.class.new
|
||||
record.send :instance_variable_set, '@attributes', attrs
|
||||
record
|
||||
end
|
||||
|
||||
# Returns an instance of the specified +klass+ with the attributes of the current record. This is mostly useful in relation to
|
||||
# single-table inheritance structures where you want a subclass to appear as the superclass. This can be used along with record
|
||||
# identification in Action Pack to allow, say, <tt>Client < Company</tt> to do something like render <tt>:partial => @client.becomes(Company)</tt>
|
||||
# to render that instance using the companies/company partial instead of clients/client.
|
||||
#
|
||||
# Note: The new instance will share a link to the same attributes as the original class. So any change to the attributes in either
|
||||
# instance will affect the other.
|
||||
def becomes(klass)
|
||||
returning klass.new do |became|
|
||||
became.instance_variable_set("@attributes", @attributes)
|
||||
became.instance_variable_set("@attributes_cache", @attributes_cache)
|
||||
became.instance_variable_set("@new_record", new_record?)
|
||||
end
|
||||
end
|
||||
|
||||
# Updates a single attribute and saves the record without going through the normal validation procedure.
|
||||
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
|
||||
# in Base is replaced with this when the validations module is mixed in, which it is by default.
|
||||
def update_attribute(name, value)
|
||||
send(name.to_s + '=', value)
|
||||
save(false)
|
||||
end
|
||||
|
||||
# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
|
||||
# fail and false will be returned.
|
||||
def update_attributes(attributes)
|
||||
self.attributes = attributes
|
||||
save
|
||||
end
|
||||
|
||||
# Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid.
|
||||
def update_attributes!(attributes)
|
||||
self.attributes = attributes
|
||||
save!
|
||||
end
|
||||
|
||||
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
|
||||
# The increment is performed directly on the underlying attribute, no setter is invoked.
|
||||
# Only makes sense for number-based attributes. Returns +self+.
|
||||
def increment(attribute, by = 1)
|
||||
self[attribute] ||= 0
|
||||
self[attribute] += by
|
||||
self
|
||||
end
|
||||
|
||||
# Wrapper around +increment+ that saves the record. This method differs from
|
||||
# its non-bang version in that it passes through the attribute setter.
|
||||
# Saving is not subjected to validation checks. Returns +true+ if the
|
||||
# record could be saved.
|
||||
def increment!(attribute, by = 1)
|
||||
increment(attribute, by).update_attribute(attribute, self[attribute])
|
||||
end
|
||||
|
||||
# Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
|
||||
# The decrement is performed directly on the underlying attribute, no setter is invoked.
|
||||
# Only makes sense for number-based attributes. Returns +self+.
|
||||
def decrement(attribute, by = 1)
|
||||
self[attribute] ||= 0
|
||||
self[attribute] -= by
|
||||
self
|
||||
end
|
||||
|
||||
# Wrapper around +decrement+ that saves the record. This method differs from
|
||||
# its non-bang version in that it passes through the attribute setter.
|
||||
# Saving is not subjected to validation checks. Returns +true+ if the
|
||||
# record could be saved.
|
||||
def decrement!(attribute, by = 1)
|
||||
decrement(attribute, by).update_attribute(attribute, self[attribute])
|
||||
end
|
||||
|
||||
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
|
||||
# if the predicate returns +true+ the attribute will become +false+. This
|
||||
# method toggles directly the underlying value without calling any setter.
|
||||
# Returns +self+.
|
||||
def toggle(attribute)
|
||||
self[attribute] = !send("#{attribute}?")
|
||||
self
|
||||
end
|
||||
|
||||
# Wrapper around +toggle+ that saves the record. This method differs from
|
||||
# its non-bang version in that it passes through the attribute setter.
|
||||
# Saving is not subjected to validation checks. Returns +true+ if the
|
||||
# record could be saved.
|
||||
def toggle!(attribute)
|
||||
toggle(attribute).update_attribute(attribute, self[attribute])
|
||||
end
|
||||
|
||||
# Reloads the attributes of this object from the database.
|
||||
# The optional options argument is passed to find when reloading so you
|
||||
# may do e.g. record.reload(:lock => true) to reload the same record with
|
||||
# an exclusive row lock.
|
||||
def reload(options = nil)
|
||||
clear_aggregation_cache
|
||||
clear_association_cache
|
||||
@attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
|
||||
@attributes_cache = {}
|
||||
self
|
||||
end
|
||||
|
||||
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
||||
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
||||
# (Alias for the protected read_attribute method).
|
||||
def [](attr_name)
|
||||
read_attribute(attr_name)
|
||||
end
|
||||
|
||||
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
|
||||
# (Alias for the protected write_attribute method).
|
||||
def []=(attr_name, value)
|
||||
write_attribute(attr_name, value)
|
||||
end
|
||||
|
||||
# Allows you to set all the attributes at once by passing in a hash with keys
|
||||
# matching the attribute names (which again matches the column names).
|
||||
#
|
||||
# If +guard_protected_attributes+ is true (the default), then sensitive
|
||||
# attributes can be protected from this form of mass-assignment by using
|
||||
# the +attr_protected+ macro. Or you can alternatively specify which
|
||||
# attributes *can* be accessed with the +attr_accessible+ macro. Then all the
|
||||
# attributes not included in that won't be allowed to be mass-assigned.
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# attr_protected :is_admin
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
# user.attributes = { :username => 'Phusion', :is_admin => true }
|
||||
# user.username # => "Phusion"
|
||||
# user.is_admin? # => false
|
||||
#
|
||||
# user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false)
|
||||
# user.is_admin? # => true
|
||||
def attributes=(new_attributes, guard_protected_attributes = true)
|
||||
return if new_attributes.nil?
|
||||
attributes = new_attributes.dup
|
||||
attributes.stringify_keys!
|
||||
|
||||
multi_parameter_attributes = []
|
||||
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.include?("(")
|
||||
multi_parameter_attributes << [ k, v ]
|
||||
else
|
||||
respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
assign_multiparameter_attributes(multi_parameter_attributes)
|
||||
end
|
||||
|
||||
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
||||
def attributes
|
||||
self.attribute_names.inject({}) do |attrs, name|
|
||||
attrs[name] = read_attribute(name)
|
||||
attrs
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a hash of attributes before typecasting and deserialization.
|
||||
def attributes_before_type_cast
|
||||
self.attribute_names.inject({}) do |attrs, name|
|
||||
attrs[name] = read_attribute_before_type_cast(name)
|
||||
attrs
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an <tt>#inspect</tt>-like string for the value of the
|
||||
# attribute +attr_name+. String attributes are elided after 50
|
||||
# characters, and Date and Time attributes are returned in the
|
||||
# <tt>:db</tt> format. Other attributes return the value of
|
||||
# <tt>#inspect</tt> without modification.
|
||||
#
|
||||
# person = Person.create!(:name => "David Heinemeier Hansson " * 3)
|
||||
#
|
||||
# person.attribute_for_inspect(:name)
|
||||
# # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
|
||||
#
|
||||
# person.attribute_for_inspect(:created_at)
|
||||
# # => '"2009-01-12 04:48:57"'
|
||||
def attribute_for_inspect(attr_name)
|
||||
value = read_attribute(attr_name)
|
||||
|
||||
if value.is_a?(String) && value.length > 50
|
||||
"#{value[0..50]}...".inspect
|
||||
elsif value.is_a?(Date) || value.is_a?(Time)
|
||||
%("#{value.to_s(:db)}")
|
||||
else
|
||||
value.inspect
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
|
||||
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
|
||||
def attribute_present?(attribute)
|
||||
value = read_attribute(attribute)
|
||||
!value.blank?
|
||||
end
|
||||
|
||||
# Returns true if the given attribute is in the attributes hash
|
||||
def has_attribute?(attr_name)
|
||||
@attributes.has_key?(attr_name.to_s)
|
||||
end
|
||||
|
||||
# Returns an array of names for the attributes available on this object sorted alphabetically.
|
||||
def attribute_names
|
||||
@attributes.keys.sort
|
||||
end
|
||||
|
||||
# Returns the column object for the named attribute.
|
||||
def column_for_attribute(name)
|
||||
self.class.columns_hash[name.to_s]
|
||||
end
|
||||
|
||||
# Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
|
||||
def ==(comparison_object)
|
||||
comparison_object.equal?(self) ||
|
||||
(comparison_object.instance_of?(self.class) &&
|
||||
comparison_object.id == id &&
|
||||
!comparison_object.new_record?)
|
||||
end
|
||||
|
||||
# Delegates to ==
|
||||
def eql?(comparison_object)
|
||||
self == (comparison_object)
|
||||
end
|
||||
|
||||
# Delegates to id in order to allow two records of the same type and id to work with something like:
|
||||
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
||||
def hash
|
||||
id.hash
|
||||
end
|
||||
|
||||
# Freeze the attributes hash such that associations are still accessible, even on destroyed records.
|
||||
def freeze
|
||||
@attributes.freeze; self
|
||||
end
|
||||
|
||||
# Returns +true+ if the attributes hash has been frozen.
|
||||
def frozen?
|
||||
@attributes.frozen?
|
||||
end
|
||||
|
||||
# Returns +true+ if the record has been destroyed.
|
||||
def destroyed?
|
||||
@destroyed
|
||||
end
|
||||
|
||||
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
|
||||
# attributes will be marked as read only since they cannot be saved.
|
||||
def readonly?
|
||||
defined?(@readonly) && @readonly == true
|
||||
end
|
||||
|
||||
# Marks this record as read only.
|
||||
def readonly!
|
||||
@readonly = true
|
||||
end
|
||||
|
||||
# Returns the contents of the record as a nicely formatted string.
|
||||
def inspect
|
||||
attributes_as_nice_string = self.class.column_names.collect { |name|
|
||||
if has_attribute?(name) || new_record?
|
||||
"#{name}: #{attribute_for_inspect(name)}"
|
||||
end
|
||||
}.compact.join(", ")
|
||||
"#<#{self.class} #{attributes_as_nice_string}>"
|
||||
end
|
||||
|
||||
private
|
||||
def create_or_update
|
||||
raise ReadOnlyRecord if readonly?
|
||||
result = new_record? ? create : update
|
||||
result != false
|
||||
end
|
||||
|
||||
# Updates the associated record with values matching those of the instance attributes.
|
||||
# Returns the number of affected rows.
|
||||
def update(attribute_names = @attributes.keys)
|
||||
quoted_attributes = attributes_with_quotes(false, false, attribute_names)
|
||||
return 0 if quoted_attributes.empty?
|
||||
connection.update(
|
||||
"UPDATE #{self.class.quoted_table_name} " +
|
||||
"SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
|
||||
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
|
||||
"#{self.class.name} Update"
|
||||
)
|
||||
end
|
||||
|
||||
# Creates a record with values matching those of the instance attributes
|
||||
# and returns its id.
|
||||
def create
|
||||
if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
|
||||
self.id = connection.next_sequence_value(self.class.sequence_name)
|
||||
end
|
||||
|
||||
quoted_attributes = attributes_with_quotes
|
||||
|
||||
statement = if quoted_attributes.empty?
|
||||
connection.empty_insert_statement(self.class.table_name)
|
||||
else
|
||||
"INSERT INTO #{self.class.quoted_table_name} " +
|
||||
"(#{quoted_column_names.join(', ')}) " +
|
||||
"VALUES(#{quoted_attributes.values.join(', ')})"
|
||||
end
|
||||
|
||||
self.id = connection.insert(statement, "#{self.class.name} Create",
|
||||
self.class.primary_key, self.id, self.class.sequence_name)
|
||||
|
||||
@new_record = false
|
||||
id
|
||||
end
|
||||
|
||||
# Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord::Base descendant.
|
||||
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to do Reply.new without having to
|
||||
# set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. No such attribute would be set for objects of the
|
||||
# Message class in that example.
|
||||
def ensure_proper_type
|
||||
unless self.class.descends_from_active_record?
|
||||
write_attribute(self.class.inheritance_column, self.class.sti_name)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_number_column_value(value)
|
||||
if value == false
|
||||
0
|
||||
elsif value == true
|
||||
1
|
||||
elsif value.is_a?(String) && value.blank?
|
||||
nil
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def remove_attributes_protected_from_mass_assignment(attributes)
|
||||
safe_attributes =
|
||||
if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
|
||||
attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
||||
elsif self.class.protected_attributes.nil?
|
||||
attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
||||
elsif self.class.accessible_attributes.nil?
|
||||
attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
|
||||
else
|
||||
raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
|
||||
end
|
||||
|
||||
removed_attributes = attributes.keys - safe_attributes.keys
|
||||
|
||||
if removed_attributes.any?
|
||||
log_protected_attribute_removal(removed_attributes)
|
||||
end
|
||||
|
||||
safe_attributes
|
||||
end
|
||||
|
||||
# Removes attributes which have been marked as readonly.
|
||||
def remove_readonly_attributes(attributes)
|
||||
unless self.class.readonly_attributes.nil?
|
||||
attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) }
|
||||
else
|
||||
attributes
|
||||
end
|
||||
end
|
||||
|
||||
def log_protected_attribute_removal(*attributes)
|
||||
logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
|
||||
end
|
||||
|
||||
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
|
||||
def attributes_protected_by_default
|
||||
default = [ self.class.primary_key, self.class.inheritance_column ]
|
||||
default << 'id' unless self.class.primary_key.eql? 'id'
|
||||
default
|
||||
end
|
||||
|
||||
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
|
||||
# an SQL statement.
|
||||
def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
|
||||
quoted = {}
|
||||
connection = self.class.connection
|
||||
attribute_names.each do |name|
|
||||
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
|
||||
value = read_attribute(name)
|
||||
|
||||
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
|
||||
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
|
||||
value = value.to_yaml
|
||||
end
|
||||
|
||||
quoted[name] = connection.quote(value, column)
|
||||
end
|
||||
end
|
||||
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
|
||||
end
|
||||
|
||||
# Quote strings appropriately for SQL statements.
|
||||
def quote_value(value, column = nil)
|
||||
self.class.connection.quote(value, column)
|
||||
end
|
||||
|
||||
# Interpolate custom SQL string in instance context.
|
||||
# Optional record argument is meant for custom insert_sql.
|
||||
def interpolate_sql(sql, record = nil)
|
||||
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
||||
end
|
||||
|
||||
# Initializes the attributes array with keys matching the columns from the linked table and
|
||||
# the values matching the corresponding default value of that column, so
|
||||
# that a new instance, or one populated from a passed-in Hash, still has all the attributes
|
||||
# that instances loaded from the database would.
|
||||
def attributes_from_column_definition
|
||||
self.class.columns.inject({}) do |attributes, column|
|
||||
attributes[column.name] = column.default unless column.name == self.class.primary_key
|
||||
attributes
|
||||
end
|
||||
end
|
||||
|
||||
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
||||
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
||||
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
||||
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
||||
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum, f for Float,
|
||||
# s for String, and a for Array. If all the values for a given attribute are empty, the attribute will be set to nil.
|
||||
def assign_multiparameter_attributes(pairs)
|
||||
execute_callstack_for_multiparameter_attributes(
|
||||
extract_callstack_for_multiparameter_attributes(pairs)
|
||||
)
|
||||
end
|
||||
|
||||
def instantiate_time_object(name, values)
|
||||
if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
|
||||
Time.zone.local(*values)
|
||||
else
|
||||
Time.time_with_datetime_fallback(@@default_timezone, *values)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_callstack_for_multiparameter_attributes(callstack)
|
||||
errors = []
|
||||
callstack.each do |name, values_with_empty_parameters|
|
||||
begin
|
||||
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
||||
# in order to allow a date to be set without a year, we must keep the empty values.
|
||||
# Otherwise, we wouldn't be able to distinguish it from a date with an empty day.
|
||||
values = values_with_empty_parameters.reject(&:nil?)
|
||||
|
||||
if values.empty?
|
||||
send(name + "=", nil)
|
||||
else
|
||||
|
||||
value = if Time == klass
|
||||
instantiate_time_object(name, values)
|
||||
elsif Date == klass
|
||||
begin
|
||||
values = values_with_empty_parameters.collect do |v| v.nil? ? 1 : v end
|
||||
Date.new(*values)
|
||||
rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
|
||||
instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
||||
end
|
||||
else
|
||||
klass.new(*values)
|
||||
end
|
||||
|
||||
send(name + "=", value)
|
||||
end
|
||||
rescue => ex
|
||||
errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
|
||||
end
|
||||
end
|
||||
unless errors.empty?
|
||||
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
|
||||
end
|
||||
end
|
||||
|
||||
def extract_callstack_for_multiparameter_attributes(pairs)
|
||||
attributes = { }
|
||||
|
||||
for pair in pairs
|
||||
multiparameter_name, value = pair
|
||||
attribute_name = multiparameter_name.split("(").first
|
||||
attributes[attribute_name] = [] unless attributes.include?(attribute_name)
|
||||
|
||||
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
||||
attributes[attribute_name] << [ find_parameter_position(multiparameter_name), parameter_value ]
|
||||
end
|
||||
|
||||
attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
|
||||
end
|
||||
|
||||
def type_cast_attribute_value(multiparameter_name, value)
|
||||
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
|
||||
end
|
||||
|
||||
def find_parameter_position(multiparameter_name)
|
||||
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
|
||||
end
|
||||
|
||||
# Returns a comma-separated pair list, like "key1 = val1, key2 = val2".
|
||||
def comma_pair_list(hash)
|
||||
hash.inject([]) { |list, pair| list << "#{pair.first} = #{pair.last}" }.join(", ")
|
||||
end
|
||||
|
||||
def quoted_column_names(attributes = attributes_with_quotes)
|
||||
connection = self.class.connection
|
||||
attributes.keys.collect do |column_name|
|
||||
connection.quote_column_name(column_name)
|
||||
end
|
||||
end
|
||||
|
||||
def self.quoted_table_name
|
||||
self.connection.quote_table_name(self.table_name)
|
||||
end
|
||||
|
||||
def quote_columns(quoter, hash)
|
||||
hash.inject({}) do |quoted, (name, value)|
|
||||
quoted[quoter.quote_column_name(name)] = value
|
||||
quoted
|
||||
end
|
||||
end
|
||||
|
||||
def quoted_comma_pair_list(quoter, hash)
|
||||
comma_pair_list(quote_columns(quoter, hash))
|
||||
end
|
||||
|
||||
def object_from_yaml(string)
|
||||
return string unless string.is_a?(String) && string =~ /^---/
|
||||
YAML::load(string) rescue string
|
||||
end
|
||||
|
||||
def clone_attributes(reader_method = :read_attribute, attributes = {})
|
||||
self.attribute_names.inject(attributes) do |attrs, name|
|
||||
attrs[name] = clone_attribute_value(reader_method, name)
|
||||
attrs
|
||||
end
|
||||
end
|
||||
|
||||
def clone_attribute_value(reader_method, attribute_name)
|
||||
value = send(reader_method, attribute_name)
|
||||
value.duplicable? ? value.clone : value
|
||||
rescue TypeError, NoMethodError
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
Base.class_eval do
|
||||
extend QueryCache::ClassMethods
|
||||
include Validations
|
||||
include Locking::Optimistic, Locking::Pessimistic
|
||||
include AttributeMethods
|
||||
include Dirty
|
||||
include Callbacks, Observing, Timestamp
|
||||
include Associations, AssociationPreload, NamedScope
|
||||
|
||||
# AutosaveAssociation needs to be included before Transactions, because we want
|
||||
# #save_with_autosave_associations to be wrapped inside a transaction.
|
||||
include AutosaveAssociation, NestedAttributes
|
||||
|
||||
include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this and make it work with LAZY flag
|
||||
require 'active_record/connection_adapters/abstract_adapter'
|
||||
@@ -1,81 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Batches # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# When processing large numbers of records, it's often a good idea to do
|
||||
# so in batches to prevent memory ballooning.
|
||||
module ClassMethods
|
||||
# Yields each record that was found by the find +options+. The find is
|
||||
# performed by find_in_batches with a batch size of 1000 (or as
|
||||
# specified by the <tt>:batch_size</tt> option).
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Person.find_each(:conditions => "age > 21") do |person|
|
||||
# person.party_all_night!
|
||||
# end
|
||||
#
|
||||
# Note: This method is only intended to use for batch processing of
|
||||
# large amounts of records that wouldn't fit in memory all at once. If
|
||||
# you just need to loop over less than 1000 records, it's probably
|
||||
# better just to use the regular find methods.
|
||||
def find_each(options = {})
|
||||
find_in_batches(options) do |records|
|
||||
records.each { |record| yield record }
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Yields each batch of records that was found by the find +options+ as
|
||||
# an array. The size of each batch is set by the <tt>:batch_size</tt>
|
||||
# option; the default is 1000.
|
||||
#
|
||||
# You can control the starting point for the batch processing by
|
||||
# supplying the <tt>:start</tt> option. This is especially useful if you
|
||||
# want multiple workers dealing with the same processing queue. You can
|
||||
# make worker 1 handle all the records between id 0 and 10,000 and
|
||||
# worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
|
||||
# option on that worker).
|
||||
#
|
||||
# It's not possible to set the order. That is automatically set to
|
||||
# ascending on the primary key ("id ASC") to make the batch ordering
|
||||
# work. This also mean that this method only works with integer-based
|
||||
# primary keys. You can't set the limit either, that's used to control
|
||||
# the the batch sizes.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# Person.find_in_batches(:conditions => "age > 21") do |group|
|
||||
# sleep(50) # Make sure it doesn't get too crowded in there!
|
||||
# group.each { |person| person.party_all_night! }
|
||||
# end
|
||||
def find_in_batches(options = {})
|
||||
raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order]
|
||||
raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
|
||||
|
||||
start = options.delete(:start).to_i
|
||||
batch_size = options.delete(:batch_size) || 1000
|
||||
|
||||
with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
|
||||
|
||||
while records.any?
|
||||
yield records
|
||||
|
||||
break if records.size < batch_size
|
||||
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def batch_order
|
||||
"#{table_name}.#{primary_key} ASC"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,311 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Calculations #:nodoc:
|
||||
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Count operates using three different approaches.
|
||||
#
|
||||
# * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
|
||||
# * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present
|
||||
# * Count using options will find the row count matched by the options used.
|
||||
#
|
||||
# The third approach, count using options, accepts an option hash as the only parameter. The options are:
|
||||
#
|
||||
# * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
|
||||
# * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
|
||||
# or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s).
|
||||
# If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
|
||||
# Pass <tt>:readonly => false</tt> to override.
|
||||
# * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
|
||||
# to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting.
|
||||
# See eager loading under Associations.
|
||||
# * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
|
||||
# * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
|
||||
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
|
||||
# include the joined columns.
|
||||
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
|
||||
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
|
||||
# of a database view).
|
||||
#
|
||||
# Examples for counting all:
|
||||
# Person.count # returns the total count of all people
|
||||
#
|
||||
# Examples for counting by column:
|
||||
# Person.count(:age) # returns the total count of all people whose age is present in database
|
||||
#
|
||||
# Examples for count with options:
|
||||
# Person.count(:conditions => "age > 26")
|
||||
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
|
||||
# Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
|
||||
# Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
|
||||
# Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
|
||||
#
|
||||
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
|
||||
def count(*args)
|
||||
calculate(:count, *construct_count_options_from_args(*args))
|
||||
end
|
||||
|
||||
# Calculates the average value on a given column. The value is returned as
|
||||
# a float, or +nil+ if there's no row. See +calculate+ for examples with
|
||||
# options.
|
||||
#
|
||||
# Person.average('age') # => 35.8
|
||||
def average(column_name, options = {})
|
||||
calculate(:avg, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the minimum value on a given column. The value is returned
|
||||
# with the same data type of the column, or +nil+ if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.minimum('age') # => 7
|
||||
def minimum(column_name, options = {})
|
||||
calculate(:min, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the maximum value on a given column. The value is returned
|
||||
# with the same data type of the column, or +nil+ if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.maximum('age') # => 93
|
||||
def maximum(column_name, options = {})
|
||||
calculate(:max, column_name, options)
|
||||
end
|
||||
|
||||
# Calculates the sum of values on a given column. The value is returned
|
||||
# with the same data type of the column, 0 if there's no row. See
|
||||
# +calculate+ for examples with options.
|
||||
#
|
||||
# Person.sum('age') # => 4562
|
||||
def sum(column_name, options = {})
|
||||
calculate(:sum, column_name, options)
|
||||
end
|
||||
|
||||
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
|
||||
# Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
|
||||
#
|
||||
# There are two basic forms of output:
|
||||
# * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else.
|
||||
# * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name
|
||||
# of a belongs_to association.
|
||||
#
|
||||
# values = Person.maximum(:age, :group => 'last_name')
|
||||
# puts values["Drake"]
|
||||
# => 43
|
||||
#
|
||||
# drake = Family.find_by_last_name('Drake')
|
||||
# values = Person.maximum(:age, :group => :family) # Person belongs_to :family
|
||||
# puts values[drake]
|
||||
# => 43
|
||||
#
|
||||
# values.each do |family, max_age|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# Options:
|
||||
# * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
|
||||
# * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
|
||||
# * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
|
||||
# The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
|
||||
# * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
|
||||
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
|
||||
# * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
|
||||
# include the joined columns.
|
||||
# * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
|
||||
#
|
||||
# Examples:
|
||||
# Person.calculate(:count, :all) # The same as Person.count
|
||||
# Person.average(:age) # SELECT AVG(age) FROM people...
|
||||
# Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake'
|
||||
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
|
||||
# Person.sum("2 * age")
|
||||
def calculate(operation, column_name, options = {})
|
||||
validate_calculation_options(operation, options)
|
||||
column_name = options[:select] if options[:select]
|
||||
column_name = '*' if column_name == :all
|
||||
column = column_for column_name
|
||||
catch :invalid_query do
|
||||
if options[:group]
|
||||
return execute_grouped_calculation(operation, column_name, column, options)
|
||||
else
|
||||
return execute_simple_calculation(operation, column_name, column, options)
|
||||
end
|
||||
end
|
||||
0
|
||||
end
|
||||
|
||||
protected
|
||||
def construct_count_options_from_args(*args)
|
||||
options = {}
|
||||
column_name = :all
|
||||
|
||||
# We need to handle
|
||||
# count()
|
||||
# count(:column_name=:all)
|
||||
# count(options={})
|
||||
# count(column_name=:all, options={})
|
||||
case args.size
|
||||
when 1
|
||||
args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
|
||||
when 2
|
||||
column_name, options = args
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
|
||||
end if args.size > 0
|
||||
|
||||
[column_name, options]
|
||||
end
|
||||
|
||||
def construct_calculation_sql(operation, column_name, options) #:nodoc:
|
||||
operation = operation.to_s.downcase
|
||||
options = options.symbolize_keys
|
||||
|
||||
scope = scope(:find)
|
||||
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
|
||||
aggregate_alias = column_alias_for(operation, column_name)
|
||||
column_name = "#{connection.quote_table_name(table_name)}.#{column_name}" if column_names.include?(column_name.to_s)
|
||||
|
||||
if operation == 'count'
|
||||
if merged_includes.any?
|
||||
options[:distinct] = true
|
||||
column_name = options[:select] || [connection.quote_table_name(table_name), primary_key] * '.'
|
||||
end
|
||||
|
||||
if options[:distinct]
|
||||
use_workaround = !connection.supports_count_distinct?
|
||||
end
|
||||
end
|
||||
|
||||
if options[:distinct] && column_name.to_s !~ /\s*DISTINCT\s+/i
|
||||
distinct = 'DISTINCT '
|
||||
end
|
||||
sql = "SELECT #{operation}(#{distinct}#{column_name}) AS #{aggregate_alias}"
|
||||
|
||||
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
|
||||
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
|
||||
|
||||
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
|
||||
if options[:from]
|
||||
sql << " FROM #{options[:from]} "
|
||||
elsif scope && scope[:from] && !use_workaround
|
||||
sql << " FROM #{scope[:from]} "
|
||||
else
|
||||
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
|
||||
sql << " FROM #{connection.quote_table_name(table_name)} "
|
||||
end
|
||||
|
||||
joins = ""
|
||||
add_joins!(joins, options[:joins], scope)
|
||||
|
||||
if merged_includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
|
||||
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
||||
end
|
||||
|
||||
sql << joins unless joins.blank?
|
||||
|
||||
add_conditions!(sql, options[:conditions], scope)
|
||||
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
|
||||
|
||||
if options[:group]
|
||||
group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field
|
||||
sql << " GROUP BY #{options[group_key]} "
|
||||
end
|
||||
|
||||
if options[:group] && options[:having]
|
||||
having = sanitize_sql_for_conditions(options[:having])
|
||||
|
||||
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
|
||||
if connection.adapter_name == 'FrontBase'
|
||||
having.downcase!
|
||||
having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
|
||||
end
|
||||
|
||||
sql << " HAVING #{having} "
|
||||
end
|
||||
|
||||
sql << " ORDER BY #{options[:order]} " if options[:order]
|
||||
add_limit!(sql, options, scope)
|
||||
sql << ") #{aggregate_alias}_subquery" if use_workaround
|
||||
sql
|
||||
end
|
||||
|
||||
def execute_simple_calculation(operation, column_name, column, options) #:nodoc:
|
||||
value = connection.select_value(construct_calculation_sql(operation, column_name, options))
|
||||
type_cast_calculated_value(value, column, operation)
|
||||
end
|
||||
|
||||
def execute_grouped_calculation(operation, column_name, column, options) #:nodoc:
|
||||
group_attr = options[:group].to_s
|
||||
association = reflect_on_association(group_attr.to_sym)
|
||||
associated = association && association.macro == :belongs_to # only count belongs_to associations
|
||||
group_field = associated ? association.primary_key_name : group_attr
|
||||
group_alias = column_alias_for(group_field)
|
||||
group_column = column_for group_field
|
||||
sql = construct_calculation_sql(operation, column_name, options.merge(:group_field => group_field, :group_alias => group_alias))
|
||||
calculated_data = connection.select_all(sql)
|
||||
aggregate_alias = column_alias_for(operation, column_name)
|
||||
|
||||
if association
|
||||
key_ids = calculated_data.collect { |row| row[group_alias] }
|
||||
key_records = association.klass.base_class.find(key_ids)
|
||||
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
|
||||
end
|
||||
|
||||
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
|
||||
key = type_cast_calculated_value(row[group_alias], group_column)
|
||||
key = key_records[key] if associated
|
||||
value = row[aggregate_alias]
|
||||
all[key] = type_cast_calculated_value(value, column, operation)
|
||||
all
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def validate_calculation_options(operation, options = {})
|
||||
options.assert_valid_keys(CALCULATIONS_OPTIONS)
|
||||
end
|
||||
|
||||
# Converts the given keys to the value that the database adapter returns as
|
||||
# a usable column name:
|
||||
#
|
||||
# column_alias_for("users.id") # => "users_id"
|
||||
# column_alias_for("sum(id)") # => "sum_id"
|
||||
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
|
||||
# column_alias_for("count(*)") # => "count_all"
|
||||
# column_alias_for("count", "id") # => "count_id"
|
||||
def column_alias_for(*keys)
|
||||
table_name = keys.join(' ')
|
||||
table_name.downcase!
|
||||
table_name.gsub!(/\*/, 'all')
|
||||
table_name.gsub!(/\W+/, ' ')
|
||||
table_name.strip!
|
||||
table_name.gsub!(/ +/, '_')
|
||||
|
||||
connection.table_alias_for(table_name)
|
||||
end
|
||||
|
||||
def column_for(field)
|
||||
field_name = field.to_s.split('.').last
|
||||
columns.detect { |c| c.name.to_s == field_name }
|
||||
end
|
||||
|
||||
def type_cast_calculated_value(value, column, operation = nil)
|
||||
operation = operation.to_s.downcase
|
||||
case operation
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'avg' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,360 +0,0 @@
|
||||
require 'observer'
|
||||
|
||||
module ActiveRecord
|
||||
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
|
||||
# before or after an alteration of the object state. This can be used to make sure that associated and
|
||||
# dependent objects are deleted when +destroy+ is called (by overwriting +before_destroy+) or to massage attributes
|
||||
# before they're validated (by overwriting +before_validation+). As an example of the callbacks initiated, consider
|
||||
# the <tt>Base#save</tt> call for a new record:
|
||||
#
|
||||
# * (-) <tt>save</tt>
|
||||
# * (-) <tt>valid</tt>
|
||||
# * (1) <tt>before_validation</tt>
|
||||
# * (2) <tt>before_validation_on_create</tt>
|
||||
# * (-) <tt>validate</tt>
|
||||
# * (-) <tt>validate_on_create</tt>
|
||||
# * (3) <tt>after_validation</tt>
|
||||
# * (4) <tt>after_validation_on_create</tt>
|
||||
# * (5) <tt>before_save</tt>
|
||||
# * (6) <tt>before_create</tt>
|
||||
# * (-) <tt>create</tt>
|
||||
# * (7) <tt>after_create</tt>
|
||||
# * (8) <tt>after_save</tt>
|
||||
#
|
||||
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
|
||||
# Active Record lifecycle. The sequence for calling <tt>Base#save</tt> an existing record is similar, except that each
|
||||
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
|
||||
#
|
||||
# Examples:
|
||||
# class CreditCard < ActiveRecord::Base
|
||||
# # Strip everything but digits, so the user can specify "555 234 34" or
|
||||
# # "5552-3434" or both will mean "55523434"
|
||||
# def before_validation_on_create
|
||||
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Subscription < ActiveRecord::Base
|
||||
# before_create :record_signup
|
||||
#
|
||||
# private
|
||||
# def record_signup
|
||||
# self.signed_up_on = Date.today
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Firm < ActiveRecord::Base
|
||||
# # Destroys the associated clients and people when the firm is destroyed
|
||||
# before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
|
||||
# before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
|
||||
# end
|
||||
#
|
||||
# == Inheritable callback queues
|
||||
#
|
||||
# Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
|
||||
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
|
||||
# hierarchy. Example:
|
||||
#
|
||||
# class Topic < ActiveRecord::Base
|
||||
# before_destroy :destroy_author
|
||||
# end
|
||||
#
|
||||
# class Reply < Topic
|
||||
# before_destroy :destroy_readers
|
||||
# end
|
||||
#
|
||||
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is run, both +destroy_author+ and
|
||||
# +destroy_readers+ are called. Contrast this to the situation where we've implemented the save behavior through overwriteable
|
||||
# methods:
|
||||
#
|
||||
# class Topic < ActiveRecord::Base
|
||||
# def before_destroy() destroy_author end
|
||||
# end
|
||||
#
|
||||
# class Reply < Topic
|
||||
# def before_destroy() destroy_readers end
|
||||
# end
|
||||
#
|
||||
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. So, use the callback macros when
|
||||
# you want to ensure that a certain callback is called for the entire hierarchy, and use the regular overwriteable methods
|
||||
# when you want to leave it up to each descendant to decide whether they want to call +super+ and trigger the inherited callbacks.
|
||||
#
|
||||
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks before specifying the
|
||||
# associations. Otherwise, you might trigger the loading of a child before the parent has registered the callbacks and they won't
|
||||
# be inherited.
|
||||
#
|
||||
# == Types of callbacks
|
||||
#
|
||||
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
|
||||
# inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects are the
|
||||
# recommended approaches, inline methods using a proc are sometimes appropriate (such as for creating mix-ins), and inline
|
||||
# eval methods are deprecated.
|
||||
#
|
||||
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
|
||||
#
|
||||
# class Topic < ActiveRecord::Base
|
||||
# before_destroy :delete_parents
|
||||
#
|
||||
# private
|
||||
# def delete_parents
|
||||
# self.class.delete_all "parent_id = #{id}"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
|
||||
#
|
||||
# class BankAccount < ActiveRecord::Base
|
||||
# before_save EncryptionWrapper.new
|
||||
# after_save EncryptionWrapper.new
|
||||
# after_initialize EncryptionWrapper.new
|
||||
# end
|
||||
#
|
||||
# class EncryptionWrapper
|
||||
# def before_save(record)
|
||||
# record.credit_card_number = encrypt(record.credit_card_number)
|
||||
# end
|
||||
#
|
||||
# def after_save(record)
|
||||
# record.credit_card_number = decrypt(record.credit_card_number)
|
||||
# end
|
||||
#
|
||||
# alias_method :after_find, :after_save
|
||||
#
|
||||
# private
|
||||
# def encrypt(value)
|
||||
# # Secrecy is committed
|
||||
# end
|
||||
#
|
||||
# def decrypt(value)
|
||||
# # Secrecy is unveiled
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
|
||||
# a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
|
||||
# initialization data such as the name of the attribute to work with:
|
||||
#
|
||||
# class BankAccount < ActiveRecord::Base
|
||||
# before_save EncryptionWrapper.new("credit_card_number")
|
||||
# after_save EncryptionWrapper.new("credit_card_number")
|
||||
# after_initialize EncryptionWrapper.new("credit_card_number")
|
||||
# end
|
||||
#
|
||||
# class EncryptionWrapper
|
||||
# def initialize(attribute)
|
||||
# @attribute = attribute
|
||||
# end
|
||||
#
|
||||
# def before_save(record)
|
||||
# record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
|
||||
# end
|
||||
#
|
||||
# def after_save(record)
|
||||
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
|
||||
# end
|
||||
#
|
||||
# alias_method :after_find, :after_save
|
||||
#
|
||||
# private
|
||||
# def encrypt(value)
|
||||
# # Secrecy is committed
|
||||
# end
|
||||
#
|
||||
# def decrypt(value)
|
||||
# # Secrecy is unveiled
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
|
||||
# which will then be evaluated within the binding of the callback. Example:
|
||||
#
|
||||
# class Topic < ActiveRecord::Base
|
||||
# before_destroy 'self.class.delete_all "parent_id = #{id}"'
|
||||
# end
|
||||
#
|
||||
# Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback is triggered. Also note that these
|
||||
# inline callbacks can be stacked just like the regular ones:
|
||||
#
|
||||
# class Topic < ActiveRecord::Base
|
||||
# before_destroy 'self.class.delete_all "parent_id = #{id}"',
|
||||
# 'puts "Evaluated after parents are destroyed"'
|
||||
# end
|
||||
#
|
||||
# == The +after_find+ and +after_initialize+ exceptions
|
||||
#
|
||||
# Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, such as <tt>Base.find(:all)</tt>, we've had
|
||||
# to implement a simple performance constraint (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and
|
||||
# +after_initialize+ will only be run if an explicit implementation is defined (<tt>def after_find</tt>). In that case, all of the
|
||||
# callback types will be called.
|
||||
#
|
||||
# == <tt>before_validation*</tt> returning statements
|
||||
#
|
||||
# If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be aborted and <tt>Base#save</tt> will return +false+.
|
||||
# If Base#save! is called it will raise a ActiveRecord::RecordInvalid exception.
|
||||
# Nothing will be appended to the errors object.
|
||||
#
|
||||
# == Canceling callbacks
|
||||
#
|
||||
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
|
||||
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
|
||||
# defined as methods on the model, which are called last.
|
||||
#
|
||||
# == Transactions
|
||||
#
|
||||
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
|
||||
# within a transaction. That includes <tt>after_*</tt> hooks. If everything
|
||||
# goes fine a COMMIT is executed once the chain has been completed.
|
||||
#
|
||||
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
|
||||
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
|
||||
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
|
||||
# needs to be aware of it because an ordinary +save+ will raise such exception
|
||||
# instead of quietly returning +false+.
|
||||
module Callbacks
|
||||
CALLBACKS = %w(
|
||||
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
|
||||
after_validation before_validation_on_create after_validation_on_create before_validation_on_update
|
||||
after_validation_on_update before_destroy after_destroy
|
||||
)
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend Observable
|
||||
|
||||
[:create_or_update, :valid?, :create, :update, :destroy].each do |method|
|
||||
base.send :alias_method_chain, method, :callbacks
|
||||
end
|
||||
|
||||
base.send :include, ActiveSupport::Callbacks
|
||||
base.define_callbacks *CALLBACKS
|
||||
end
|
||||
|
||||
# Is called when the object was instantiated by one of the finders, like <tt>Base.find</tt>.
|
||||
#def after_find() end
|
||||
|
||||
# Is called after the object has been instantiated by a call to <tt>Base.new</tt>.
|
||||
#def after_initialize() end
|
||||
|
||||
# Is called _before_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
|
||||
def before_save() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
#
|
||||
# class Contact < ActiveRecord::Base
|
||||
# after_save { logger.info( 'New contact saved!' ) }
|
||||
# end
|
||||
def after_save() end
|
||||
def create_or_update_with_callbacks #:nodoc:
|
||||
return false if callback(:before_save) == false
|
||||
if result = create_or_update_without_callbacks
|
||||
callback(:after_save)
|
||||
end
|
||||
result
|
||||
end
|
||||
private :create_or_update_with_callbacks
|
||||
|
||||
# Is called _before_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
|
||||
def before_create() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
def after_create() end
|
||||
def create_with_callbacks #:nodoc:
|
||||
return false if callback(:before_create) == false
|
||||
result = create_without_callbacks
|
||||
callback(:after_create)
|
||||
result
|
||||
end
|
||||
private :create_with_callbacks
|
||||
|
||||
# Is called _before_ <tt>Base.save</tt> on existing objects that have a record.
|
||||
def before_update() end
|
||||
|
||||
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
|
||||
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
|
||||
# invoke an external indexer at this point it won't see the changes in the database.
|
||||
def after_update() end
|
||||
|
||||
def update_with_callbacks(*args) #:nodoc:
|
||||
return false if callback(:before_update) == false
|
||||
result = update_without_callbacks(*args)
|
||||
callback(:after_update)
|
||||
result
|
||||
end
|
||||
private :update_with_callbacks
|
||||
|
||||
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
|
||||
def before_validation() end
|
||||
|
||||
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
|
||||
def after_validation() end
|
||||
|
||||
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
|
||||
# that haven't been saved yet (no record exists).
|
||||
def before_validation_on_create() end
|
||||
|
||||
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
|
||||
# that haven't been saved yet (no record exists).
|
||||
def after_validation_on_create() end
|
||||
|
||||
# Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
|
||||
# existing objects that have a record.
|
||||
def before_validation_on_update() end
|
||||
|
||||
# Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
|
||||
# existing objects that have a record.
|
||||
def after_validation_on_update() end
|
||||
|
||||
def valid_with_callbacks? #:nodoc:
|
||||
return false if callback(:before_validation) == false
|
||||
if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
|
||||
return false if false == result
|
||||
|
||||
result = valid_without_callbacks?
|
||||
|
||||
callback(:after_validation)
|
||||
if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
# Is called _before_ <tt>Base.destroy</tt>.
|
||||
#
|
||||
# Note: If you need to _destroy_ or _nullify_ associated records first,
|
||||
# use the <tt>:dependent</tt> option on your associations.
|
||||
def before_destroy() end
|
||||
|
||||
# Is called _after_ <tt>Base.destroy</tt> (and all the attributes have been frozen).
|
||||
#
|
||||
# class Contact < ActiveRecord::Base
|
||||
# after_destroy { |record| logger.info( "Contact #{record.id} was destroyed." ) }
|
||||
# end
|
||||
def after_destroy() end
|
||||
def destroy_with_callbacks #:nodoc:
|
||||
return false if callback(:before_destroy) == false
|
||||
result = destroy_without_callbacks
|
||||
callback(:after_destroy)
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
def callback(method)
|
||||
result = run_callbacks(method) { |result, object| false == result }
|
||||
|
||||
if result != false && respond_to_without_attributes?(method)
|
||||
result = send(method)
|
||||
end
|
||||
|
||||
notify(method)
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def notify(method) #:nodoc:
|
||||
self.class.changed
|
||||
self.class.notify_observers(method, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,371 +0,0 @@
|
||||
require 'monitor'
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
# Raised when a connection could not be obtained within the connection
|
||||
# acquisition timeout period.
|
||||
class ConnectionTimeoutError < ConnectionNotEstablished
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
# Connection pool base class for managing ActiveRecord database
|
||||
# connections.
|
||||
#
|
||||
# == Introduction
|
||||
#
|
||||
# A connection pool synchronizes thread access to a limited number of
|
||||
# database connections. The basic idea is that each thread checks out a
|
||||
# database connection from the pool, uses that connection, and checks the
|
||||
# connection back in. ConnectionPool is completely thread-safe, and will
|
||||
# ensure that a connection cannot be used by two threads at the same time,
|
||||
# as long as ConnectionPool's contract is correctly followed. It will also
|
||||
# handle cases in which there are more threads than connections: if all
|
||||
# connections have been checked out, and a thread tries to checkout a
|
||||
# connection anyway, then ConnectionPool will wait until some other thread
|
||||
# has checked in a connection.
|
||||
#
|
||||
# == Obtaining (checking out) a connection
|
||||
#
|
||||
# Connections can be obtained and used from a connection pool in several
|
||||
# ways:
|
||||
#
|
||||
# 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 2.1 and
|
||||
# earlier (pre-connection-pooling). Eventually, when you're done with
|
||||
# the connection(s) and wish it to be returned to the pool, you call
|
||||
# ActiveRecord::Base.clear_active_connections!. This will be the
|
||||
# default behavior for ActiveRecord when used in conjunction with
|
||||
# ActionPack's request handling cycle.
|
||||
# 2. Manually check out a connection from the pool with
|
||||
# ActiveRecord::Base.connection_pool.checkout. You are responsible for
|
||||
# returning this connection to the pool when finished by calling
|
||||
# ActiveRecord::Base.connection_pool.checkin(connection).
|
||||
# 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
|
||||
# obtains a connection, yields it as the sole argument to the block,
|
||||
# and returns it to the pool after the block completes.
|
||||
#
|
||||
# Connections in the pool are actually AbstractAdapter objects (or objects
|
||||
# compatible with AbstractAdapter's interface).
|
||||
#
|
||||
# == Options
|
||||
#
|
||||
# There are two connection-pooling-related options that you can add to
|
||||
# your database connection configuration:
|
||||
#
|
||||
# * +pool+: number indicating size of connection pool (default 5)
|
||||
# * +wait_timeout+: number of seconds to block and wait for a connection
|
||||
# before giving up and raising a timeout error (default 5 seconds).
|
||||
class ConnectionPool
|
||||
attr_reader :spec
|
||||
|
||||
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
|
||||
# object which describes database connection information (e.g. adapter,
|
||||
# host name, username, password, etc), as well as the maximum size for
|
||||
# this ConnectionPool.
|
||||
#
|
||||
# The default ConnectionPool maximum size is 5.
|
||||
def initialize(spec)
|
||||
@spec = spec
|
||||
|
||||
# The cache of reserved connections mapped to threads
|
||||
@reserved_connections = {}
|
||||
|
||||
# The mutex used to synchronize pool access
|
||||
@connection_mutex = Monitor.new
|
||||
@queue = @connection_mutex.new_cond
|
||||
|
||||
# default 5 second timeout unless on ruby 1.9
|
||||
@timeout =
|
||||
if RUBY_VERSION < '1.9'
|
||||
spec.config[:wait_timeout] || 5
|
||||
end
|
||||
|
||||
# default max pool size to 5
|
||||
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
|
||||
|
||||
@connections = []
|
||||
@checked_out = []
|
||||
end
|
||||
|
||||
# Retrieve the connection associated with the current thread, or call
|
||||
# #checkout to obtain one if necessary.
|
||||
#
|
||||
# #connection can be called any number of times; the connection is
|
||||
# held in a hash keyed by the thread id.
|
||||
def connection
|
||||
if conn = @reserved_connections[current_connection_id]
|
||||
conn
|
||||
else
|
||||
@reserved_connections[current_connection_id] = checkout
|
||||
end
|
||||
end
|
||||
|
||||
# Signal that the thread is finished with the current connection.
|
||||
# #release_connection releases the connection-thread association
|
||||
# and returns the connection to the pool.
|
||||
def release_connection
|
||||
conn = @reserved_connections.delete(current_connection_id)
|
||||
checkin conn if conn
|
||||
end
|
||||
|
||||
# Reserve a connection, and yield it to a block. Ensure the connection is
|
||||
# checked back in when finished.
|
||||
def with_connection
|
||||
conn = checkout
|
||||
yield conn
|
||||
ensure
|
||||
checkin conn
|
||||
end
|
||||
|
||||
# Returns true if a connection has already been opened.
|
||||
def connected?
|
||||
!@connections.empty?
|
||||
end
|
||||
|
||||
# Disconnects all connections in the pool, and clears the pool.
|
||||
def disconnect!
|
||||
@reserved_connections.each do |name,conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect!
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@reserved_connections.each do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
@reserved_connections = {}
|
||||
@connections.each do |conn|
|
||||
conn.disconnect! if conn.requires_reloading?
|
||||
end
|
||||
@connections = []
|
||||
end
|
||||
|
||||
# Verify active connections and remove and disconnect connections
|
||||
# associated with stale threads.
|
||||
def verify_active_connections! #:nodoc:
|
||||
clear_stale_cached_connections!
|
||||
@connections.each do |connection|
|
||||
connection.verify!
|
||||
end
|
||||
end
|
||||
|
||||
# Return any checked-out connections back to the pool by threads that
|
||||
# are no longer alive.
|
||||
def clear_stale_cached_connections!
|
||||
remove_stale_cached_threads!(@reserved_connections) do |name, conn|
|
||||
checkin conn
|
||||
end
|
||||
end
|
||||
|
||||
# Check-out a database connection from the pool, indicating that you want
|
||||
# to use it. You should call #checkin when you no longer need this.
|
||||
#
|
||||
# This is done by either returning an existing connection, or by creating
|
||||
# a new connection. If the maximum number of connections for this pool has
|
||||
# already been reached, but the pool is empty (i.e. they're all being used),
|
||||
# then this method will wait until a thread has checked in a connection.
|
||||
# The wait time is bounded however: if no connection can be checked out
|
||||
# within the timeout specified for this pool, then a ConnectionTimeoutError
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns: an AbstractAdapter object.
|
||||
#
|
||||
# Raises:
|
||||
# - ConnectionTimeoutError: no connection can be obtained from the pool
|
||||
# within the timeout period.
|
||||
def checkout
|
||||
# Checkout an available connection
|
||||
@connection_mutex.synchronize do
|
||||
loop do
|
||||
conn = if @checked_out.size < @connections.size
|
||||
checkout_existing_connection
|
||||
elsif @connections.size < @size
|
||||
checkout_new_connection
|
||||
end
|
||||
return conn if conn
|
||||
# No connections available; wait for one
|
||||
if @queue.wait(@timeout)
|
||||
next
|
||||
else
|
||||
# try looting dead threads
|
||||
clear_stale_cached_connections!
|
||||
if @size == @checked_out.size
|
||||
raise ConnectionTimeoutError, "could not obtain a database connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check-in a database connection back into the pool, indicating that you
|
||||
# no longer need this connection.
|
||||
#
|
||||
# +conn+: an AbstractAdapter object, which was obtained by earlier by
|
||||
# calling +checkout+ on this pool.
|
||||
def checkin(conn)
|
||||
@connection_mutex.synchronize do
|
||||
conn.run_callbacks :checkin
|
||||
@checked_out.delete conn
|
||||
@queue.signal
|
||||
end
|
||||
end
|
||||
|
||||
synchronize :clear_reloadable_connections!, :verify_active_connections!,
|
||||
:connected?, :disconnect!, :with => :@connection_mutex
|
||||
|
||||
private
|
||||
def new_connection
|
||||
ActiveRecord::Base.send(spec.adapter_method, spec.config)
|
||||
end
|
||||
|
||||
def current_connection_id #:nodoc:
|
||||
Thread.current.object_id
|
||||
end
|
||||
|
||||
# Remove stale threads from the cache.
|
||||
def remove_stale_cached_threads!(cache, &block)
|
||||
keys = Set.new(cache.keys)
|
||||
|
||||
Thread.list.each do |thread|
|
||||
keys.delete(thread.object_id) if thread.alive?
|
||||
end
|
||||
keys.each do |key|
|
||||
next unless cache.has_key?(key)
|
||||
block.call(key, cache[key])
|
||||
cache.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def checkout_new_connection
|
||||
c = new_connection
|
||||
@connections << c
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_existing_connection
|
||||
c = (@connections - @checked_out).first
|
||||
checkout_and_verify(c)
|
||||
end
|
||||
|
||||
def checkout_and_verify(c)
|
||||
c.verify!
|
||||
c.run_callbacks :checkout
|
||||
@checked_out << c
|
||||
c
|
||||
end
|
||||
end
|
||||
|
||||
# ConnectionHandler is a collection of ConnectionPool objects. It is used
|
||||
# for keeping separate connection pools for ActiveRecord models that connect
|
||||
# to different databases.
|
||||
#
|
||||
# For example, suppose that you have 5 models, with the following hierarchy:
|
||||
#
|
||||
# |
|
||||
# +-- Book
|
||||
# | |
|
||||
# | +-- ScaryBook
|
||||
# | +-- GoodBook
|
||||
# +-- Author
|
||||
# +-- BankAccount
|
||||
#
|
||||
# Suppose that Book is to connect to a separate database (i.e. one other
|
||||
# than the default database). Then Book, ScaryBook and GoodBook will all use
|
||||
# the same connection pool. Likewise, Author and BankAccount will use the
|
||||
# same connection pool. However, the connection pool used by Author/BankAccount
|
||||
# is not the same as the one used by Book/ScaryBook/GoodBook.
|
||||
#
|
||||
# Normally there is only a single ConnectionHandler instance, accessible via
|
||||
# ActiveRecord::Base.connection_handler. ActiveRecord models use this to
|
||||
# determine that connection pool that they should use.
|
||||
class ConnectionHandler
|
||||
def initialize(pools = {})
|
||||
@connection_pools = pools
|
||||
end
|
||||
|
||||
def connection_pools
|
||||
@connection_pools ||= {}
|
||||
end
|
||||
|
||||
def establish_connection(name, spec)
|
||||
@connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
|
||||
end
|
||||
|
||||
# Returns any connections in use by the current thread back to the pool,
|
||||
# and also returns connections to the pool cached by threads that are no
|
||||
# longer alive.
|
||||
def clear_active_connections!
|
||||
@connection_pools.each_value {|pool| pool.release_connection }
|
||||
end
|
||||
|
||||
# Clears the cache which maps classes
|
||||
def clear_reloadable_connections!
|
||||
@connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
|
||||
end
|
||||
|
||||
def clear_all_connections!
|
||||
@connection_pools.each_value {|pool| pool.disconnect! }
|
||||
end
|
||||
|
||||
# Verify active connections.
|
||||
def verify_active_connections! #:nodoc:
|
||||
@connection_pools.each_value {|pool| pool.verify_active_connections! }
|
||||
end
|
||||
|
||||
# Locate the connection of the nearest super class. This can be an
|
||||
# active or defined connection: if it is the latter, it will be
|
||||
# opened and set as the active connection for the class it was defined
|
||||
# for (not necessarily the current class).
|
||||
def retrieve_connection(klass) #:nodoc:
|
||||
pool = retrieve_connection_pool(klass)
|
||||
(pool && pool.connection) or raise ConnectionNotEstablished
|
||||
end
|
||||
|
||||
# Returns true if a connection that's accessible to this class has
|
||||
# already been opened.
|
||||
def connected?(klass)
|
||||
conn = retrieve_connection_pool(klass)
|
||||
conn ? conn.connected? : false
|
||||
end
|
||||
|
||||
# Remove the connection for this class. This will close the active
|
||||
# connection and the defined connection (if they exist). The result
|
||||
# can be used as an argument for establish_connection, for easily
|
||||
# re-establishing the connection.
|
||||
def remove_connection(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
@connection_pools.delete_if { |key, value| value == pool }
|
||||
pool.disconnect! if pool
|
||||
pool.spec.config if pool
|
||||
end
|
||||
|
||||
def retrieve_connection_pool(klass)
|
||||
pool = @connection_pools[klass.name]
|
||||
return pool if pool
|
||||
return nil if ActiveRecord::Base == klass
|
||||
retrieve_connection_pool klass.superclass
|
||||
end
|
||||
end
|
||||
|
||||
class ConnectionManagement
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@app.call(env)
|
||||
ensure
|
||||
# Don't return connection (and peform implicit rollback) if
|
||||
# this request is a part of integration test
|
||||
unless env.key?("rack.test")
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,139 +0,0 @@
|
||||
module ActiveRecord
|
||||
class Base
|
||||
class ConnectionSpecification #:nodoc:
|
||||
attr_reader :config, :adapter_method
|
||||
def initialize (config, adapter_method)
|
||||
@config, @adapter_method = config, adapter_method
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The connection handler
|
||||
cattr_accessor :connection_handler, :instance_writer => false
|
||||
@@connection_handler = ConnectionAdapters::ConnectionHandler.new
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work that isn't
|
||||
# easily done without going straight to SQL.
|
||||
def connection
|
||||
self.class.connection
|
||||
end
|
||||
|
||||
# Establishes the connection to the database. Accepts a hash as input where
|
||||
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
|
||||
# example for regular databases (MySQL, Postgresql, etc):
|
||||
#
|
||||
# ActiveRecord::Base.establish_connection(
|
||||
# :adapter => "mysql",
|
||||
# :host => "localhost",
|
||||
# :username => "myuser",
|
||||
# :password => "mypass",
|
||||
# :database => "somedatabase"
|
||||
# )
|
||||
#
|
||||
# Example for SQLite database:
|
||||
#
|
||||
# ActiveRecord::Base.establish_connection(
|
||||
# :adapter => "sqlite",
|
||||
# :database => "path/to/dbfile"
|
||||
# )
|
||||
#
|
||||
# Also accepts keys as strings (for parsing from YAML for example):
|
||||
#
|
||||
# ActiveRecord::Base.establish_connection(
|
||||
# "adapter" => "sqlite",
|
||||
# "database" => "path/to/dbfile"
|
||||
# )
|
||||
#
|
||||
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
|
||||
# may be returned on an error.
|
||||
def self.establish_connection(spec = nil)
|
||||
case spec
|
||||
when nil
|
||||
raise AdapterNotSpecified unless defined? RAILS_ENV
|
||||
establish_connection(RAILS_ENV)
|
||||
when ConnectionSpecification
|
||||
@@connection_handler.establish_connection(name, spec)
|
||||
when Symbol, String
|
||||
if configuration = configurations[spec.to_s]
|
||||
establish_connection(configuration)
|
||||
else
|
||||
raise AdapterNotSpecified, "#{spec} database is not configured"
|
||||
end
|
||||
else
|
||||
spec = spec.symbolize_keys
|
||||
unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end
|
||||
|
||||
begin
|
||||
require 'rubygems'
|
||||
gem "activerecord-#{spec[:adapter]}-adapter"
|
||||
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
|
||||
rescue LoadError
|
||||
begin
|
||||
require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
|
||||
rescue LoadError
|
||||
raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{$!})"
|
||||
end
|
||||
end
|
||||
|
||||
adapter_method = "#{spec[:adapter]}_connection"
|
||||
if !respond_to?(adapter_method)
|
||||
raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter"
|
||||
end
|
||||
|
||||
remove_connection
|
||||
establish_connection(ConnectionSpecification.new(spec, adapter_method))
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def allow_concurrency=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout has been deprecated and no longer has any effect. Please remove all references to verification_timeout.")
|
||||
end
|
||||
|
||||
# Deprecated and no longer has any effect.
|
||||
def verification_timeout=(flag)
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Base.verification_timeout= has been deprecated and no longer has any effect. Please remove all references to verification_timeout=.")
|
||||
end
|
||||
|
||||
# Returns the connection currently associated with the class. This can
|
||||
# also be used to "borrow" the connection to do database work unrelated
|
||||
# to any of the specific Active Records.
|
||||
def connection
|
||||
retrieve_connection
|
||||
end
|
||||
|
||||
def connection_pool
|
||||
connection_handler.retrieve_connection_pool(self)
|
||||
end
|
||||
|
||||
def retrieve_connection
|
||||
connection_handler.retrieve_connection(self)
|
||||
end
|
||||
|
||||
# Returns true if +ActiveRecord+ is connected.
|
||||
def connected?
|
||||
connection_handler.connected?(self)
|
||||
end
|
||||
|
||||
def remove_connection(klass = self)
|
||||
connection_handler.remove_connection(klass)
|
||||
end
|
||||
|
||||
delegate :clear_active_connections!, :clear_reloadable_connections!,
|
||||
:clear_all_connections!,:verify_active_connections!, :to => :connection_handler
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,289 +0,0 @@
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module DatabaseStatements
|
||||
# Returns an array of record hashes with the column names as keys and
|
||||
# column values as values.
|
||||
def select_all(sql, name = nil)
|
||||
select(sql, name)
|
||||
end
|
||||
|
||||
# Returns a record hash with the column names as keys and column values
|
||||
# as values.
|
||||
def select_one(sql, name = nil)
|
||||
result = select_all(sql, name)
|
||||
result.first if result
|
||||
end
|
||||
|
||||
# Returns a single value from a record
|
||||
def select_value(sql, name = nil)
|
||||
if result = select_one(sql, name)
|
||||
result.values.first
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array of the values of the first column in a select:
|
||||
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
|
||||
def select_values(sql, name = nil)
|
||||
result = select_rows(sql, name)
|
||||
result.map { |v| v[0] }
|
||||
end
|
||||
|
||||
# Returns an array of arrays containing the field values.
|
||||
# Order is the same as that returned by +columns+.
|
||||
def select_rows(sql, name = nil)
|
||||
end
|
||||
undef_method :select_rows
|
||||
|
||||
# Executes the SQL statement in the context of this connection.
|
||||
def execute(sql, name = nil, skip_logging = false)
|
||||
end
|
||||
undef_method :execute
|
||||
|
||||
# Returns the last auto-generated ID from the affected table.
|
||||
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
||||
insert_sql(sql, name, pk, id_value, sequence_name)
|
||||
end
|
||||
|
||||
# Executes the update statement and returns the number of rows affected.
|
||||
def update(sql, name = nil)
|
||||
update_sql(sql, name)
|
||||
end
|
||||
|
||||
# Executes the delete statement and returns the number of rows affected.
|
||||
def delete(sql, name = nil)
|
||||
delete_sql(sql, name)
|
||||
end
|
||||
|
||||
# Checks whether there is currently no transaction active. This is done
|
||||
# by querying the database driver, and does not use the transaction
|
||||
# house-keeping information recorded by #increment_open_transactions and
|
||||
# friends.
|
||||
#
|
||||
# Returns true if there is no transaction active, false if there is a
|
||||
# transaction active, and nil if this information is unknown.
|
||||
#
|
||||
# Not all adapters supports transaction state introspection. Currently,
|
||||
# only the PostgreSQL adapter supports this.
|
||||
def outside_transaction?
|
||||
nil
|
||||
end
|
||||
|
||||
# Runs the given block in a database transaction, and returns the result
|
||||
# of the block.
|
||||
#
|
||||
# == Nested transactions support
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that supports true nested transactions that
|
||||
# we're aware of, is MS-SQL.
|
||||
#
|
||||
# In order to get around this problem, #transaction will emulate the effect
|
||||
# of nested transactions, by using savepoints:
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
|
||||
#
|
||||
# It is safe to call this method if a database transaction is already open,
|
||||
# i.e. if #transaction is called within another #transaction block. In case
|
||||
# of a nested call, #transaction will behave as follows:
|
||||
#
|
||||
# - The block will be run without doing anything. All database statements
|
||||
# that happen within the block are effectively appended to the already
|
||||
# open database transaction.
|
||||
# - However, if +:requires_new+ is set, the block will be wrapped in a
|
||||
# database savepoint acting as a sub-transaction.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# MySQL doesn't support DDL transactions. If you perform a DDL operation,
|
||||
# then any created savepoints will be automatically released. For example,
|
||||
# if you've created a savepoint, then you execute a CREATE TABLE statement,
|
||||
# then the savepoint that was created will be automatically released.
|
||||
#
|
||||
# This means that, on MySQL, you shouldn't execute DDL operations inside
|
||||
# a #transaction call that you know might create a savepoint. Otherwise,
|
||||
# #transaction will raise exceptions when it tries to release the
|
||||
# already-automatically-released savepoints:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...)
|
||||
# # active_record_1 now automatically released
|
||||
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
|
||||
# end
|
||||
def transaction(options = {})
|
||||
options.assert_valid_keys :requires_new, :joinable
|
||||
|
||||
last_transaction_joinable = @transaction_joinable
|
||||
if options.has_key?(:joinable)
|
||||
@transaction_joinable = options[:joinable]
|
||||
else
|
||||
@transaction_joinable = true
|
||||
end
|
||||
requires_new = options[:requires_new] || !last_transaction_joinable
|
||||
|
||||
transaction_open = false
|
||||
begin
|
||||
if block_given?
|
||||
if requires_new || open_transactions == 0
|
||||
if open_transactions == 0
|
||||
begin_db_transaction
|
||||
elsif requires_new
|
||||
create_savepoint
|
||||
end
|
||||
increment_open_transactions
|
||||
transaction_open = true
|
||||
end
|
||||
yield
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if transaction_open && !outside_transaction?
|
||||
transaction_open = false
|
||||
decrement_open_transactions
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
end
|
||||
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
|
||||
end
|
||||
ensure
|
||||
@transaction_joinable = last_transaction_joinable
|
||||
|
||||
if outside_transaction?
|
||||
@open_transactions = 0
|
||||
elsif transaction_open
|
||||
decrement_open_transactions
|
||||
begin
|
||||
if open_transactions == 0
|
||||
commit_db_transaction
|
||||
else
|
||||
release_savepoint
|
||||
end
|
||||
rescue Exception => database_transaction_rollback
|
||||
if open_transactions == 0
|
||||
rollback_db_transaction
|
||||
else
|
||||
rollback_to_savepoint
|
||||
end
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Begins the transaction (and turns off auto-committing).
|
||||
def begin_db_transaction() end
|
||||
|
||||
# Commits the transaction (and turns on auto-committing).
|
||||
def commit_db_transaction() end
|
||||
|
||||
# Rolls back the transaction (and turns on auto-committing). Must be
|
||||
# done if the transaction block raises an exception or returns false.
|
||||
def rollback_db_transaction() end
|
||||
|
||||
# Alias for <tt>add_limit_offset!</tt>.
|
||||
def add_limit!(sql, options)
|
||||
add_limit_offset!(sql, options) if options
|
||||
end
|
||||
|
||||
# Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
|
||||
# fragment that has the same semantics as LIMIT and OFFSET.
|
||||
#
|
||||
# +options+ must be a Hash which contains a +:limit+ option (required)
|
||||
# and an +:offset+ option (optional).
|
||||
#
|
||||
# This method *modifies* the +sql+ parameter.
|
||||
#
|
||||
# ===== Examples
|
||||
# add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
|
||||
# generates
|
||||
# SELECT * FROM suppliers LIMIT 10 OFFSET 50
|
||||
def add_limit_offset!(sql, options)
|
||||
if limit = options[:limit]
|
||||
sql << " LIMIT #{sanitize_limit(limit)}"
|
||||
if offset = options[:offset]
|
||||
sql << " OFFSET #{offset.to_i}"
|
||||
end
|
||||
end
|
||||
sql
|
||||
end
|
||||
|
||||
# Appends a locking clause to an SQL statement.
|
||||
# This method *modifies* the +sql+ parameter.
|
||||
# # SELECT * FROM suppliers FOR UPDATE
|
||||
# add_lock! 'SELECT * FROM suppliers', :lock => true
|
||||
# add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
|
||||
def add_lock!(sql, options)
|
||||
case lock = options[:lock]
|
||||
when true; sql << ' FOR UPDATE'
|
||||
when String; sql << " #{lock}"
|
||||
end
|
||||
end
|
||||
|
||||
def default_sequence_name(table, column)
|
||||
nil
|
||||
end
|
||||
|
||||
# Set the sequence to the max value of the table's column.
|
||||
def reset_sequence!(table, column, sequence = nil)
|
||||
# Do nothing by default. Implement for PostgreSQL, Oracle, ...
|
||||
end
|
||||
|
||||
# Inserts the given fixture into the table. Overridden in adapters that require
|
||||
# something beyond a simple insert (eg. Oracle).
|
||||
def insert_fixture(fixture, table_name)
|
||||
execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
|
||||
end
|
||||
|
||||
def empty_insert_statement(table_name)
|
||||
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
|
||||
end
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"="
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
"WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns an array of record hashes with the column names as keys and
|
||||
# column values as values.
|
||||
def select(sql, name = nil)
|
||||
end
|
||||
undef_method :select
|
||||
|
||||
# Returns the last auto-generated ID from the affected table.
|
||||
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
||||
execute(sql, name)
|
||||
id_value
|
||||
end
|
||||
|
||||
# Executes the update statement and returns the number of rows affected.
|
||||
def update_sql(sql, name = nil)
|
||||
execute(sql, name)
|
||||
end
|
||||
|
||||
# Executes the delete statement and returns the number of rows affected.
|
||||
def delete_sql(sql, name = nil)
|
||||
update_sql(sql, name)
|
||||
end
|
||||
|
||||
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
|
||||
#
|
||||
# +limit+ may be anything that can evaluate to a string via #to_s. It
|
||||
# should look like an integer, or a comma-delimited list of integers.
|
||||
#
|
||||
# Returns the sanitized limit parameter, either as an integer, or as a
|
||||
# string which contains a comma-delimited list of integers.
|
||||
def sanitize_limit(limit)
|
||||
if limit.to_s =~ /,/
|
||||
limit.to_s.split(',').map{ |i| i.to_i }.join(',')
|
||||
else
|
||||
limit.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,94 +0,0 @@
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module QueryCache
|
||||
class << self
|
||||
def included(base)
|
||||
base.class_eval do
|
||||
alias_method_chain :columns, :query_cache
|
||||
alias_method_chain :select_all, :query_cache
|
||||
end
|
||||
|
||||
dirties_query_cache base, :insert, :update, :delete
|
||||
end
|
||||
|
||||
def dirties_query_cache(base, *method_names)
|
||||
method_names.each do |method_name|
|
||||
base.class_eval <<-end_code, __FILE__, __LINE__
|
||||
def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args)
|
||||
clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
|
||||
#{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args)
|
||||
end # end
|
||||
#
|
||||
alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty
|
||||
end_code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :query_cache, :query_cache_enabled
|
||||
|
||||
# Enable the query cache within the block.
|
||||
def cache
|
||||
old, @query_cache_enabled = @query_cache_enabled, true
|
||||
@query_cache ||= {}
|
||||
yield
|
||||
ensure
|
||||
clear_query_cache
|
||||
@query_cache_enabled = old
|
||||
end
|
||||
|
||||
# Disable the query cache within the block.
|
||||
def uncached
|
||||
old, @query_cache_enabled = @query_cache_enabled, false
|
||||
yield
|
||||
ensure
|
||||
@query_cache_enabled = old
|
||||
end
|
||||
|
||||
# Clears the query cache.
|
||||
#
|
||||
# One reason you may wish to call this method explicitly is between queries
|
||||
# that ask the database to randomize results. Otherwise the cache would see
|
||||
# the same SQL query and repeatedly return the same result each time, silently
|
||||
# undermining the randomness you were expecting.
|
||||
def clear_query_cache
|
||||
@query_cache.clear if @query_cache
|
||||
end
|
||||
|
||||
def select_all_with_query_cache(*args)
|
||||
if @query_cache_enabled
|
||||
cache_sql(args.first) { select_all_without_query_cache(*args) }
|
||||
else
|
||||
select_all_without_query_cache(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def columns_with_query_cache(*args)
|
||||
if @query_cache_enabled
|
||||
@query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
|
||||
else
|
||||
columns_without_query_cache(*args)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def cache_sql(sql)
|
||||
result =
|
||||
if @query_cache.has_key?(sql)
|
||||
log_info(sql, "CACHE", 0.0)
|
||||
@query_cache[sql]
|
||||
else
|
||||
@query_cache[sql] = yield
|
||||
end
|
||||
|
||||
if Array === result
|
||||
result.collect { |row| row.dup }
|
||||
else
|
||||
result.duplicable? ? result.dup : result
|
||||
end
|
||||
rescue TypeError
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module Quoting
|
||||
# Quotes the column value to help prevent
|
||||
# {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection].
|
||||
def quote(value, column = nil)
|
||||
# records are quoted as their primary key
|
||||
return value.quoted_id if value.respond_to?(:quoted_id)
|
||||
|
||||
case value
|
||||
when String, ActiveSupport::Multibyte::Chars
|
||||
value = value.to_s
|
||||
if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
||||
"#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
|
||||
elsif column && [:integer, :float].include?(column.type)
|
||||
value = column.type == :integer ? value.to_i : value.to_f
|
||||
value.to_s
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
|
||||
end
|
||||
when NilClass then "NULL"
|
||||
when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
|
||||
when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
|
||||
when Float, Fixnum, Bignum then value.to_s
|
||||
# BigDecimals need to be output in a non-normalized form and quoted.
|
||||
when BigDecimal then value.to_s('F')
|
||||
else
|
||||
if value.acts_like?(:date) || value.acts_like?(:time)
|
||||
"'#{quoted_date(value)}'"
|
||||
else
|
||||
"#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Quotes a string, escaping any ' (single quote) and \ (backslash)
|
||||
# characters.
|
||||
def quote_string(s)
|
||||
s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
|
||||
end
|
||||
|
||||
# Quotes the column name. Defaults to no quoting.
|
||||
def quote_column_name(column_name)
|
||||
column_name
|
||||
end
|
||||
|
||||
# Quotes the table name. Defaults to column name quoting.
|
||||
def quote_table_name(table_name)
|
||||
quote_column_name(table_name)
|
||||
end
|
||||
|
||||
def quoted_true
|
||||
"'t'"
|
||||
end
|
||||
|
||||
def quoted_false
|
||||
"'f'"
|
||||
end
|
||||
|
||||
def quoted_date(value)
|
||||
value.to_s(:db)
|
||||
end
|
||||
|
||||
def quoted_string_prefix
|
||||
''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,722 +0,0 @@
|
||||
require 'date'
|
||||
require 'set'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters #:nodoc:
|
||||
# An abstract definition of a column in a table.
|
||||
class Column
|
||||
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
||||
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
||||
|
||||
module Format
|
||||
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
||||
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
|
||||
end
|
||||
|
||||
attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
|
||||
attr_accessor :primary
|
||||
|
||||
# Instantiates a new column in the table.
|
||||
#
|
||||
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
|
||||
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
|
||||
# +sql_type+ is only used to extract the column's length, if necessary. For example +60+ in <tt>company_name varchar(60)</tt>.
|
||||
# +null+ determines if this column allows +NULL+ values.
|
||||
def initialize(name, default, sql_type = nil, null = true)
|
||||
@name, @sql_type, @null = name, sql_type, null
|
||||
@limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type)
|
||||
@type = simplified_type(sql_type)
|
||||
@default = extract_default(default)
|
||||
|
||||
@primary = nil
|
||||
end
|
||||
|
||||
# Returns +true+ if the column is either of type string or text.
|
||||
def text?
|
||||
type == :string || type == :text
|
||||
end
|
||||
|
||||
# Returns +true+ if the column is either of type integer, float or decimal.
|
||||
def number?
|
||||
type == :integer || type == :float || type == :decimal
|
||||
end
|
||||
|
||||
def has_default?
|
||||
!default.nil?
|
||||
end
|
||||
|
||||
# Returns the Ruby class that corresponds to the abstract data type.
|
||||
def klass
|
||||
case type
|
||||
when :integer then Fixnum
|
||||
when :float then Float
|
||||
when :decimal then BigDecimal
|
||||
when :datetime then Time
|
||||
when :date then Date
|
||||
when :timestamp then Time
|
||||
when :time then Time
|
||||
when :text, :string then String
|
||||
when :binary then String
|
||||
when :boolean then Object
|
||||
end
|
||||
end
|
||||
|
||||
# Casts value (which is a String) to an appropriate instance.
|
||||
def type_cast(value)
|
||||
return nil if value.nil?
|
||||
case type
|
||||
when :string then value
|
||||
when :text then value
|
||||
when :integer then value.to_i rescue value ? 1 : 0
|
||||
when :float then value.to_f
|
||||
when :decimal then self.class.value_to_decimal(value)
|
||||
when :datetime then self.class.string_to_time(value)
|
||||
when :timestamp then self.class.string_to_time(value)
|
||||
when :time then self.class.string_to_dummy_time(value)
|
||||
when :date then self.class.string_to_date(value)
|
||||
when :binary then self.class.binary_to_string(value)
|
||||
when :boolean then self.class.value_to_boolean(value)
|
||||
else value
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_code(var_name)
|
||||
case type
|
||||
when :string then nil
|
||||
when :text then nil
|
||||
when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)"
|
||||
when :float then "#{var_name}.to_f"
|
||||
when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
|
||||
when :datetime then "#{self.class.name}.string_to_time(#{var_name})"
|
||||
when :timestamp then "#{self.class.name}.string_to_time(#{var_name})"
|
||||
when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})"
|
||||
when :date then "#{self.class.name}.string_to_date(#{var_name})"
|
||||
when :binary then "#{self.class.name}.binary_to_string(#{var_name})"
|
||||
when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
|
||||
else nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the human name of the column name.
|
||||
#
|
||||
# ===== Examples
|
||||
# Column.new('sales_stage', ...).human_name # => 'Sales stage'
|
||||
def human_name
|
||||
Base.human_attribute_name(@name)
|
||||
end
|
||||
|
||||
def extract_default(default)
|
||||
type_cast(default)
|
||||
end
|
||||
|
||||
class << self
|
||||
# Used to convert from Strings to BLOBs
|
||||
def string_to_binary(value)
|
||||
value
|
||||
end
|
||||
|
||||
# Used to convert from BLOBs to Strings
|
||||
def binary_to_string(value)
|
||||
value
|
||||
end
|
||||
|
||||
def string_to_date(string)
|
||||
return string unless string.is_a?(String)
|
||||
return nil if string.empty?
|
||||
|
||||
fast_string_to_date(string) || fallback_string_to_date(string)
|
||||
end
|
||||
|
||||
def string_to_time(string)
|
||||
return string unless string.is_a?(String)
|
||||
return nil if string.empty?
|
||||
|
||||
fast_string_to_time(string) || fallback_string_to_time(string)
|
||||
end
|
||||
|
||||
def string_to_dummy_time(string)
|
||||
return string unless string.is_a?(String)
|
||||
return nil if string.empty?
|
||||
|
||||
string_to_time "2000-01-01 #{string}"
|
||||
end
|
||||
|
||||
# convert something to a boolean
|
||||
def value_to_boolean(value)
|
||||
if value.is_a?(String) && value.blank?
|
||||
nil
|
||||
else
|
||||
TRUE_VALUES.include?(value)
|
||||
end
|
||||
end
|
||||
|
||||
# convert something to a BigDecimal
|
||||
def value_to_decimal(value)
|
||||
# Using .class is faster than .is_a? and
|
||||
# subclasses of BigDecimal will be handled
|
||||
# in the else clause
|
||||
if value.class == BigDecimal
|
||||
value
|
||||
elsif value.respond_to?(:to_d)
|
||||
value.to_d
|
||||
else
|
||||
value.to_s.to_d
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# '0.123456' -> 123456
|
||||
# '1.123456' -> 123456
|
||||
def microseconds(time)
|
||||
((time[:sec_fraction].to_f % 1) * 1_000_000).to_i
|
||||
end
|
||||
|
||||
def new_date(year, mon, mday)
|
||||
if year && year != 0
|
||||
Date.new(year, mon, mday) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
def new_time(year, mon, mday, hour, min, sec, microsec)
|
||||
# Treat 0000-00-00 00:00:00 as nil.
|
||||
return nil if year.nil? || year == 0
|
||||
|
||||
Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
|
||||
end
|
||||
|
||||
def fast_string_to_date(string)
|
||||
if string =~ Format::ISO_DATE
|
||||
new_date $1.to_i, $2.to_i, $3.to_i
|
||||
end
|
||||
end
|
||||
|
||||
# Doesn't handle time zones.
|
||||
def fast_string_to_time(string)
|
||||
if string =~ Format::ISO_DATETIME
|
||||
microsec = ($7.to_f * 1_000_000).to_i
|
||||
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
|
||||
end
|
||||
end
|
||||
|
||||
def fallback_string_to_date(string)
|
||||
new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
|
||||
end
|
||||
|
||||
def fallback_string_to_time(string)
|
||||
time_hash = Date._parse(string)
|
||||
time_hash[:sec_fraction] = microseconds(time_hash)
|
||||
|
||||
new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def extract_limit(sql_type)
|
||||
$1.to_i if sql_type =~ /\((.*)\)/
|
||||
end
|
||||
|
||||
def extract_precision(sql_type)
|
||||
$2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
|
||||
end
|
||||
|
||||
def extract_scale(sql_type)
|
||||
case sql_type
|
||||
when /^(numeric|decimal|number)\((\d+)\)/i then 0
|
||||
when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def simplified_type(field_type)
|
||||
case field_type
|
||||
when /int/i
|
||||
:integer
|
||||
when /float|double/i
|
||||
:float
|
||||
when /decimal|numeric|number/i
|
||||
extract_scale(field_type) == 0 ? :integer : :decimal
|
||||
when /datetime/i
|
||||
:datetime
|
||||
when /timestamp/i
|
||||
:timestamp
|
||||
when /time/i
|
||||
:time
|
||||
when /date/i
|
||||
:date
|
||||
when /clob/i, /text/i
|
||||
:text
|
||||
when /blob/i, /binary/i
|
||||
:binary
|
||||
when /char/i, /string/i
|
||||
:string
|
||||
when /boolean/i
|
||||
:boolean
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class IndexDefinition < Struct.new(:table, :name, :unique, :columns) #:nodoc:
|
||||
end
|
||||
|
||||
# Abstract representation of a column definition. Instances of this type
|
||||
# are typically created by methods in TableDefinition, and added to the
|
||||
# +columns+ attribute of said TableDefinition object, in order to be used
|
||||
# for generating a number of table creation or table changing SQL statements.
|
||||
class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
|
||||
|
||||
def sql_type
|
||||
base.type_to_sql(type.to_sym, limit, precision, scale) rescue type
|
||||
end
|
||||
|
||||
def to_sql
|
||||
column_sql = "#{base.quote_column_name(name)} #{sql_type}"
|
||||
column_options = {}
|
||||
column_options[:null] = null unless null.nil?
|
||||
column_options[:default] = default unless default.nil?
|
||||
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
|
||||
column_sql
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_column_options!(sql, options)
|
||||
base.add_column_options!(sql, options.merge(:column => self))
|
||||
end
|
||||
end
|
||||
|
||||
# Represents the schema of an SQL table in an abstract way. This class
|
||||
# provides methods for manipulating the schema representation.
|
||||
#
|
||||
# Inside migration files, the +t+ object in +create_table+ and
|
||||
# +change_table+ is actually of this type:
|
||||
#
|
||||
# class SomeMigration < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :foo do |t|
|
||||
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The table definitions
|
||||
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
|
||||
class TableDefinition
|
||||
# An array of ColumnDefinition objects, representing the column changes
|
||||
# that have been defined.
|
||||
attr_accessor :columns
|
||||
|
||||
def initialize(base)
|
||||
@columns = []
|
||||
@base = base
|
||||
end
|
||||
|
||||
#Handles non supported datatypes - e.g. XML
|
||||
def method_missing(symbol, *args)
|
||||
if symbol.to_s == 'xml'
|
||||
xml_column_fallback(args)
|
||||
end
|
||||
end
|
||||
|
||||
def xml_column_fallback(*args)
|
||||
case @base.adapter_name.downcase
|
||||
when 'sqlite', 'mysql'
|
||||
options = args.extract_options!
|
||||
column(args[0], :text, options)
|
||||
end
|
||||
end
|
||||
# Appends a primary key definition to the table definition.
|
||||
# Can be called multiple times, but this is probably not a good idea.
|
||||
def primary_key(name)
|
||||
column(name, :primary_key)
|
||||
end
|
||||
|
||||
# Returns a ColumnDefinition for the column with name +name+.
|
||||
def [](name)
|
||||
@columns.find {|column| column.name.to_s == name.to_s}
|
||||
end
|
||||
|
||||
# Instantiates a new column for the table.
|
||||
# The +type+ parameter is normally one of the migrations native types,
|
||||
# which is one of the following:
|
||||
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
|
||||
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
|
||||
# <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
|
||||
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
|
||||
#
|
||||
# You may use a type not in this list as long as it is supported by your
|
||||
# database (for example, "polygon" in MySQL), but this will not be database
|
||||
# agnostic and should usually be avoided.
|
||||
#
|
||||
# Available options are (none of these exists by default):
|
||||
# * <tt>:limit</tt> -
|
||||
# Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
|
||||
# * <tt>:default</tt> -
|
||||
# The column's default value. Use nil for NULL.
|
||||
# * <tt>:null</tt> -
|
||||
# Allows or disallows +NULL+ values in the column. This option could
|
||||
# have been named <tt>:null_allowed</tt>.
|
||||
# * <tt>:precision</tt> -
|
||||
# Specifies the precision for a <tt>:decimal</tt> column.
|
||||
# * <tt>:scale</tt> -
|
||||
# Specifies the scale for a <tt>:decimal</tt> column.
|
||||
#
|
||||
# For clarity's sake: the precision is the number of significant digits,
|
||||
# while the scale is the number of digits that can be stored following
|
||||
# the decimal point. For example, the number 123.45 has a precision of 5
|
||||
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
|
||||
# range from -999.99 to 999.99.
|
||||
#
|
||||
# Please be aware of different RDBMS implementations behavior with
|
||||
# <tt>:decimal</tt> columns:
|
||||
# * The SQL standard says the default scale should be 0, <tt>:scale</tt> <=
|
||||
# <tt>:precision</tt>, and makes no comments about the requirements of
|
||||
# <tt>:precision</tt>.
|
||||
# * MySQL: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..30].
|
||||
# Default is (10,0).
|
||||
# * PostgreSQL: <tt>:precision</tt> [1..infinity],
|
||||
# <tt>:scale</tt> [0..infinity]. No default.
|
||||
# * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used.
|
||||
# Internal storage as strings. No default.
|
||||
# * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>,
|
||||
# but the maximum supported <tt>:precision</tt> is 16. No default.
|
||||
# * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127].
|
||||
# Default is (38,0).
|
||||
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
|
||||
# Default unknown.
|
||||
# * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
|
||||
# Default (9,0). Internal types NUMERIC and DECIMAL have different
|
||||
# storage rules, decimal being better.
|
||||
# * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
|
||||
# Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
|
||||
# NUMERIC is 19, and DECIMAL is 38.
|
||||
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
|
||||
# Default (38,0).
|
||||
# * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
|
||||
# Default (38,0).
|
||||
# * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
|
||||
#
|
||||
# This method returns <tt>self</tt>.
|
||||
#
|
||||
# == Examples
|
||||
# # Assuming td is an instance of TableDefinition
|
||||
# td.column(:granted, :boolean)
|
||||
# # granted BOOLEAN
|
||||
#
|
||||
# td.column(:picture, :binary, :limit => 2.megabytes)
|
||||
# # => picture BLOB(2097152)
|
||||
#
|
||||
# td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
|
||||
# # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
|
||||
#
|
||||
# td.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
|
||||
# # => bill_gates_money DECIMAL(15,2)
|
||||
#
|
||||
# td.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
|
||||
# # => sensor_reading DECIMAL(30,20)
|
||||
#
|
||||
# # While <tt>:scale</tt> defaults to zero on most databases, it
|
||||
# # probably wouldn't hurt to include it.
|
||||
# td.column(:huge_integer, :decimal, :precision => 30)
|
||||
# # => huge_integer DECIMAL(30)
|
||||
#
|
||||
# # Defines a column with a database-specific type.
|
||||
# td.column(:foo, 'polygon')
|
||||
# # => foo polygon
|
||||
#
|
||||
# == Short-hand examples
|
||||
#
|
||||
# Instead of calling +column+ directly, you can also work with the short-hand definitions for the default types.
|
||||
# They use the type as the method name instead of as a parameter and allow for multiple columns to be defined
|
||||
# in a single statement.
|
||||
#
|
||||
# What can be written like this with the regular calls to column:
|
||||
#
|
||||
# create_table "products", :force => true do |t|
|
||||
# t.column "shop_id", :integer
|
||||
# t.column "creator_id", :integer
|
||||
# t.column "name", :string, :default => "Untitled"
|
||||
# t.column "value", :string, :default => "Untitled"
|
||||
# t.column "created_at", :datetime
|
||||
# t.column "updated_at", :datetime
|
||||
# end
|
||||
#
|
||||
# Can also be written as follows using the short-hand:
|
||||
#
|
||||
# create_table :products do |t|
|
||||
# t.integer :shop_id, :creator_id
|
||||
# t.string :name, :value, :default => "Untitled"
|
||||
# t.timestamps
|
||||
# end
|
||||
#
|
||||
# There's a short-hand method for each of the type values declared at the top. And then there's
|
||||
# TableDefinition#timestamps that'll add created_at and +updated_at+ as datetimes.
|
||||
#
|
||||
# TableDefinition#references will add an appropriately-named _id column, plus a corresponding _type
|
||||
# column if the <tt>:polymorphic</tt> option is supplied. If <tt>:polymorphic</tt> is a hash of options, these will be
|
||||
# used when creating the <tt>_type</tt> column. So what can be written like this:
|
||||
#
|
||||
# create_table :taggings do |t|
|
||||
# t.integer :tag_id, :tagger_id, :taggable_id
|
||||
# t.string :tagger_type
|
||||
# t.string :taggable_type, :default => 'Photo'
|
||||
# end
|
||||
#
|
||||
# Can also be written as follows using references:
|
||||
#
|
||||
# create_table :taggings do |t|
|
||||
# t.references :tag
|
||||
# t.references :tagger, :polymorphic => true
|
||||
# t.references :taggable, :polymorphic => { :default => 'Photo' }
|
||||
# end
|
||||
def column(name, type, options = {})
|
||||
column = self[name] || ColumnDefinition.new(@base, name, type)
|
||||
if options[:limit]
|
||||
column.limit = options[:limit]
|
||||
elsif native[type.to_sym].is_a?(Hash)
|
||||
column.limit = native[type.to_sym][:limit]
|
||||
end
|
||||
column.precision = options[:precision]
|
||||
column.scale = options[:scale]
|
||||
column.default = options[:default]
|
||||
column.null = options[:null]
|
||||
@columns << column unless @columns.include? column
|
||||
self
|
||||
end
|
||||
|
||||
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
||||
class_eval <<-EOV
|
||||
def #{column_type}(*args) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each { |name| column(name, '#{column_type}', options) } # column_names.each { |name| column(name, 'string', options) }
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
|
||||
# <tt>:updated_at</tt> to the table.
|
||||
def timestamps(*args)
|
||||
options = args.extract_options!
|
||||
column(:created_at, :datetime, options)
|
||||
column(:updated_at, :datetime, options)
|
||||
end
|
||||
|
||||
def references(*args)
|
||||
options = args.extract_options!
|
||||
polymorphic = options.delete(:polymorphic)
|
||||
args.each do |col|
|
||||
column("#{col}_id", :integer, options)
|
||||
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
|
||||
end
|
||||
end
|
||||
alias :belongs_to :references
|
||||
|
||||
# Returns a String whose contents are the column definitions
|
||||
# concatenated together. This string can then be prepended and appended to
|
||||
# to generate the final SQL to create the table.
|
||||
def to_sql
|
||||
@columns.map(&:to_sql) * ', '
|
||||
end
|
||||
|
||||
private
|
||||
def native
|
||||
@base.native_database_types
|
||||
end
|
||||
end
|
||||
|
||||
# Represents a SQL table in an abstract way for updating a table.
|
||||
# Also see TableDefinition and SchemaStatements#create_table
|
||||
#
|
||||
# Available transformations are:
|
||||
#
|
||||
# change_table :table do |t|
|
||||
# t.column
|
||||
# t.index
|
||||
# t.timestamps
|
||||
# t.change
|
||||
# t.change_default
|
||||
# t.rename
|
||||
# t.references
|
||||
# t.belongs_to
|
||||
# t.string
|
||||
# t.text
|
||||
# t.integer
|
||||
# t.float
|
||||
# t.decimal
|
||||
# t.datetime
|
||||
# t.timestamp
|
||||
# t.time
|
||||
# t.date
|
||||
# t.binary
|
||||
# t.boolean
|
||||
# t.remove
|
||||
# t.remove_references
|
||||
# t.remove_belongs_to
|
||||
# t.remove_index
|
||||
# t.remove_timestamps
|
||||
# end
|
||||
#
|
||||
class Table
|
||||
def initialize(table_name, base)
|
||||
@table_name = table_name
|
||||
@base = base
|
||||
end
|
||||
|
||||
# Adds a new column to the named table.
|
||||
# See TableDefinition#column for details of the options you can use.
|
||||
# ===== Example
|
||||
# ====== Creating a simple column
|
||||
# t.column(:name, :string)
|
||||
def column(column_name, type, options = {})
|
||||
@base.add_column(@table_name, column_name, type, options)
|
||||
end
|
||||
|
||||
# Adds a new index to the table. +column_name+ can be a single Symbol, or
|
||||
# an Array of Symbols. See SchemaStatements#add_index
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Creating a simple index
|
||||
# t.index(:name)
|
||||
# ====== Creating a unique index
|
||||
# t.index([:branch_id, :party_id], :unique => true)
|
||||
# ====== Creating a named index
|
||||
# t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
|
||||
def index(column_name, options = {})
|
||||
@base.add_index(@table_name, column_name, options)
|
||||
end
|
||||
|
||||
# Adds timestamps (created_at and updated_at) columns to the table. See SchemaStatements#add_timestamps
|
||||
# ===== Example
|
||||
# t.timestamps
|
||||
def timestamps
|
||||
@base.add_timestamps(@table_name)
|
||||
end
|
||||
|
||||
# Changes the column's definition according to the new options.
|
||||
# See TableDefinition#column for details of the options you can use.
|
||||
# ===== Examples
|
||||
# t.change(:name, :string, :limit => 80)
|
||||
# t.change(:description, :text)
|
||||
def change(column_name, type, options = {})
|
||||
@base.change_column(@table_name, column_name, type, options)
|
||||
end
|
||||
|
||||
# Sets a new default value for a column. See SchemaStatements#change_column_default
|
||||
# ===== Examples
|
||||
# t.change_default(:qualification, 'new')
|
||||
# t.change_default(:authorized, 1)
|
||||
def change_default(column_name, default)
|
||||
@base.change_column_default(@table_name, column_name, default)
|
||||
end
|
||||
|
||||
# Removes the column(s) from the table definition.
|
||||
# ===== Examples
|
||||
# t.remove(:qualification)
|
||||
# t.remove(:qualification, :experience)
|
||||
def remove(*column_names)
|
||||
@base.remove_column(@table_name, column_names)
|
||||
end
|
||||
|
||||
# Removes the given index from the table.
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Remove the suppliers_name_index in the suppliers table
|
||||
# t.remove_index :name
|
||||
# ====== Remove the index named accounts_branch_id_index in the accounts table
|
||||
# t.remove_index :column => :branch_id
|
||||
# ====== Remove the index named accounts_branch_id_party_id_index in the accounts table
|
||||
# t.remove_index :column => [:branch_id, :party_id]
|
||||
# ====== Remove the index named by_branch_party in the accounts table
|
||||
# t.remove_index :name => :by_branch_party
|
||||
def remove_index(options = {})
|
||||
@base.remove_index(@table_name, options)
|
||||
end
|
||||
|
||||
# Removes the timestamp columns (created_at and updated_at) from the table.
|
||||
# ===== Example
|
||||
# t.remove_timestamps
|
||||
def remove_timestamps
|
||||
@base.remove_timestamps(@table_name)
|
||||
end
|
||||
|
||||
# Renames a column.
|
||||
# ===== Example
|
||||
# t.rename(:description, :name)
|
||||
def rename(column_name, new_column_name)
|
||||
@base.rename_column(@table_name, column_name, new_column_name)
|
||||
end
|
||||
|
||||
# Adds a reference. Optionally adds a +type+ column.
|
||||
# <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
|
||||
# ===== Examples
|
||||
# t.references(:goat)
|
||||
# t.references(:goat, :polymorphic => true)
|
||||
# t.belongs_to(:goat)
|
||||
def references(*args)
|
||||
options = args.extract_options!
|
||||
polymorphic = options.delete(:polymorphic)
|
||||
args.each do |col|
|
||||
@base.add_column(@table_name, "#{col}_id", :integer, options)
|
||||
@base.add_column(@table_name, "#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) unless polymorphic.nil?
|
||||
end
|
||||
end
|
||||
alias :belongs_to :references
|
||||
|
||||
# Removes a reference. Optionally removes a +type+ column.
|
||||
# <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
|
||||
# ===== Examples
|
||||
# t.remove_references(:goat)
|
||||
# t.remove_references(:goat, :polymorphic => true)
|
||||
# t.remove_belongs_to(:goat)
|
||||
def remove_references(*args)
|
||||
options = args.extract_options!
|
||||
polymorphic = options.delete(:polymorphic)
|
||||
args.each do |col|
|
||||
@base.remove_column(@table_name, "#{col}_id")
|
||||
@base.remove_column(@table_name, "#{col}_type") unless polymorphic.nil?
|
||||
end
|
||||
end
|
||||
alias :remove_belongs_to :remove_references
|
||||
|
||||
# Adds a column or columns of a specified type
|
||||
# ===== Examples
|
||||
# t.string(:goat)
|
||||
# t.string(:goat, :sheep)
|
||||
%w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
|
||||
class_eval <<-EOV
|
||||
def #{column_type}(*args) # def string(*args)
|
||||
options = args.extract_options! # options = args.extract_options!
|
||||
column_names = args # column_names = args
|
||||
#
|
||||
column_names.each do |name| # column_names.each do |name|
|
||||
column = ColumnDefinition.new(@base, name, '#{column_type}') # column = ColumnDefinition.new(@base, name, 'string')
|
||||
if options[:limit] # if options[:limit]
|
||||
column.limit = options[:limit] # column.limit = options[:limit]
|
||||
elsif native['#{column_type}'.to_sym].is_a?(Hash) # elsif native['string'.to_sym].is_a?(Hash)
|
||||
column.limit = native['#{column_type}'.to_sym][:limit] # column.limit = native['string'.to_sym][:limit]
|
||||
end # end
|
||||
column.precision = options[:precision] # column.precision = options[:precision]
|
||||
column.scale = options[:scale] # column.scale = options[:scale]
|
||||
column.default = options[:default] # column.default = options[:default]
|
||||
column.null = options[:null] # column.null = options[:null]
|
||||
@base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
|
||||
end # end
|
||||
end # end
|
||||
EOV
|
||||
end
|
||||
|
||||
private
|
||||
def native
|
||||
@base.native_database_types
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
module SchemaStatements
|
||||
# Returns a Hash of mappings from the abstract data types to the native
|
||||
# database types. See TableDefinition#column for details on the recognized
|
||||
# abstract data types.
|
||||
def native_database_types
|
||||
{}
|
||||
end
|
||||
|
||||
# This is the maximum length a table alias can be
|
||||
def table_alias_length
|
||||
255
|
||||
end
|
||||
|
||||
# Truncates a table alias according to the limits of the current adapter.
|
||||
def table_alias_for(table_name)
|
||||
table_name[0..table_alias_length-1].gsub(/\./, '_')
|
||||
end
|
||||
|
||||
# def tables(name = nil) end
|
||||
|
||||
def table_exists?(table_name)
|
||||
tables.include?(table_name.to_s)
|
||||
end
|
||||
|
||||
# Returns an array of indexes for the given table.
|
||||
# def indexes(table_name, name = nil) end
|
||||
|
||||
# Returns an array of Column objects for the table specified by +table_name+.
|
||||
# See the concrete implementation for details on the expected parameter values.
|
||||
def columns(table_name, name = nil) end
|
||||
|
||||
# Creates a new table with the name +table_name+. +table_name+ may either
|
||||
# be a String or a Symbol.
|
||||
#
|
||||
# There are two ways to work with +create_table+. You can use the block
|
||||
# form or the regular form, like this:
|
||||
#
|
||||
# === Block form
|
||||
# # create_table() passes a TableDefinition object to the block.
|
||||
# # This form will not only create the table, but also columns for the
|
||||
# # table.
|
||||
# create_table(:suppliers) do |t|
|
||||
# t.column :name, :string, :limit => 60
|
||||
# # Other fields here
|
||||
# end
|
||||
#
|
||||
# === Regular form
|
||||
# # Creates a table called 'suppliers' with no columns.
|
||||
# create_table(:suppliers)
|
||||
# # Add a column to 'suppliers'.
|
||||
# add_column(:suppliers, :name, :string, {:limit => 60})
|
||||
#
|
||||
# The +options+ hash can include the following keys:
|
||||
# [<tt>:id</tt>]
|
||||
# Whether to automatically add a primary key column. Defaults to true.
|
||||
# Join tables for +has_and_belongs_to_many+ should set <tt>:id => false</tt>.
|
||||
# [<tt>:primary_key</tt>]
|
||||
# The name of the primary key, if one is to be added automatically.
|
||||
# Defaults to +id+.
|
||||
# [<tt>:options</tt>]
|
||||
# Any extra options you want appended to the table definition.
|
||||
# [<tt>:temporary</tt>]
|
||||
# Make a temporary table.
|
||||
# [<tt>:force</tt>]
|
||||
# Set to true to drop the table before creating it.
|
||||
# Defaults to false.
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Add a backend specific option to the generated SQL (MySQL)
|
||||
# create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
|
||||
# generates:
|
||||
# CREATE TABLE suppliers (
|
||||
# id int(11) DEFAULT NULL auto_increment PRIMARY KEY
|
||||
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||
#
|
||||
# ====== Rename the primary key column
|
||||
# create_table(:objects, :primary_key => 'guid') do |t|
|
||||
# t.column :name, :string, :limit => 80
|
||||
# end
|
||||
# generates:
|
||||
# CREATE TABLE objects (
|
||||
# guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
|
||||
# name varchar(80)
|
||||
# )
|
||||
#
|
||||
# ====== Do not add a primary key column
|
||||
# create_table(:categories_suppliers, :id => false) do |t|
|
||||
# t.column :category_id, :integer
|
||||
# t.column :supplier_id, :integer
|
||||
# end
|
||||
# generates:
|
||||
# CREATE TABLE categories_suppliers (
|
||||
# category_id int,
|
||||
# supplier_id int
|
||||
# )
|
||||
#
|
||||
# See also TableDefinition#column for details on how to create columns.
|
||||
def create_table(table_name, options = {})
|
||||
table_definition = TableDefinition.new(self)
|
||||
table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
|
||||
|
||||
yield table_definition
|
||||
|
||||
if options[:force] && table_exists?(table_name)
|
||||
drop_table(table_name, options)
|
||||
end
|
||||
|
||||
create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
|
||||
create_sql << "#{quote_table_name(table_name)} ("
|
||||
create_sql << table_definition.to_sql
|
||||
create_sql << ") #{options[:options]}"
|
||||
execute create_sql
|
||||
end
|
||||
|
||||
# A block for changing columns in +table+.
|
||||
#
|
||||
# === Example
|
||||
# # change_table() yields a Table instance
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.column :name, :string, :limit => 60
|
||||
# # Other column alterations here
|
||||
# end
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Add a column
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.column :name, :string, :limit => 60
|
||||
# end
|
||||
#
|
||||
# ====== Add 2 integer columns
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.integer :width, :height, :null => false, :default => 0
|
||||
# end
|
||||
#
|
||||
# ====== Add created_at/updated_at columns
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.timestamps
|
||||
# end
|
||||
#
|
||||
# ====== Add a foreign key column
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.references :company
|
||||
# end
|
||||
#
|
||||
# Creates a <tt>company_id(integer)</tt> column
|
||||
#
|
||||
# ====== Add a polymorphic foreign key column
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.belongs_to :company, :polymorphic => true
|
||||
# end
|
||||
#
|
||||
# Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
|
||||
#
|
||||
# ====== Remove a column
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.remove :company
|
||||
# end
|
||||
#
|
||||
# ====== Remove several columns
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.remove :company_id
|
||||
# t.remove :width, :height
|
||||
# end
|
||||
#
|
||||
# ====== Remove an index
|
||||
# change_table(:suppliers) do |t|
|
||||
# t.remove_index :company_id
|
||||
# end
|
||||
#
|
||||
# See also Table for details on
|
||||
# all of the various column transformation
|
||||
def change_table(table_name)
|
||||
yield Table.new(table_name, self)
|
||||
end
|
||||
|
||||
# Renames a table.
|
||||
# ===== Example
|
||||
# rename_table('octopuses', 'octopi')
|
||||
def rename_table(table_name, new_name)
|
||||
raise NotImplementedError, "rename_table is not implemented"
|
||||
end
|
||||
|
||||
# Drops a table from the database.
|
||||
def drop_table(table_name, options = {})
|
||||
execute "DROP TABLE #{quote_table_name(table_name)}"
|
||||
end
|
||||
|
||||
# Adds a new column to the named table.
|
||||
# See TableDefinition#column for details of the options you can use.
|
||||
def add_column(table_name, column_name, type, options = {})
|
||||
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(add_column_sql, options)
|
||||
execute(add_column_sql)
|
||||
end
|
||||
|
||||
# Removes the column(s) from the table definition.
|
||||
# ===== Examples
|
||||
# remove_column(:suppliers, :qualification)
|
||||
# remove_columns(:suppliers, :qualification, :experience)
|
||||
def remove_column(table_name, *column_names)
|
||||
column_names.flatten.each do |column_name|
|
||||
execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
|
||||
end
|
||||
end
|
||||
alias :remove_columns :remove_column
|
||||
|
||||
# Changes the column's definition according to the new options.
|
||||
# See TableDefinition#column for details of the options you can use.
|
||||
# ===== Examples
|
||||
# change_column(:suppliers, :name, :string, :limit => 80)
|
||||
# change_column(:accounts, :description, :text)
|
||||
def change_column(table_name, column_name, type, options = {})
|
||||
raise NotImplementedError, "change_column is not implemented"
|
||||
end
|
||||
|
||||
# Sets a new default value for a column. If you want to set the default
|
||||
# value to +NULL+, you are out of luck. You need to
|
||||
# DatabaseStatements#execute the appropriate SQL statement yourself.
|
||||
# ===== Examples
|
||||
# change_column_default(:suppliers, :qualification, 'new')
|
||||
# change_column_default(:accounts, :authorized, 1)
|
||||
def change_column_default(table_name, column_name, default)
|
||||
raise NotImplementedError, "change_column_default is not implemented"
|
||||
end
|
||||
|
||||
# Renames a column.
|
||||
# ===== Example
|
||||
# rename_column(:suppliers, :description, :name)
|
||||
def rename_column(table_name, column_name, new_column_name)
|
||||
raise NotImplementedError, "rename_column is not implemented"
|
||||
end
|
||||
|
||||
# Adds a new index to the table. +column_name+ can be a single Symbol, or
|
||||
# an Array of Symbols.
|
||||
#
|
||||
# The index will be named after the table and the first column name,
|
||||
# unless you pass <tt>:name</tt> as an option.
|
||||
#
|
||||
# When creating an index on multiple columns, the first column is used as a name
|
||||
# for the index. For example, when you specify an index on two columns
|
||||
# [<tt>:first</tt>, <tt>:last</tt>], the DBMS creates an index for both columns as well as an
|
||||
# index for the first column <tt>:first</tt>. Using just the first name for this index
|
||||
# makes sense, because you will never have to create a singular index with this
|
||||
# name.
|
||||
#
|
||||
# ===== Examples
|
||||
# ====== Creating a simple index
|
||||
# add_index(:suppliers, :name)
|
||||
# generates
|
||||
# CREATE INDEX suppliers_name_index ON suppliers(name)
|
||||
# ====== Creating a unique index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true)
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
|
||||
# ====== Creating a named index
|
||||
# add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
|
||||
# generates
|
||||
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
|
||||
def add_index(table_name, column_name, options = {})
|
||||
column_names = Array(column_name)
|
||||
index_name = index_name(table_name, :column => column_names)
|
||||
|
||||
if Hash === options # legacy support, since this param was a string
|
||||
index_type = options[:unique] ? "UNIQUE" : ""
|
||||
index_name = options[:name] || index_name
|
||||
else
|
||||
index_type = options
|
||||
end
|
||||
quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ")
|
||||
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})"
|
||||
end
|
||||
|
||||
# Remove the given index from the table.
|
||||
#
|
||||
# Remove the suppliers_name_index in the suppliers table.
|
||||
# remove_index :suppliers, :name
|
||||
# Remove the index named accounts_branch_id_index in the accounts table.
|
||||
# remove_index :accounts, :column => :branch_id
|
||||
# Remove the index named accounts_branch_id_party_id_index in the accounts table.
|
||||
# remove_index :accounts, :column => [:branch_id, :party_id]
|
||||
# Remove the index named by_branch_party in the accounts table.
|
||||
# remove_index :accounts, :name => :by_branch_party
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))} ON #{table_name}"
|
||||
end
|
||||
|
||||
def index_name(table_name, options) #:nodoc:
|
||||
if Hash === options # legacy support
|
||||
if options[:column]
|
||||
"index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
|
||||
elsif options[:name]
|
||||
options[:name]
|
||||
else
|
||||
raise ArgumentError, "You must specify the index name"
|
||||
end
|
||||
else
|
||||
index_name(table_name, :column => options)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
|
||||
# entire structure of the database.
|
||||
def structure_dump
|
||||
end
|
||||
|
||||
def dump_schema_information #:nodoc:
|
||||
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
|
||||
migrated = select_values("SELECT version FROM #{sm_table}")
|
||||
migrated.map { |v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}');" }.join("\n\n")
|
||||
end
|
||||
|
||||
# Should not be called normally, but this operation is non-destructive.
|
||||
# The migrations module handles this automatically.
|
||||
def initialize_schema_migrations_table
|
||||
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
|
||||
|
||||
unless tables.detect { |t| t == sm_table }
|
||||
create_table(sm_table, :id => false) do |schema_migrations_table|
|
||||
schema_migrations_table.column :version, :string, :null => false
|
||||
end
|
||||
add_index sm_table, :version, :unique => true,
|
||||
:name => "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
|
||||
|
||||
# Backwards-compatibility: if we find schema_info, assume we've
|
||||
# migrated up to that point:
|
||||
si_table = Base.table_name_prefix + 'schema_info' + Base.table_name_suffix
|
||||
|
||||
if tables.detect { |t| t == si_table }
|
||||
|
||||
old_version = select_value("SELECT version FROM #{quote_table_name(si_table)}").to_i
|
||||
assume_migrated_upto_version(old_version)
|
||||
drop_table(si_table)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def assume_migrated_upto_version(version)
|
||||
version = version.to_i
|
||||
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
|
||||
|
||||
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
|
||||
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
|
||||
filename.split('/').last.split('_').first.to_i
|
||||
end
|
||||
|
||||
unless migrated.include?(version)
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
|
||||
end
|
||||
|
||||
inserted = Set.new
|
||||
(versions - migrated).each do |v|
|
||||
if inserted.include?(v)
|
||||
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
|
||||
elsif v < version
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
|
||||
inserted << v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
||||
if native = native_database_types[type]
|
||||
column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup
|
||||
|
||||
if type == :decimal # ignore limit, use precision and scale
|
||||
scale ||= native[:scale]
|
||||
|
||||
if precision ||= native[:precision]
|
||||
if scale
|
||||
column_type_sql << "(#{precision},#{scale})"
|
||||
else
|
||||
column_type_sql << "(#{precision})"
|
||||
end
|
||||
elsif scale
|
||||
raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
|
||||
end
|
||||
|
||||
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
|
||||
column_type_sql << "(#{limit})"
|
||||
end
|
||||
|
||||
column_type_sql
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
def add_column_options!(sql, options) #:nodoc:
|
||||
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
|
||||
# must explicitly check for :null to allow change_column to work on migrations
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
end
|
||||
end
|
||||
|
||||
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
||||
# Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
|
||||
#
|
||||
# distinct("posts.id", "posts.created_at desc")
|
||||
def distinct(columns, order_by)
|
||||
"DISTINCT #{columns}"
|
||||
end
|
||||
|
||||
# ORDER BY clause for the passed order option.
|
||||
# PostgreSQL overrides this due to its stricter standards compliance.
|
||||
def add_order_by_for_association_limiting!(sql, options)
|
||||
sql << " ORDER BY #{options[:order]}"
|
||||
end
|
||||
|
||||
# Adds timestamps (created_at and updated_at) columns to the named table.
|
||||
# ===== Examples
|
||||
# add_timestamps(:suppliers)
|
||||
def add_timestamps(table_name)
|
||||
add_column table_name, :created_at, :datetime
|
||||
add_column table_name, :updated_at, :datetime
|
||||
end
|
||||
|
||||
# Removes the timestamp columns (created_at and updated_at) from the table definition.
|
||||
# ===== Examples
|
||||
# remove_timestamps(:suppliers)
|
||||
def remove_timestamps(table_name)
|
||||
remove_column table_name, :updated_at
|
||||
remove_column table_name, :created_at
|
||||
end
|
||||
|
||||
protected
|
||||
def options_include_default?(options)
|
||||
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,241 +0,0 @@
|
||||
require 'benchmark'
|
||||
require 'date'
|
||||
require 'bigdecimal'
|
||||
require 'bigdecimal/util'
|
||||
|
||||
# TODO: Autoload these files
|
||||
require 'active_record/connection_adapters/abstract/schema_definitions'
|
||||
require 'active_record/connection_adapters/abstract/schema_statements'
|
||||
require 'active_record/connection_adapters/abstract/database_statements'
|
||||
require 'active_record/connection_adapters/abstract/quoting'
|
||||
require 'active_record/connection_adapters/abstract/connection_pool'
|
||||
require 'active_record/connection_adapters/abstract/connection_specification'
|
||||
require 'active_record/connection_adapters/abstract/query_cache'
|
||||
|
||||
module ActiveRecord
|
||||
module ConnectionAdapters # :nodoc:
|
||||
# ActiveRecord supports multiple database systems. AbstractAdapter and
|
||||
# related classes form the abstraction layer which makes this possible.
|
||||
# An AbstractAdapter represents a connection to a database, and provides an
|
||||
# abstract interface for database-specific functionality such as establishing
|
||||
# a connection, escaping values, building the right SQL fragments for ':offset'
|
||||
# and ':limit' options, etc.
|
||||
#
|
||||
# All the concrete database adapters follow the interface laid down in this class.
|
||||
# ActiveRecord::Base.connection returns an AbstractAdapter object, which
|
||||
# you can use.
|
||||
#
|
||||
# Most of the methods in the adapter are useful during migrations. Most
|
||||
# notably, the instance methods provided by SchemaStatement are very useful.
|
||||
class AbstractAdapter
|
||||
include Quoting, DatabaseStatements, SchemaStatements
|
||||
include QueryCache
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :checkout, :checkin
|
||||
|
||||
@@row_even = true
|
||||
|
||||
def initialize(connection, logger = nil) #:nodoc:
|
||||
@connection, @logger = connection, logger
|
||||
@runtime = 0
|
||||
@last_verification = 0
|
||||
@query_cache_enabled = false
|
||||
end
|
||||
|
||||
# Returns the human-readable name of the adapter. Use mixed case - one
|
||||
# can always use downcase if needed.
|
||||
def adapter_name
|
||||
'Abstract'
|
||||
end
|
||||
|
||||
# Does this adapter support migrations? Backend specific, as the
|
||||
# abstract adapter always returns +false+.
|
||||
def supports_migrations?
|
||||
false
|
||||
end
|
||||
|
||||
# Can this adapter determine the primary key for tables not attached
|
||||
# to an ActiveRecord class, such as join tables? Backend specific, as
|
||||
# the abstract adapter always returns +false+.
|
||||
def supports_primary_key?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support using DISTINCT within COUNT? This is +true+
|
||||
# for all adapters except sqlite.
|
||||
def supports_count_distinct?
|
||||
true
|
||||
end
|
||||
|
||||
# Does this adapter support DDL rollbacks in transactions? That is, would
|
||||
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
|
||||
# SQL Server, and others support this. MySQL and others do not.
|
||||
def supports_ddl_transactions?
|
||||
false
|
||||
end
|
||||
|
||||
# Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite
|
||||
# does not.
|
||||
def supports_savepoints?
|
||||
false
|
||||
end
|
||||
|
||||
# Should primary key values be selected from their corresponding
|
||||
# sequence before the insert statement? If true, next_sequence_value
|
||||
# is called before each insert to set the record's primary key.
|
||||
# This is false for all adapters but Firebird.
|
||||
def prefetch_primary_key?(table_name = nil)
|
||||
false
|
||||
end
|
||||
|
||||
def reset_runtime #:nodoc:
|
||||
rt, @runtime = @runtime, 0
|
||||
rt
|
||||
end
|
||||
|
||||
# QUOTING ==================================================
|
||||
|
||||
# Override to return the quoted table name. Defaults to column quoting.
|
||||
def quote_table_name(name)
|
||||
quote_column_name(name)
|
||||
end
|
||||
|
||||
# REFERENTIAL INTEGRITY ====================================
|
||||
|
||||
# Override to turn off referential integrity while executing <tt>&block</tt>.
|
||||
def disable_referential_integrity(&block)
|
||||
yield
|
||||
end
|
||||
|
||||
# CONNECTION MANAGEMENT ====================================
|
||||
|
||||
# Checks whether the connection to the database is still active. This includes
|
||||
# checking whether the database is actually capable of responding, i.e. whether
|
||||
# the connection isn't stale.
|
||||
def active?
|
||||
@active != false
|
||||
end
|
||||
|
||||
# Disconnects from the database if already connected, and establishes a
|
||||
# new connection with the database.
|
||||
def reconnect!
|
||||
@active = true
|
||||
end
|
||||
|
||||
# Disconnects from the database if already connected. Otherwise, this
|
||||
# method does nothing.
|
||||
def disconnect!
|
||||
@active = false
|
||||
end
|
||||
|
||||
# Reset the state of this connection, directing the DBMS to clear
|
||||
# transactions and other connection-related server-side state. Usually a
|
||||
# database-dependent operation.
|
||||
#
|
||||
# The default implementation does nothing; the implementation should be
|
||||
# overridden by concrete adapters.
|
||||
def reset!
|
||||
# this should be overridden by concrete adapters
|
||||
end
|
||||
|
||||
# Returns true if its safe to reload the connection between requests for development mode.
|
||||
def requires_reloading?
|
||||
true
|
||||
end
|
||||
|
||||
# Checks whether the connection to the database is still active (i.e. not stale).
|
||||
# This is done under the hood by calling <tt>active?</tt>. If the connection
|
||||
# is no longer active, then this method will reconnect to the database.
|
||||
def verify!(*ignored)
|
||||
reconnect! unless active?
|
||||
end
|
||||
|
||||
# Provides access to the underlying database driver for this adapter. For
|
||||
# example, this method returns a Mysql object in case of MysqlAdapter,
|
||||
# and a PGconn object in case of PostgreSQLAdapter.
|
||||
#
|
||||
# This is useful for when you need to call a proprietary method such as
|
||||
# PostgreSQL's lo_* methods.
|
||||
def raw_connection
|
||||
@connection
|
||||
end
|
||||
|
||||
def open_transactions
|
||||
@open_transactions ||= 0
|
||||
end
|
||||
|
||||
def increment_open_transactions
|
||||
@open_transactions ||= 0
|
||||
@open_transactions += 1
|
||||
end
|
||||
|
||||
def decrement_open_transactions
|
||||
@open_transactions -= 1
|
||||
end
|
||||
|
||||
def transaction_joinable=(joinable)
|
||||
@transaction_joinable = joinable
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
end
|
||||
|
||||
def current_savepoint_name
|
||||
"active_record_#{open_transactions}"
|
||||
end
|
||||
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def log(sql, name)
|
||||
if block_given?
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = yield }
|
||||
@runtime += ms
|
||||
log_info(sql, name, ms)
|
||||
result
|
||||
else
|
||||
log_info(sql, name, 0)
|
||||
nil
|
||||
end
|
||||
rescue Exception => e
|
||||
# Log message and raise exception.
|
||||
# Set last_verification to 0, so that connection gets verified
|
||||
# upon reentering the request loop
|
||||
@last_verification = 0
|
||||
message = "#{e.class.name}: #{e.message}: #{sql}"
|
||||
log_info(message, name, 0)
|
||||
raise ActiveRecord::StatementInvalid, message
|
||||
end
|
||||
|
||||
def format_log_entry(message, dump = nil)
|
||||
if ActiveRecord::Base.colorize_logging
|
||||
if @@row_even
|
||||
@@row_even = false
|
||||
message_color, dump_color = "4;36;1", "0;1"
|
||||
else
|
||||
@@row_even = true
|
||||
message_color, dump_color = "4;35;1", "0"
|
||||
end
|
||||
|
||||
log_entry = " \e[#{message_color}m#{message}\e[0m "
|
||||
log_entry << "\e[#{dump_color}m%#{String === dump ? 's' : 'p'}\e[0m" % dump if dump
|
||||
log_entry
|
||||
else
|
||||
"%s %s" % [message, dump]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,630 +0,0 @@
|
||||
require 'active_record/connection_adapters/abstract_adapter'
|
||||
require 'set'
|
||||
|
||||
module MysqlCompat #:nodoc:
|
||||
# add all_hashes method to standard mysql-c bindings or pure ruby version
|
||||
def self.define_all_hashes_method!
|
||||
raise 'Mysql not loaded' unless defined?(::Mysql)
|
||||
|
||||
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
|
||||
return if target.instance_methods.include?('all_hashes') ||
|
||||
target.instance_methods.include?(:all_hashes)
|
||||
|
||||
# Ruby driver has a version string and returns null values in each_hash
|
||||
# C driver >= 2.7 returns null values in each_hash
|
||||
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
|
||||
target.class_eval <<-'end_eval'
|
||||
def all_hashes # def all_hashes
|
||||
rows = [] # rows = []
|
||||
each_hash { |row| rows << row } # each_hash { |row| rows << row }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
# adapters before 2.7 don't have a version constant
|
||||
# and don't return null values in each_hash
|
||||
else
|
||||
target.class_eval <<-'end_eval'
|
||||
def all_hashes # def all_hashes
|
||||
rows = [] # rows = []
|
||||
all_fields = fetch_fields.inject({}) { |fields, f| # all_fields = fetch_fields.inject({}) { |fields, f|
|
||||
fields[f.name] = nil; fields # fields[f.name] = nil; fields
|
||||
} # }
|
||||
each_hash { |row| rows << all_fields.dup.update(row) } # each_hash { |row| rows << all_fields.dup.update(row) }
|
||||
rows # rows
|
||||
end # end
|
||||
end_eval
|
||||
end
|
||||
|
||||
unless target.instance_methods.include?('all_hashes') ||
|
||||
target.instance_methods.include?(:all_hashes)
|
||||
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
# Establishes a connection to the database that's used by all Active Record objects.
|
||||
def self.mysql_connection(config) # :nodoc:
|
||||
config = config.symbolize_keys
|
||||
host = config[:host]
|
||||
port = config[:port]
|
||||
socket = config[:socket]
|
||||
username = config[:username] ? config[:username].to_s : 'root'
|
||||
password = config[:password].to_s
|
||||
database = config[:database]
|
||||
|
||||
# Require the MySQL driver and define Mysql::Result.all_hashes
|
||||
unless defined? Mysql
|
||||
begin
|
||||
require_library_or_gem('mysql')
|
||||
rescue LoadError
|
||||
$stderr.puts '!!! The bundled mysql.rb driver has been removed from Rails 2.2. Please install the mysql gem and try again: gem install mysql.'
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
MysqlCompat.define_all_hashes_method!
|
||||
|
||||
mysql = Mysql.init
|
||||
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
||||
|
||||
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
|
||||
options = [host, username, password, database, port, socket, default_flags]
|
||||
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
class MysqlColumn < Column #:nodoc:
|
||||
def extract_default(default)
|
||||
if sql_type =~ /blob/i || type == :text
|
||||
if default.blank?
|
||||
return null ? nil : ''
|
||||
else
|
||||
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
|
||||
end
|
||||
elsif missing_default_forged_as_empty_string?(default)
|
||||
nil
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def has_default?
|
||||
return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def simplified_type(field_type)
|
||||
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
|
||||
return :string if field_type =~ /enum/i
|
||||
super
|
||||
end
|
||||
|
||||
def extract_limit(sql_type)
|
||||
case sql_type
|
||||
when /blob|text/i
|
||||
case sql_type
|
||||
when /tiny/i
|
||||
255
|
||||
when /medium/i
|
||||
16777215
|
||||
when /long/i
|
||||
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
|
||||
else
|
||||
super # we could return 65535 here, but we leave it undecorated by default
|
||||
end
|
||||
when /^bigint/i; 8
|
||||
when /^int/i; 4
|
||||
when /^mediumint/i; 3
|
||||
when /^smallint/i; 2
|
||||
when /^tinyint/i; 1
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# MySQL misreports NOT NULL column default when none is given.
|
||||
# We can't detect this for columns which may have a legitimate ''
|
||||
# default (string) but we can for others (integer, datetime, boolean,
|
||||
# and the rest).
|
||||
#
|
||||
# Test whether the column has default '', is not null, and is not
|
||||
# a type allowing default ''.
|
||||
def missing_default_forged_as_empty_string?(default)
|
||||
type != :string && !null && default == ''
|
||||
end
|
||||
end
|
||||
|
||||
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
|
||||
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
|
||||
#
|
||||
# Options:
|
||||
#
|
||||
# * <tt>:host</tt> - Defaults to "localhost".
|
||||
# * <tt>:port</tt> - Defaults to 3306.
|
||||
# * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
|
||||
# * <tt>:username</tt> - Defaults to "root"
|
||||
# * <tt>:password</tt> - Defaults to nothing.
|
||||
# * <tt>:database</tt> - The name of the database. No default, must be provided.
|
||||
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
|
||||
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
|
||||
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
|
||||
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
|
||||
#
|
||||
class MysqlAdapter < AbstractAdapter
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
|
||||
# as boolean. If you wish to disable this emulation (which was the default
|
||||
# behavior in versions 0.13.1 and earlier) you can add the following line
|
||||
# to your environment.rb file:
|
||||
#
|
||||
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
|
||||
cattr_accessor :emulate_booleans
|
||||
self.emulate_booleans = true
|
||||
|
||||
ADAPTER_NAME = 'MySQL'.freeze
|
||||
|
||||
LOST_CONNECTION_ERROR_MESSAGES = [
|
||||
"Server shutdown in progress",
|
||||
"Broken pipe",
|
||||
"Lost connection to MySQL server during query",
|
||||
"MySQL server has gone away" ]
|
||||
|
||||
QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "int", :limit => 4 },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "datetime" },
|
||||
:timestamp => { :name => "datetime" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "blob" },
|
||||
:boolean => { :name => "tinyint", :limit => 1 }
|
||||
}
|
||||
|
||||
def initialize(connection, logger, connection_options, config)
|
||||
super(connection, logger)
|
||||
@connection_options, @config = connection_options, config
|
||||
@quoted_column_names, @quoted_table_names = {}, {}
|
||||
connect
|
||||
end
|
||||
|
||||
def adapter_name #:nodoc:
|
||||
ADAPTER_NAME
|
||||
end
|
||||
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_primary_key? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
end
|
||||
|
||||
|
||||
# QUOTING ==================================================
|
||||
|
||||
def quote(value, column = nil)
|
||||
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
||||
s = column.class.string_to_binary(value).unpack("H*")[0]
|
||||
"x'#{s}'"
|
||||
elsif value.kind_of?(BigDecimal)
|
||||
value.to_s("F")
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def quote_column_name(name) #:nodoc:
|
||||
@quoted_column_names[name] ||= "`#{name}`"
|
||||
end
|
||||
|
||||
def quote_table_name(name) #:nodoc:
|
||||
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
|
||||
end
|
||||
|
||||
def quote_string(string) #:nodoc:
|
||||
@connection.quote(string)
|
||||
end
|
||||
|
||||
def quoted_true
|
||||
QUOTED_TRUE
|
||||
end
|
||||
|
||||
def quoted_false
|
||||
QUOTED_FALSE
|
||||
end
|
||||
|
||||
# REFERENTIAL INTEGRITY ====================================
|
||||
|
||||
def disable_referential_integrity(&block) #:nodoc:
|
||||
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
|
||||
|
||||
begin
|
||||
update("SET FOREIGN_KEY_CHECKS = 0")
|
||||
yield
|
||||
ensure
|
||||
update("SET FOREIGN_KEY_CHECKS = #{old}")
|
||||
end
|
||||
end
|
||||
|
||||
# CONNECTION MANAGEMENT ====================================
|
||||
|
||||
def active?
|
||||
if @connection.respond_to?(:stat)
|
||||
@connection.stat
|
||||
else
|
||||
@connection.query 'select 1'
|
||||
end
|
||||
|
||||
# mysql-ruby doesn't raise an exception when stat fails.
|
||||
if @connection.respond_to?(:errno)
|
||||
@connection.errno.zero?
|
||||
else
|
||||
true
|
||||
end
|
||||
rescue Mysql::Error
|
||||
false
|
||||
end
|
||||
|
||||
def reconnect!
|
||||
disconnect!
|
||||
connect
|
||||
end
|
||||
|
||||
def disconnect!
|
||||
@connection.close rescue nil
|
||||
end
|
||||
|
||||
def reset!
|
||||
if @connection.respond_to?(:change_user)
|
||||
# See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
|
||||
# reset the connection is to change the user to the same user.
|
||||
@connection.change_user(@config[:username], @config[:password], @config[:database])
|
||||
configure_connection
|
||||
end
|
||||
end
|
||||
|
||||
# DATABASE STATEMENTS ======================================
|
||||
|
||||
def select_rows(sql, name = nil)
|
||||
@connection.query_with_result = true
|
||||
result = execute(sql, name)
|
||||
rows = []
|
||||
result.each { |row| rows << row }
|
||||
result.free
|
||||
rows
|
||||
end
|
||||
|
||||
# Executes a SQL query and returns a MySQL::Result object. Note that you have to free the Result object after you're done using it.
|
||||
def execute(sql, name = nil) #:nodoc:
|
||||
log(sql, name) { @connection.query(sql) }
|
||||
rescue ActiveRecord::StatementInvalid => exception
|
||||
if exception.message.split(":").first =~ /Packets out of order/
|
||||
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
||||
super sql, name
|
||||
id_value || @connection.insert_id
|
||||
end
|
||||
|
||||
def update_sql(sql, name = nil) #:nodoc:
|
||||
super
|
||||
@connection.affected_rows
|
||||
end
|
||||
|
||||
def begin_db_transaction #:nodoc:
|
||||
execute "BEGIN"
|
||||
rescue Exception
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def commit_db_transaction #:nodoc:
|
||||
execute "COMMIT"
|
||||
rescue Exception
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def rollback_db_transaction #:nodoc:
|
||||
execute "ROLLBACK"
|
||||
rescue Exception
|
||||
# Transactions aren't supported
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def add_limit_offset!(sql, options) #:nodoc:
|
||||
if limit = options[:limit]
|
||||
limit = sanitize_limit(limit)
|
||||
unless offset = options[:offset]
|
||||
sql << " LIMIT #{limit}"
|
||||
else
|
||||
sql << " LIMIT #{offset.to_i}, #{limit}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
def structure_dump #:nodoc:
|
||||
if supports_views?
|
||||
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
|
||||
else
|
||||
sql = "SHOW TABLES"
|
||||
end
|
||||
|
||||
select_all(sql).inject("") do |structure, table|
|
||||
table.delete('Table_type')
|
||||
structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
|
||||
end
|
||||
end
|
||||
|
||||
def recreate_database(name, options = {}) #:nodoc:
|
||||
drop_database(name)
|
||||
create_database(name, options)
|
||||
end
|
||||
|
||||
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
|
||||
# Charset defaults to utf8.
|
||||
#
|
||||
# Example:
|
||||
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
|
||||
# create_database 'matt_development'
|
||||
# create_database 'matt_development', :charset => :big5
|
||||
def create_database(name, options = {})
|
||||
if options[:collation]
|
||||
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
|
||||
else
|
||||
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
|
||||
end
|
||||
end
|
||||
|
||||
def drop_database(name) #:nodoc:
|
||||
execute "DROP DATABASE IF EXISTS `#{name}`"
|
||||
end
|
||||
|
||||
def current_database
|
||||
select_value 'SELECT DATABASE() as db'
|
||||
end
|
||||
|
||||
# Returns the database character set.
|
||||
def charset
|
||||
show_variable 'character_set_database'
|
||||
end
|
||||
|
||||
# Returns the database collation strategy.
|
||||
def collation
|
||||
show_variable 'collation_database'
|
||||
end
|
||||
|
||||
def tables(name = nil) #:nodoc:
|
||||
tables = []
|
||||
result = execute("SHOW TABLES", name)
|
||||
result.each { |field| tables << field[0] }
|
||||
result.free
|
||||
tables
|
||||
end
|
||||
|
||||
def drop_table(table_name, options = {})
|
||||
super(table_name, options)
|
||||
end
|
||||
|
||||
def indexes(table_name, name = nil)#:nodoc:
|
||||
indexes = []
|
||||
current_index = nil
|
||||
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
|
||||
result.each do |row|
|
||||
if current_index != row[2]
|
||||
next if row[2] == "PRIMARY" # skip the primary key
|
||||
current_index = row[2]
|
||||
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
||||
end
|
||||
|
||||
indexes.last.columns << row[4]
|
||||
end
|
||||
result.free
|
||||
indexes
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil)#:nodoc:
|
||||
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
|
||||
columns = []
|
||||
result = execute(sql, name)
|
||||
result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
||||
result.free
|
||||
columns
|
||||
end
|
||||
|
||||
def create_table(table_name, options = {}) #:nodoc:
|
||||
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
|
||||
end
|
||||
|
||||
def rename_table(table_name, new_name)
|
||||
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
||||
end
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
change_column table_name, column_name, column.sql_type, :default => default
|
||||
end
|
||||
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
unless null || default.nil?
|
||||
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
||||
end
|
||||
|
||||
change_column table_name, column_name, column.sql_type, :null => null
|
||||
end
|
||||
|
||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
unless options_include_default?(options)
|
||||
options[:default] = column.default
|
||||
end
|
||||
|
||||
unless options.has_key?(:null)
|
||||
options[:null] = column.null
|
||||
end
|
||||
|
||||
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(change_column_sql, options)
|
||||
execute(change_column_sql)
|
||||
end
|
||||
|
||||
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
||||
options = {}
|
||||
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
options[:default] = column.default
|
||||
options[:null] = column.null
|
||||
else
|
||||
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
||||
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
||||
add_column_options!(rename_column_sql, options)
|
||||
execute(rename_column_sql)
|
||||
end
|
||||
|
||||
# Maps logical Rails types to MySQL-specific data types.
|
||||
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
||||
return super unless type.to_s == 'integer'
|
||||
|
||||
case limit
|
||||
when 1; 'tinyint'
|
||||
when 2; 'smallint'
|
||||
when 3; 'mediumint'
|
||||
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
||||
when 5..8; 'bigint'
|
||||
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# SHOW VARIABLES LIKE 'name'
|
||||
def show_variable(name)
|
||||
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
|
||||
variables.first['Value'] unless variables.empty?
|
||||
end
|
||||
|
||||
# Returns a table's primary key and belonging sequence.
|
||||
def pk_and_sequence_for(table) #:nodoc:
|
||||
keys = []
|
||||
result = execute("describe #{quote_table_name(table)}")
|
||||
result.each_hash do |h|
|
||||
keys << h["Field"]if h["Key"] == "PRI"
|
||||
end
|
||||
result.free
|
||||
keys.length == 1 ? [keys.first, nil] : nil
|
||||
end
|
||||
|
||||
# Returns just a table's primary key
|
||||
def primary_key(table)
|
||||
pk_and_sequence = pk_and_sequence_for(table)
|
||||
pk_and_sequence && pk_and_sequence.first
|
||||
end
|
||||
|
||||
def case_sensitive_equality_operator
|
||||
"= BINARY"
|
||||
end
|
||||
|
||||
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
||||
where_sql
|
||||
end
|
||||
|
||||
private
|
||||
def connect
|
||||
encoding = @config[:encoding]
|
||||
if encoding
|
||||
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
||||
end
|
||||
|
||||
if @config[:sslca] || @config[:sslkey]
|
||||
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
|
||||
end
|
||||
|
||||
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
|
||||
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
|
||||
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
|
||||
|
||||
@connection.real_connect(*@connection_options)
|
||||
|
||||
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
|
||||
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
|
||||
|
||||
configure_connection
|
||||
end
|
||||
|
||||
def configure_connection
|
||||
encoding = @config[:encoding]
|
||||
execute("SET NAMES '#{encoding}'") if encoding
|
||||
|
||||
# By default, MySQL 'where id is null' selects the last inserted id.
|
||||
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
||||
execute("SET SQL_AUTO_IS_NULL=0")
|
||||
end
|
||||
|
||||
def select(sql, name = nil)
|
||||
@connection.query_with_result = true
|
||||
result = execute(sql, name)
|
||||
rows = result.all_hashes
|
||||
result.free
|
||||
rows
|
||||
end
|
||||
|
||||
def supports_views?
|
||||
version[0] >= 5
|
||||
end
|
||||
|
||||
def version
|
||||
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
||||
end
|
||||
|
||||
def column_for(table_name, column_name)
|
||||
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
raise "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
column
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,1113 +0,0 @@
|
||||
require 'active_record/connection_adapters/abstract_adapter'
|
||||
|
||||
begin
|
||||
require_library_or_gem 'pg'
|
||||
rescue LoadError => e
|
||||
begin
|
||||
require_library_or_gem 'postgres'
|
||||
class PGresult
|
||||
alias_method :nfields, :num_fields unless self.method_defined?(:nfields)
|
||||
alias_method :ntuples, :num_tuples unless self.method_defined?(:ntuples)
|
||||
alias_method :ftype, :type unless self.method_defined?(:ftype)
|
||||
alias_method :cmd_tuples, :cmdtuples unless self.method_defined?(:cmd_tuples)
|
||||
end
|
||||
rescue LoadError
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
# Establishes a connection to the database that's used by all Active Record objects
|
||||
def self.postgresql_connection(config) # :nodoc:
|
||||
config = config.symbolize_keys
|
||||
host = config[:host]
|
||||
port = config[:port] || 5432
|
||||
username = config[:username].to_s if config[:username]
|
||||
password = config[:password].to_s if config[:password]
|
||||
|
||||
if config.has_key?(:database)
|
||||
database = config[:database]
|
||||
else
|
||||
raise ArgumentError, "No database specified. Missing argument: database."
|
||||
end
|
||||
|
||||
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
||||
# so just pass a nil connection object for the time being.
|
||||
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
class TableDefinition
|
||||
def xml(*args)
|
||||
options = args.extract_options!
|
||||
column(args[0], 'xml', options)
|
||||
end
|
||||
end
|
||||
# PostgreSQL-specific extensions to column definitions in a table.
|
||||
class PostgreSQLColumn < Column #:nodoc:
|
||||
# Instantiates a new PostgreSQL column definition in a table.
|
||||
def initialize(name, default, sql_type = nil, null = true)
|
||||
super(name, self.class.extract_value_from_default(default), sql_type, null)
|
||||
end
|
||||
|
||||
private
|
||||
def extract_limit(sql_type)
|
||||
case sql_type
|
||||
when /^bigint/i; 8
|
||||
when /^smallint/i; 2
|
||||
else super
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts the scale from PostgreSQL-specific data types.
|
||||
def extract_scale(sql_type)
|
||||
# Money type has a fixed scale of 2.
|
||||
sql_type =~ /^money/ ? 2 : super
|
||||
end
|
||||
|
||||
# Extracts the precision from PostgreSQL-specific data types.
|
||||
def extract_precision(sql_type)
|
||||
# Actual code is defined dynamically in PostgreSQLAdapter.connect
|
||||
# depending on the server specifics
|
||||
super
|
||||
end
|
||||
|
||||
# Maps PostgreSQL-specific data types to logical Rails types.
|
||||
def simplified_type(field_type)
|
||||
case field_type
|
||||
# Numeric and monetary types
|
||||
when /^(?:real|double precision)$/
|
||||
:float
|
||||
# Monetary types
|
||||
when /^money$/
|
||||
:decimal
|
||||
# Character types
|
||||
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
|
||||
:string
|
||||
# Binary data types
|
||||
when /^bytea$/
|
||||
:binary
|
||||
# Date/time types
|
||||
when /^timestamp with(?:out)? time zone$/
|
||||
:datetime
|
||||
when /^interval$/
|
||||
:string
|
||||
# Geometric types
|
||||
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
|
||||
:string
|
||||
# Network address types
|
||||
when /^(?:cidr|inet|macaddr)$/
|
||||
:string
|
||||
# Bit strings
|
||||
when /^bit(?: varying)?(?:\(\d+\))?$/
|
||||
:string
|
||||
# XML type
|
||||
when /^xml$/
|
||||
:xml
|
||||
# Arrays
|
||||
when /^\D+\[\]$/
|
||||
:string
|
||||
# Object identifier types
|
||||
when /^oid$/
|
||||
:integer
|
||||
# Pass through all types that are not specific to PostgreSQL.
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts the value from a PostgreSQL column default definition.
|
||||
def self.extract_value_from_default(default)
|
||||
case default
|
||||
# Numeric types
|
||||
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
|
||||
$1
|
||||
# Character types
|
||||
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
|
||||
$1
|
||||
# Character types (8.1 formatting)
|
||||
when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
|
||||
$1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
|
||||
# Binary data types
|
||||
when /\A'(.*)'::bytea\z/m
|
||||
$1
|
||||
# Date/time types
|
||||
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
|
||||
$1
|
||||
when /\A'(.*)'::interval\z/
|
||||
$1
|
||||
# Boolean type
|
||||
when 'true'
|
||||
true
|
||||
when 'false'
|
||||
false
|
||||
# Geometric types
|
||||
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
|
||||
$1
|
||||
# Network address types
|
||||
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
|
||||
$1
|
||||
# Bit string types
|
||||
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
|
||||
$1
|
||||
# XML type
|
||||
when /\A'(.*)'::xml\z/m
|
||||
$1
|
||||
# Arrays
|
||||
when /\A'(.*)'::"?\D+"?\[\]\z/
|
||||
$1
|
||||
# Object identifier types
|
||||
when /\A-?\d+\z/
|
||||
$1
|
||||
else
|
||||
# Anything else is blank, some user type, or some function
|
||||
# and we can't know the value of that, so return nil.
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters
|
||||
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
|
||||
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
|
||||
#
|
||||
# Options:
|
||||
#
|
||||
# * <tt>:host</tt> - Defaults to "localhost".
|
||||
# * <tt>:port</tt> - Defaults to 5432.
|
||||
# * <tt>:username</tt> - Defaults to nothing.
|
||||
# * <tt>:password</tt> - Defaults to nothing.
|
||||
# * <tt>:database</tt> - The name of the database. No default, must be provided.
|
||||
# * <tt>:schema_search_path</tt> - An optional schema search path for the connection given as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
|
||||
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO <encoding></tt> call on the connection.
|
||||
# * <tt>:min_messages</tt> - An optional client min messages that is used in a <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
|
||||
# * <tt>:allow_concurrency</tt> - If true, use async query methods so Ruby threads don't deadlock; otherwise, use blocking query methods.
|
||||
class PostgreSQLAdapter < AbstractAdapter
|
||||
ADAPTER_NAME = 'PostgreSQL'.freeze
|
||||
|
||||
NATIVE_DATABASE_TYPES = {
|
||||
:primary_key => "serial primary key".freeze,
|
||||
:string => { :name => "character varying", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "integer" },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "timestamp" },
|
||||
:timestamp => { :name => "timestamp" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "bytea" },
|
||||
:boolean => { :name => "boolean" },
|
||||
:xml => { :name => "xml" }
|
||||
}
|
||||
|
||||
# Returns 'PostgreSQL' as adapter name for identification purposes.
|
||||
def adapter_name
|
||||
ADAPTER_NAME
|
||||
end
|
||||
|
||||
# Initializes and connects a PostgreSQL adapter.
|
||||
def initialize(connection, logger, connection_parameters, config)
|
||||
super(connection, logger)
|
||||
@connection_parameters, @config = connection_parameters, config
|
||||
|
||||
connect
|
||||
end
|
||||
|
||||
# Is this connection alive and ready for queries?
|
||||
def active?
|
||||
if @connection.respond_to?(:status)
|
||||
@connection.status == PGconn::CONNECTION_OK
|
||||
else
|
||||
# We're asking the driver, not ActiveRecord, so use @connection.query instead of #query
|
||||
@connection.query 'SELECT 1'
|
||||
true
|
||||
end
|
||||
# postgres-pr raises a NoMethodError when querying if no connection is available.
|
||||
rescue PGError, NoMethodError
|
||||
false
|
||||
end
|
||||
|
||||
# Close then reopen the connection.
|
||||
def reconnect!
|
||||
if @connection.respond_to?(:reset)
|
||||
@connection.reset
|
||||
configure_connection
|
||||
else
|
||||
disconnect!
|
||||
connect
|
||||
end
|
||||
end
|
||||
|
||||
# Close the connection.
|
||||
def disconnect!
|
||||
@connection.close rescue nil
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
NATIVE_DATABASE_TYPES
|
||||
end
|
||||
|
||||
# Does PostgreSQL support migrations?
|
||||
def supports_migrations?
|
||||
true
|
||||
end
|
||||
|
||||
# Does PostgreSQL support finding primary key on non-ActiveRecord tables?
|
||||
def supports_primary_key? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
# Does PostgreSQL support standard conforming strings?
|
||||
def supports_standard_conforming_strings?
|
||||
# Temporarily set the client message level above error to prevent unintentional
|
||||
# error messages in the logs when working on a PostgreSQL database server that
|
||||
# does not support standard conforming strings.
|
||||
client_min_messages_old = client_min_messages
|
||||
self.client_min_messages = 'panic'
|
||||
|
||||
# postgres-pr does not raise an exception when client_min_messages is set higher
|
||||
# than error and "SHOW standard_conforming_strings" fails, but returns an empty
|
||||
# PGresult instead.
|
||||
has_support = query('SHOW standard_conforming_strings')[0][0] rescue false
|
||||
self.client_min_messages = client_min_messages_old
|
||||
has_support
|
||||
end
|
||||
|
||||
def supports_insert_with_returning?
|
||||
postgresql_version >= 80200
|
||||
end
|
||||
|
||||
def supports_ddl_transactions?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_savepoints?
|
||||
true
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL,
|
||||
# or report the default of 63 on PostgreSQL 7.x.
|
||||
def table_alias_length
|
||||
@table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63)
|
||||
end
|
||||
|
||||
# QUOTING ==================================================
|
||||
|
||||
# Escapes binary strings for bytea input to the database.
|
||||
def escape_bytea(value)
|
||||
if @connection.respond_to?(:escape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:escape_bytea) do |value|
|
||||
@connection.escape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
elsif PGconn.respond_to?(:escape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:escape_bytea) do |value|
|
||||
PGconn.escape_bytea(value) if value
|
||||
end
|
||||
end
|
||||
else
|
||||
self.class.instance_eval do
|
||||
define_method(:escape_bytea) do |value|
|
||||
if value
|
||||
result = ''
|
||||
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
escape_bytea(value)
|
||||
end
|
||||
|
||||
# Unescapes bytea output from a database to the binary string it represents.
|
||||
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
|
||||
# on escaped binary output from database drive.
|
||||
def unescape_bytea(value)
|
||||
# In each case, check if the value actually is escaped PostgreSQL bytea output
|
||||
# or an unescaped Active Record attribute that was just written.
|
||||
if PGconn.respond_to?(:unescape_bytea)
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
PGconn.unescape_bytea(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
self.class.instance_eval do
|
||||
define_method(:unescape_bytea) do |value|
|
||||
if value =~ /\\\d{3}/
|
||||
result = ''
|
||||
i, max = 0, value.size
|
||||
while i < max
|
||||
char = value[i]
|
||||
if char == ?\\
|
||||
if value[i+1] == ?\\
|
||||
char = ?\\
|
||||
i += 1
|
||||
else
|
||||
char = value[i+1..i+3].oct
|
||||
i += 3
|
||||
end
|
||||
end
|
||||
result << char
|
||||
i += 1
|
||||
end
|
||||
result
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
unescape_bytea(value)
|
||||
end
|
||||
|
||||
# Quotes PostgreSQL-specific data types for SQL input.
|
||||
def quote(value, column = nil) #:nodoc:
|
||||
if value.kind_of?(String) && column && column.type == :binary
|
||||
"#{quoted_string_prefix}'#{escape_bytea(value)}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^xml$/
|
||||
"xml E'#{quote_string(value)}'"
|
||||
elsif value.kind_of?(Numeric) && column && column.sql_type =~ /^money$/
|
||||
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
||||
"'#{value.to_s}'"
|
||||
elsif value.kind_of?(String) && column && column.sql_type =~ /^bit/
|
||||
case value
|
||||
when /^[01]*$/
|
||||
"B'#{value}'" # Bit-string notation
|
||||
when /^[0-9A-F]*$/i
|
||||
"X'#{value}'" # Hexadecimal notation
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Quotes strings for use in SQL input in the postgres driver for better performance.
|
||||
def quote_string(s) #:nodoc:
|
||||
if @connection.respond_to?(:escape)
|
||||
self.class.instance_eval do
|
||||
define_method(:quote_string) do |s|
|
||||
@connection.escape(s)
|
||||
end
|
||||
end
|
||||
elsif PGconn.respond_to?(:escape)
|
||||
self.class.instance_eval do
|
||||
define_method(:quote_string) do |s|
|
||||
PGconn.escape(s)
|
||||
end
|
||||
end
|
||||
else
|
||||
# There are some incorrectly compiled postgres drivers out there
|
||||
# that don't define PGconn.escape.
|
||||
self.class.instance_eval do
|
||||
remove_method(:quote_string)
|
||||
end
|
||||
end
|
||||
quote_string(s)
|
||||
end
|
||||
|
||||
# Checks the following cases:
|
||||
#
|
||||
# - table_name
|
||||
# - "table.name"
|
||||
# - schema_name.table_name
|
||||
# - schema_name."table.name"
|
||||
# - "schema.name".table_name
|
||||
# - "schema.name"."table.name"
|
||||
def quote_table_name(name)
|
||||
schema, name_part = extract_pg_identifier_from_name(name.to_s)
|
||||
|
||||
unless name_part
|
||||
quote_column_name(schema)
|
||||
else
|
||||
table_name, name_part = extract_pg_identifier_from_name(name_part)
|
||||
"#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
|
||||
end
|
||||
end
|
||||
|
||||
# Quotes column names for use in SQL queries.
|
||||
def quote_column_name(name) #:nodoc:
|
||||
PGconn.quote_ident(name.to_s)
|
||||
end
|
||||
|
||||
# Quote date/time values for use in SQL input. Includes microseconds
|
||||
# if the value is a Time responding to usec.
|
||||
def quoted_date(value) #:nodoc:
|
||||
if value.acts_like?(:time) && value.respond_to?(:usec)
|
||||
"#{super}.#{sprintf("%06d", value.usec)}"
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# REFERENTIAL INTEGRITY ====================================
|
||||
|
||||
def supports_disable_referential_integrity?() #:nodoc:
|
||||
version = query("SHOW server_version")[0][0].split('.')
|
||||
(version[0].to_i >= 8 && version[1].to_i >= 1) ? true : false
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
|
||||
def disable_referential_integrity(&block) #:nodoc:
|
||||
if supports_disable_referential_integrity?() then
|
||||
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
if supports_disable_referential_integrity?() then
|
||||
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
|
||||
end
|
||||
end
|
||||
|
||||
# DATABASE STATEMENTS ======================================
|
||||
|
||||
# Executes a SELECT query and returns an array of rows. Each row is an
|
||||
# array of field values.
|
||||
def select_rows(sql, name = nil)
|
||||
select_raw(sql, name).last
|
||||
end
|
||||
|
||||
# Executes an INSERT query and returns the new record's ID
|
||||
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
||||
# Extract the table from the insert sql. Yuck.
|
||||
table = sql.split(" ", 4)[2].gsub('"', '')
|
||||
|
||||
# Try an insert with 'returning id' if available (PG >= 8.2)
|
||||
if supports_insert_with_returning?
|
||||
pk, sequence_name = *pk_and_sequence_for(table) unless pk
|
||||
if pk
|
||||
id = select_value("#{sql} RETURNING #{quote_column_name(pk)}")
|
||||
clear_query_cache
|
||||
return id
|
||||
end
|
||||
end
|
||||
|
||||
# Otherwise, insert then grab last_insert_id.
|
||||
if insert_id = super
|
||||
insert_id
|
||||
else
|
||||
# If neither pk nor sequence name is given, look them up.
|
||||
unless pk || sequence_name
|
||||
pk, sequence_name = *pk_and_sequence_for(table)
|
||||
end
|
||||
|
||||
# If a pk is given, fallback to default sequence name.
|
||||
# Don't fetch last insert id for a table without a pk.
|
||||
if pk && sequence_name ||= default_sequence_name(table, pk)
|
||||
last_insert_id(table, sequence_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# create a 2D array representing the result set
|
||||
def result_as_array(res) #:nodoc:
|
||||
# check if we have any binary column and if they need escaping
|
||||
unescape_col = []
|
||||
for j in 0...res.nfields do
|
||||
# unescape string passed BYTEA field (OID == 17)
|
||||
unescape_col << ( res.ftype(j)==17 )
|
||||
end
|
||||
|
||||
ary = []
|
||||
for i in 0...res.ntuples do
|
||||
ary << []
|
||||
for j in 0...res.nfields do
|
||||
data = res.getvalue(i,j)
|
||||
data = unescape_bytea(data) if unescape_col[j] and data.is_a?(String)
|
||||
ary[i] << data
|
||||
end
|
||||
end
|
||||
return ary
|
||||
end
|
||||
|
||||
|
||||
# Queries the database and returns the results in an Array-like object
|
||||
def query(sql, name = nil) #:nodoc:
|
||||
log(sql, name) do
|
||||
if @async
|
||||
res = @connection.async_exec(sql)
|
||||
else
|
||||
res = @connection.exec(sql)
|
||||
end
|
||||
return result_as_array(res)
|
||||
end
|
||||
end
|
||||
|
||||
# Executes an SQL statement, returning a PGresult object on success
|
||||
# or raising a PGError exception otherwise.
|
||||
def execute(sql, name = nil)
|
||||
log(sql, name) do
|
||||
if @async
|
||||
@connection.async_exec(sql)
|
||||
else
|
||||
@connection.exec(sql)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes an UPDATE query and returns the number of affected tuples.
|
||||
def update_sql(sql, name = nil)
|
||||
super.cmd_tuples
|
||||
end
|
||||
|
||||
# Begins a transaction.
|
||||
def begin_db_transaction
|
||||
execute "BEGIN"
|
||||
end
|
||||
|
||||
# Commits a transaction.
|
||||
def commit_db_transaction
|
||||
execute "COMMIT"
|
||||
end
|
||||
|
||||
# Aborts a transaction.
|
||||
def rollback_db_transaction
|
||||
execute "ROLLBACK"
|
||||
end
|
||||
|
||||
if defined?(PGconn::PQTRANS_IDLE)
|
||||
# The ruby-pg driver supports inspecting the transaction status,
|
||||
# while the ruby-postgres driver does not.
|
||||
def outside_transaction?
|
||||
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
||||
end
|
||||
end
|
||||
|
||||
def create_savepoint
|
||||
execute("SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def rollback_to_savepoint
|
||||
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
def release_savepoint
|
||||
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
||||
end
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
def recreate_database(name) #:nodoc:
|
||||
drop_database(name)
|
||||
create_database(name)
|
||||
end
|
||||
|
||||
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
|
||||
# <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
|
||||
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
|
||||
#
|
||||
# Example:
|
||||
# create_database config[:database], config
|
||||
# create_database 'foo_development', :encoding => 'unicode'
|
||||
def create_database(name, options = {})
|
||||
options = options.reverse_merge(:encoding => "utf8")
|
||||
|
||||
option_string = options.symbolize_keys.sum do |key, value|
|
||||
case key
|
||||
when :owner
|
||||
" OWNER = \"#{value}\""
|
||||
when :template
|
||||
" TEMPLATE = \"#{value}\""
|
||||
when :encoding
|
||||
" ENCODING = '#{value}'"
|
||||
when :tablespace
|
||||
" TABLESPACE = \"#{value}\""
|
||||
when :connection_limit
|
||||
" CONNECTION LIMIT = #{value}"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
|
||||
end
|
||||
|
||||
# Drops a PostgreSQL database
|
||||
#
|
||||
# Example:
|
||||
# drop_database 'matt_development'
|
||||
def drop_database(name) #:nodoc:
|
||||
if postgresql_version >= 80200
|
||||
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
|
||||
else
|
||||
begin
|
||||
execute "DROP DATABASE #{quote_table_name(name)}"
|
||||
rescue ActiveRecord::StatementInvalid
|
||||
@logger.warn "#{name} database doesn't exist." if @logger
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns the list of all tables in the schema search path or a specified schema.
|
||||
def tables(name = nil)
|
||||
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
||||
query(<<-SQL, name).map { |row| row[0] }
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname IN (#{schemas})
|
||||
SQL
|
||||
end
|
||||
|
||||
# Returns the list of all indexes for a table.
|
||||
def indexes(table_name, name = nil)
|
||||
schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
|
||||
result = query(<<-SQL, name)
|
||||
SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
|
||||
FROM pg_class t, pg_class i, pg_index d
|
||||
WHERE i.relkind = 'i'
|
||||
AND d.indexrelid = i.oid
|
||||
AND d.indisprimary = 'f'
|
||||
AND t.oid = d.indrelid
|
||||
AND t.relname = '#{table_name}'
|
||||
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
|
||||
ORDER BY i.relname
|
||||
SQL
|
||||
|
||||
|
||||
indexes = []
|
||||
|
||||
indexes = result.map do |row|
|
||||
index_name = row[0]
|
||||
unique = row[1] == 't'
|
||||
indkey = row[2].split(" ")
|
||||
oid = row[3]
|
||||
|
||||
columns = query(<<-SQL, "Columns for index #{row[0]} on #{table_name}").inject({}) {|attlist, r| attlist[r[1]] = r[0]; attlist}
|
||||
SELECT a.attname, a.attnum
|
||||
FROM pg_attribute a
|
||||
WHERE a.attrelid = #{oid}
|
||||
AND a.attnum IN (#{indkey.join(",")})
|
||||
SQL
|
||||
|
||||
column_names = indkey.map {|attnum| columns[attnum] }
|
||||
IndexDefinition.new(table_name, index_name, unique, column_names)
|
||||
|
||||
end
|
||||
|
||||
indexes
|
||||
end
|
||||
|
||||
# Returns the list of all column definitions for a table.
|
||||
def columns(table_name, name = nil)
|
||||
# Limit, precision, and scale are all handled by the superclass.
|
||||
column_definitions(table_name).collect do |name, type, default, notnull|
|
||||
PostgreSQLColumn.new(name, default, type, notnull == 'f')
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the current database name.
|
||||
def current_database
|
||||
query('select current_database()')[0][0]
|
||||
end
|
||||
|
||||
# Returns the current database encoding format.
|
||||
def encoding
|
||||
query(<<-end_sql)[0][0]
|
||||
SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
|
||||
WHERE pg_database.datname LIKE '#{current_database}'
|
||||
end_sql
|
||||
end
|
||||
|
||||
# Sets the schema search path to a string of comma-separated schema names.
|
||||
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
||||
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
||||
#
|
||||
# This should be not be called manually but set in database.yml.
|
||||
def schema_search_path=(schema_csv)
|
||||
if schema_csv
|
||||
execute "SET search_path TO #{schema_csv}"
|
||||
@schema_search_path = schema_csv
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the active schema search path.
|
||||
def schema_search_path
|
||||
@schema_search_path ||= query('SHOW search_path')[0][0]
|
||||
end
|
||||
|
||||
# Returns the current client message level.
|
||||
def client_min_messages
|
||||
query('SHOW client_min_messages')[0][0]
|
||||
end
|
||||
|
||||
# Set the client message level.
|
||||
def client_min_messages=(level)
|
||||
execute("SET client_min_messages TO '#{level}'")
|
||||
end
|
||||
|
||||
# Returns the sequence name for a table's primary key or some other specified key.
|
||||
def default_sequence_name(table_name, pk = nil) #:nodoc:
|
||||
default_pk, default_seq = pk_and_sequence_for(table_name)
|
||||
default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq"
|
||||
end
|
||||
|
||||
# Resets the sequence of a table's primary key to the maximum value.
|
||||
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
||||
unless pk and sequence
|
||||
default_pk, default_sequence = pk_and_sequence_for(table)
|
||||
pk ||= default_pk
|
||||
sequence ||= default_sequence
|
||||
end
|
||||
if pk
|
||||
if sequence
|
||||
quoted_sequence = quote_column_name(sequence)
|
||||
|
||||
select_value <<-end_sql, 'Reset sequence'
|
||||
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
|
||||
end_sql
|
||||
else
|
||||
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a table's primary key and belonging sequence.
|
||||
def pk_and_sequence_for(table) #:nodoc:
|
||||
# First try looking for a sequence with a dependency on the
|
||||
# given table's primary key.
|
||||
result = query(<<-end_sql, 'PK and serial sequence')[0]
|
||||
SELECT attr.attname, seq.relname
|
||||
FROM pg_class seq,
|
||||
pg_attribute attr,
|
||||
pg_depend dep,
|
||||
pg_namespace name,
|
||||
pg_constraint cons
|
||||
WHERE seq.oid = dep.objid
|
||||
AND seq.relkind = 'S'
|
||||
AND attr.attrelid = dep.refobjid
|
||||
AND attr.attnum = dep.refobjsubid
|
||||
AND attr.attrelid = cons.conrelid
|
||||
AND attr.attnum = cons.conkey[1]
|
||||
AND cons.contype = 'p'
|
||||
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
|
||||
end_sql
|
||||
|
||||
if result.nil? or result.empty?
|
||||
# If that fails, try parsing the primary key's default value.
|
||||
# Support the 7.x and 8.0 nextval('foo'::text) as well as
|
||||
# the 8.1+ nextval('foo'::regclass).
|
||||
result = query(<<-end_sql, 'PK and custom sequence')[0]
|
||||
SELECT attr.attname,
|
||||
CASE
|
||||
WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
|
||||
substr(split_part(def.adsrc, '''', 2),
|
||||
strpos(split_part(def.adsrc, '''', 2), '.')+1)
|
||||
ELSE split_part(def.adsrc, '''', 2)
|
||||
END
|
||||
FROM pg_class t
|
||||
JOIN pg_attribute attr ON (t.oid = attrelid)
|
||||
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
|
||||
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
|
||||
WHERE t.oid = '#{quote_table_name(table)}'::regclass
|
||||
AND cons.contype = 'p'
|
||||
AND def.adsrc ~* 'nextval'
|
||||
end_sql
|
||||
end
|
||||
|
||||
# [primary_key, sequence]
|
||||
[result.first, result.last]
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
# Returns just a table's primary key
|
||||
def primary_key(table)
|
||||
pk_and_sequence = pk_and_sequence_for(table)
|
||||
pk_and_sequence && pk_and_sequence.first
|
||||
end
|
||||
|
||||
# Renames a table.
|
||||
def rename_table(name, new_name)
|
||||
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
|
||||
end
|
||||
|
||||
# Adds a new column to the named table.
|
||||
# See TableDefinition#column for details of the options you can use.
|
||||
def add_column(table_name, column_name, type, options = {})
|
||||
default = options[:default]
|
||||
notnull = options[:null] == false
|
||||
|
||||
# Add the column.
|
||||
execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
|
||||
|
||||
change_column_default(table_name, column_name, default) if options_include_default?(options)
|
||||
change_column_null(table_name, column_name, false, default) if notnull
|
||||
end
|
||||
|
||||
# Changes the column of a table.
|
||||
def change_column(table_name, column_name, type, options = {})
|
||||
quoted_table_name = quote_table_name(table_name)
|
||||
|
||||
begin
|
||||
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
rescue ActiveRecord::StatementInvalid => e
|
||||
raise e if postgresql_version > 80000
|
||||
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
|
||||
begin
|
||||
begin_db_transaction
|
||||
tmp_column_name = "#{column_name}_ar_tmp"
|
||||
add_column(table_name, tmp_column_name, type, options)
|
||||
execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
|
||||
remove_column(table_name, column_name)
|
||||
rename_column(table_name, tmp_column_name, column_name)
|
||||
commit_db_transaction
|
||||
rescue
|
||||
rollback_db_transaction
|
||||
end
|
||||
end
|
||||
|
||||
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
|
||||
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
|
||||
end
|
||||
|
||||
# Changes the default value of a table column.
|
||||
def change_column_default(table_name, column_name, default)
|
||||
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
|
||||
end
|
||||
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
unless null || default.nil?
|
||||
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
||||
end
|
||||
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
|
||||
end
|
||||
|
||||
# Renames a column in a table.
|
||||
def rename_column(table_name, column_name, new_column_name)
|
||||
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
||||
end
|
||||
|
||||
# Drops an index from a table.
|
||||
def remove_index(table_name, options = {})
|
||||
execute "DROP INDEX #{quote_table_name(index_name(table_name, options))}"
|
||||
end
|
||||
|
||||
# Maps logical Rails types to PostgreSQL-specific data types.
|
||||
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
||||
return super unless type.to_s == 'integer'
|
||||
|
||||
case limit
|
||||
when 1..2; 'smallint'
|
||||
when 3..4, nil; 'integer'
|
||||
when 5..8; 'bigint'
|
||||
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
||||
#
|
||||
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
|
||||
# requires that the ORDER BY include the distinct column.
|
||||
#
|
||||
# distinct("posts.id", "posts.created_at desc")
|
||||
def distinct(columns, order_by) #:nodoc:
|
||||
return "DISTINCT #{columns}" if order_by.blank?
|
||||
|
||||
# Construct a clean list of column names from the ORDER BY clause, removing
|
||||
# any ASC/DESC modifiers
|
||||
order_columns = order_by.split(',').collect { |s| s.split.first }
|
||||
order_columns.delete_if &:blank?
|
||||
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
|
||||
|
||||
# Return a DISTINCT ON() clause that's distinct on the columns we want but includes
|
||||
# all the required columns for the ORDER BY to work properly.
|
||||
sql = "DISTINCT ON (#{columns}) #{columns}, "
|
||||
sql << order_columns * ', '
|
||||
end
|
||||
|
||||
# Returns an ORDER BY clause for the passed order option.
|
||||
#
|
||||
# PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this
|
||||
# by wrapping the +sql+ string as a sub-select and ordering in that query.
|
||||
def add_order_by_for_association_limiting!(sql, options) #:nodoc:
|
||||
return sql if options[:order].blank?
|
||||
|
||||
order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
|
||||
order.map! { |s| 'DESC' if s =~ /\bdesc$/i }
|
||||
order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ')
|
||||
|
||||
sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}"
|
||||
end
|
||||
|
||||
protected
|
||||
# Returns the version of the connected PostgreSQL version.
|
||||
def postgresql_version
|
||||
@postgresql_version ||=
|
||||
if @connection.respond_to?(:server_version)
|
||||
@connection.server_version
|
||||
else
|
||||
# Mimic PGconn.server_version behavior
|
||||
begin
|
||||
query('SELECT version()')[0][0] =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/
|
||||
($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i
|
||||
rescue
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# The internal PostgreSQL identifier of the money data type.
|
||||
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
|
||||
|
||||
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
||||
# connected server's characteristics.
|
||||
def connect
|
||||
@connection = PGconn.connect(*@connection_parameters)
|
||||
PGconn.translate_results = false if PGconn.respond_to?(:translate_results=)
|
||||
|
||||
# Ignore async_exec and async_query when using postgres-pr.
|
||||
@async = @config[:allow_concurrency] && @connection.respond_to?(:async_exec)
|
||||
|
||||
# Use escape string syntax if available. We cannot do this lazily when encountering
|
||||
# the first string, because that could then break any transactions in progress.
|
||||
# See: http://www.postgresql.org/docs/current/static/runtime-config-compatible.html
|
||||
# If PostgreSQL doesn't know the standard_conforming_strings parameter then it doesn't
|
||||
# support escape string syntax. Don't override the inherited quoted_string_prefix.
|
||||
if supports_standard_conforming_strings?
|
||||
self.class.instance_eval do
|
||||
define_method(:quoted_string_prefix) { 'E' }
|
||||
end
|
||||
end
|
||||
|
||||
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
||||
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
||||
# should know about this but can't detect it there, so deal with it here.
|
||||
money_precision = (postgresql_version >= 80300) ? 19 : 10
|
||||
PostgreSQLColumn.module_eval(<<-end_eval)
|
||||
def extract_precision(sql_type) # def extract_precision(sql_type)
|
||||
if sql_type =~ /^money$/ # if sql_type =~ /^money$/
|
||||
#{money_precision} # 19
|
||||
else # else
|
||||
super # super
|
||||
end # end
|
||||
end # end
|
||||
end_eval
|
||||
|
||||
configure_connection
|
||||
end
|
||||
|
||||
# Configures the encoding, verbosity, and schema search path of the connection.
|
||||
# This is called by #connect and should not be called manually.
|
||||
def configure_connection
|
||||
if @config[:encoding]
|
||||
if @connection.respond_to?(:set_client_encoding)
|
||||
@connection.set_client_encoding(@config[:encoding])
|
||||
else
|
||||
execute("SET client_encoding TO '#{@config[:encoding]}'")
|
||||
end
|
||||
end
|
||||
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
||||
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
||||
end
|
||||
|
||||
# Returns the current ID of a table's sequence.
|
||||
def last_insert_id(table, sequence_name) #:nodoc:
|
||||
Integer(select_value("SELECT currval('#{sequence_name}')"))
|
||||
end
|
||||
|
||||
# Executes a SELECT query and returns the results, performing any data type
|
||||
# conversions that are required to be performed here instead of in PostgreSQLColumn.
|
||||
def select(sql, name = nil)
|
||||
fields, rows = select_raw(sql, name)
|
||||
result = []
|
||||
for row in rows
|
||||
row_hash = {}
|
||||
fields.each_with_index do |f, i|
|
||||
row_hash[f] = row[i]
|
||||
end
|
||||
result << row_hash
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def select_raw(sql, name = nil)
|
||||
res = execute(sql, name)
|
||||
results = result_as_array(res)
|
||||
fields = []
|
||||
rows = []
|
||||
if res.ntuples > 0
|
||||
fields = res.fields
|
||||
results.each do |row|
|
||||
hashed_row = {}
|
||||
row.each_index do |cell_index|
|
||||
# If this is a money type column and there are any currency symbols,
|
||||
# then strip them off. Indeed it would be prettier to do this in
|
||||
# PostgreSQLColumn.string_to_decimal but would break form input
|
||||
# fields that call value_before_type_cast.
|
||||
if res.ftype(cell_index) == MONEY_COLUMN_TYPE_OID
|
||||
# Because money output is formatted according to the locale, there are two
|
||||
# cases to consider (note the decimal separators):
|
||||
# (1) $12,345,678.12
|
||||
# (2) $12.345.678,12
|
||||
case column = row[cell_index]
|
||||
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
|
||||
row[cell_index] = column.gsub(/[^-\d\.]/, '')
|
||||
when /^-?\D+[\d\.]+,\d{2}$/ # (2)
|
||||
row[cell_index] = column.gsub(/[^-\d,]/, '').sub(/,/, '.')
|
||||
end
|
||||
end
|
||||
|
||||
hashed_row[fields[cell_index]] = column
|
||||
end
|
||||
rows << row
|
||||
end
|
||||
end
|
||||
res.clear
|
||||
return fields, rows
|
||||
end
|
||||
|
||||
# Returns the list of a table's column names, data types, and default values.
|
||||
#
|
||||
# The underlying query is roughly:
|
||||
# SELECT column.name, column.type, default.value
|
||||
# FROM column LEFT JOIN default
|
||||
# ON column.table_id = default.table_id
|
||||
# AND column.num = default.column_num
|
||||
# WHERE column.table_id = get_table_id('table_name')
|
||||
# AND column.num > 0
|
||||
# AND NOT column.is_dropped
|
||||
# ORDER BY column.num
|
||||
#
|
||||
# If the table name is not prefixed with a schema, the database will
|
||||
# take the first match from the schema search path.
|
||||
#
|
||||
# Query implementation notes:
|
||||
# - format_type includes the column size constraint, e.g. varchar(50)
|
||||
# - ::regclass is a function that gives the id for a table name
|
||||
def column_definitions(table_name) #:nodoc:
|
||||
query <<-end_sql
|
||||
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
||||
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
||||
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
||||
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
|
||||
AND a.attnum > 0 AND NOT a.attisdropped
|
||||
ORDER BY a.attnum
|
||||
end_sql
|
||||
end
|
||||
|
||||
def extract_pg_identifier_from_name(name)
|
||||
match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
|
||||
|
||||
if match_data
|
||||
rest = name[match_data[0].length..-1]
|
||||
rest = rest[1..-1] if rest[0,1] == "."
|
||||
[match_data[1], (rest.length > 0 ? rest : nil)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
require 'active_record/connection_adapters/sqlite_adapter'
|
||||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
# sqlite3 adapter reuses sqlite_connection.
|
||||
def self.sqlite3_connection(config) # :nodoc:
|
||||
parse_sqlite_config!(config)
|
||||
|
||||
unless self.class.const_defined?(:SQLite3)
|
||||
require_library_or_gem(config[:adapter])
|
||||
end
|
||||
|
||||
db = SQLite3::Database.new(
|
||||
config[:database],
|
||||
:results_as_hash => true,
|
||||
:type_translation => false
|
||||
)
|
||||
|
||||
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
|
||||
|
||||
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters #:nodoc:
|
||||
class SQLite3Adapter < SQLiteAdapter # :nodoc:
|
||||
def table_structure(table_name)
|
||||
returning structure = @connection.table_info(quote_table_name(table_name)) do
|
||||
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,453 +0,0 @@
|
||||
# encoding: binary
|
||||
require 'active_record/connection_adapters/abstract_adapter'
|
||||
|
||||
module ActiveRecord
|
||||
class Base
|
||||
class << self
|
||||
# Establishes a connection to the database that's used by all Active Record objects
|
||||
def sqlite_connection(config) # :nodoc:
|
||||
parse_sqlite_config!(config)
|
||||
|
||||
unless self.class.const_defined?(:SQLite)
|
||||
require_library_or_gem(config[:adapter])
|
||||
|
||||
db = SQLite::Database.new(config[:database], 0)
|
||||
db.show_datatypes = "ON" if !defined? SQLite::Version
|
||||
db.results_as_hash = true if defined? SQLite::Version
|
||||
db.type_translation = false
|
||||
|
||||
message = "Support for SQLite2Adapter and DeprecatedSQLiteAdapter has been removed from Rails 3. "
|
||||
message << "You should migrate to SQLite 3+ or use the plugin from git://github.com/rails/sqlite2_adapter.git with Rails 3."
|
||||
ActiveSupport::Deprecation.warn(message)
|
||||
|
||||
# "Downgrade" deprecated sqlite API
|
||||
if SQLite.const_defined?(:Version)
|
||||
ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
|
||||
else
|
||||
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def parse_sqlite_config!(config)
|
||||
if config.include?(:dbfile)
|
||||
ActiveSupport::Deprecation.warn "Please update config/database.yml to use 'database' instead of 'dbfile'"
|
||||
end
|
||||
|
||||
config[:database] ||= config[:dbfile]
|
||||
# Require database.
|
||||
unless config[:database]
|
||||
raise ArgumentError, "No database file specified. Missing argument: database"
|
||||
end
|
||||
|
||||
# Allow database path relative to RAILS_ROOT, but only if
|
||||
# the database path is not the special path that tells
|
||||
# Sqlite to build a database only in memory.
|
||||
if Object.const_defined?(:RAILS_ROOT) && ':memory:' != config[:database]
|
||||
config[:database] = File.expand_path(config[:database], RAILS_ROOT)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ConnectionAdapters #:nodoc:
|
||||
class SQLiteColumn < Column #:nodoc:
|
||||
class << self
|
||||
def string_to_binary(value)
|
||||
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
||||
value.gsub(/\0|\%/n) do |b|
|
||||
case b
|
||||
when "\0" then "%00"
|
||||
when "%" then "%25"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def binary_to_string(value)
|
||||
value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding)
|
||||
value.gsub(/%00|%25/n) do |b|
|
||||
case b
|
||||
when "%00" then "\0"
|
||||
when "%25" then "%"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The SQLite adapter works with both the 2.x and 3.x series of SQLite with the sqlite-ruby drivers (available both as gems and
|
||||
# from http://rubyforge.org/projects/sqlite-ruby/).
|
||||
#
|
||||
# Options:
|
||||
#
|
||||
# * <tt>:database</tt> - Path to the database file.
|
||||
class SQLiteAdapter < AbstractAdapter
|
||||
class Version
|
||||
include Comparable
|
||||
|
||||
def initialize(version_string)
|
||||
@version = version_string.split('.').map(&:to_i)
|
||||
end
|
||||
|
||||
def <=>(version_string)
|
||||
@version <=> version_string.split('.').map(&:to_i)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(connection, logger, config)
|
||||
super(connection, logger)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def adapter_name #:nodoc:
|
||||
'SQLite'
|
||||
end
|
||||
|
||||
def supports_ddl_transactions?
|
||||
sqlite_version >= '2.0.0'
|
||||
end
|
||||
|
||||
def supports_migrations? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def supports_primary_key? #:nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def requires_reloading?
|
||||
true
|
||||
end
|
||||
|
||||
def supports_add_column?
|
||||
sqlite_version >= '3.1.6'
|
||||
end
|
||||
|
||||
def disconnect!
|
||||
super
|
||||
@connection.close rescue nil
|
||||
end
|
||||
|
||||
def supports_count_distinct? #:nodoc:
|
||||
sqlite_version >= '3.2.6'
|
||||
end
|
||||
|
||||
def supports_autoincrement? #:nodoc:
|
||||
sqlite_version >= '3.1.0'
|
||||
end
|
||||
|
||||
def native_database_types #:nodoc:
|
||||
{
|
||||
:primary_key => default_primary_key_type,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "integer" },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "datetime" },
|
||||
:timestamp => { :name => "datetime" },
|
||||
:time => { :name => "time" },
|
||||
:date => { :name => "date" },
|
||||
:binary => { :name => "blob" },
|
||||
:boolean => { :name => "boolean" }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# QUOTING ==================================================
|
||||
|
||||
def quote_string(s) #:nodoc:
|
||||
@connection.class.quote(s)
|
||||
end
|
||||
|
||||
def quote_column_name(name) #:nodoc:
|
||||
%Q("#{name}")
|
||||
end
|
||||
|
||||
|
||||
# DATABASE STATEMENTS ======================================
|
||||
|
||||
def execute(sql, name = nil) #:nodoc:
|
||||
catch_schema_changes { log(sql, name) { @connection.execute(sql) } }
|
||||
end
|
||||
|
||||
def update_sql(sql, name = nil) #:nodoc:
|
||||
super
|
||||
@connection.changes
|
||||
end
|
||||
|
||||
def delete_sql(sql, name = nil) #:nodoc:
|
||||
sql += " WHERE 1=1" unless sql =~ /WHERE/i
|
||||
super sql, name
|
||||
end
|
||||
|
||||
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
||||
super || @connection.last_insert_row_id
|
||||
end
|
||||
|
||||
def select_rows(sql, name = nil)
|
||||
execute(sql, name).map do |row|
|
||||
(0...(row.size / 2)).map { |i| row[i] }
|
||||
end
|
||||
end
|
||||
|
||||
def begin_db_transaction #:nodoc:
|
||||
catch_schema_changes { @connection.transaction }
|
||||
end
|
||||
|
||||
def commit_db_transaction #:nodoc:
|
||||
catch_schema_changes { @connection.commit }
|
||||
end
|
||||
|
||||
def rollback_db_transaction #:nodoc:
|
||||
catch_schema_changes { @connection.rollback }
|
||||
end
|
||||
|
||||
# SELECT ... FOR UPDATE is redundant since the table is locked.
|
||||
def add_lock!(sql, options) #:nodoc:
|
||||
sql
|
||||
end
|
||||
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
def tables(name = nil) #:nodoc:
|
||||
sql = <<-SQL
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table' AND NOT name = 'sqlite_sequence'
|
||||
SQL
|
||||
|
||||
execute(sql, name).map do |row|
|
||||
row[0]
|
||||
end
|
||||
end
|
||||
|
||||
def columns(table_name, name = nil) #:nodoc:
|
||||
table_structure(table_name).map do |field|
|
||||
SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'] == "0")
|
||||
end
|
||||
end
|
||||
|
||||
def indexes(table_name, name = nil) #:nodoc:
|
||||
execute("PRAGMA index_list(#{quote_table_name(table_name)})", name).map do |row|
|
||||
index = IndexDefinition.new(table_name, row['name'])
|
||||
index.unique = row['unique'] != '0'
|
||||
index.columns = execute("PRAGMA index_info('#{index.name}')").map { |col| col['name'] }
|
||||
index
|
||||
end
|
||||
end
|
||||
|
||||
def primary_key(table_name) #:nodoc:
|
||||
column = table_structure(table_name).find {|field| field['pk'].to_i == 1}
|
||||
column ? column['name'] : nil
|
||||
end
|
||||
|
||||
def remove_index(table_name, options={}) #:nodoc:
|
||||
execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
|
||||
end
|
||||
|
||||
def rename_table(name, new_name)
|
||||
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
||||
end
|
||||
|
||||
# See: http://www.sqlite.org/lang_altertable.html
|
||||
# SQLite has an additional restriction on the ALTER TABLE statement
|
||||
def valid_alter_table_options( type, options)
|
||||
type.to_sym != :primary_key
|
||||
end
|
||||
|
||||
def add_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
if supports_add_column? && valid_alter_table_options( type, options )
|
||||
super(table_name, column_name, type, options)
|
||||
else
|
||||
alter_table(table_name) do |definition|
|
||||
definition.column(column_name, type, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_column(table_name, *column_names) #:nodoc:
|
||||
column_names.flatten.each do |column_name|
|
||||
alter_table(table_name) do |definition|
|
||||
definition.columns.delete(definition[column_name])
|
||||
end
|
||||
end
|
||||
end
|
||||
alias :remove_columns :remove_column
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
alter_table(table_name) do |definition|
|
||||
definition[column_name].default = default
|
||||
end
|
||||
end
|
||||
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
unless null || default.nil?
|
||||
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
||||
end
|
||||
alter_table(table_name) do |definition|
|
||||
definition[column_name].null = null
|
||||
end
|
||||
end
|
||||
|
||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
alter_table(table_name) do |definition|
|
||||
include_default = options_include_default?(options)
|
||||
definition[column_name].instance_eval do
|
||||
self.type = type
|
||||
self.limit = options[:limit] if options.include?(:limit)
|
||||
self.default = options[:default] if include_default
|
||||
self.null = options[:null] if options.include?(:null)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
||||
unless columns(table_name).detect{|c| c.name == column_name.to_s }
|
||||
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
|
||||
end
|
||||
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
|
||||
end
|
||||
|
||||
def empty_insert_statement(table_name)
|
||||
"INSERT INTO #{table_name} VALUES(NULL)"
|
||||
end
|
||||
|
||||
protected
|
||||
def select(sql, name = nil) #:nodoc:
|
||||
execute(sql, name).map do |row|
|
||||
record = {}
|
||||
row.each_key do |key|
|
||||
if key.is_a?(String)
|
||||
record[key.sub(/^"?\w+"?\./, '')] = row[key]
|
||||
end
|
||||
end
|
||||
record
|
||||
end
|
||||
end
|
||||
|
||||
def table_structure(table_name)
|
||||
returning structure = execute("PRAGMA table_info(#{quote_table_name(table_name)})") do
|
||||
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
|
||||
end
|
||||
end
|
||||
|
||||
def alter_table(table_name, options = {}) #:nodoc:
|
||||
altered_table_name = "altered_#{table_name}"
|
||||
caller = lambda {|definition| yield definition if block_given?}
|
||||
|
||||
transaction do
|
||||
move_table(table_name, altered_table_name,
|
||||
options.merge(:temporary => true))
|
||||
move_table(altered_table_name, table_name, &caller)
|
||||
end
|
||||
end
|
||||
|
||||
def move_table(from, to, options = {}, &block) #:nodoc:
|
||||
copy_table(from, to, options, &block)
|
||||
drop_table(from)
|
||||
end
|
||||
|
||||
def copy_table(from, to, options = {}) #:nodoc:
|
||||
options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s))
|
||||
create_table(to, options) do |definition|
|
||||
@definition = definition
|
||||
columns(from).each do |column|
|
||||
column_name = options[:rename] ?
|
||||
(options[:rename][column.name] ||
|
||||
options[:rename][column.name.to_sym] ||
|
||||
column.name) : column.name
|
||||
|
||||
@definition.column(column_name, column.type,
|
||||
:limit => column.limit, :default => column.default,
|
||||
:null => column.null)
|
||||
end
|
||||
@definition.primary_key(primary_key(from)) if primary_key(from)
|
||||
yield @definition if block_given?
|
||||
end
|
||||
|
||||
copy_table_indexes(from, to, options[:rename] || {})
|
||||
copy_table_contents(from, to,
|
||||
@definition.columns.map {|column| column.name},
|
||||
options[:rename] || {})
|
||||
end
|
||||
|
||||
def copy_table_indexes(from, to, rename = {}) #:nodoc:
|
||||
indexes(from).each do |index|
|
||||
name = index.name
|
||||
if to == "altered_#{from}"
|
||||
name = "temp_#{name}"
|
||||
elsif from == "altered_#{to}"
|
||||
name = name[5..-1]
|
||||
end
|
||||
|
||||
to_column_names = columns(to).map(&:name)
|
||||
columns = index.columns.map {|c| rename[c] || c }.select do |column|
|
||||
to_column_names.include?(column)
|
||||
end
|
||||
|
||||
unless columns.empty?
|
||||
# index name can't be the same
|
||||
opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
|
||||
opts[:unique] = true if index.unique
|
||||
add_index(to, columns, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
|
||||
column_mappings = Hash[*columns.map {|name| [name, name]}.flatten]
|
||||
rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map}
|
||||
from_columns = columns(from).collect {|col| col.name}
|
||||
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
|
||||
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
|
||||
|
||||
quoted_to = quote_table_name(to)
|
||||
@connection.execute "SELECT * FROM #{quote_table_name(from)}" do |row|
|
||||
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
|
||||
sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
|
||||
sql << ')'
|
||||
@connection.execute sql
|
||||
end
|
||||
end
|
||||
|
||||
def catch_schema_changes
|
||||
return yield
|
||||
rescue ActiveRecord::StatementInvalid => exception
|
||||
if exception.message =~ /database schema has changed/
|
||||
reconnect!
|
||||
retry
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def sqlite_version
|
||||
@sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
|
||||
end
|
||||
|
||||
def default_primary_key_type
|
||||
if supports_autoincrement?
|
||||
'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'.freeze
|
||||
else
|
||||
'INTEGER PRIMARY KEY NOT NULL'.freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SQLite2Adapter < SQLiteAdapter # :nodoc:
|
||||
def rename_table(name, new_name)
|
||||
move_table(name, new_name)
|
||||
end
|
||||
end
|
||||
|
||||
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
|
||||
def insert(sql, name = nil, pk = nil, id_value = nil)
|
||||
execute(sql, name = nil)
|
||||
id_value || @connection.last_insert_rowid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,183 +0,0 @@
|
||||
module ActiveRecord
|
||||
# Track unsaved attribute changes.
|
||||
#
|
||||
# A newly instantiated object is unchanged:
|
||||
# person = Person.find_by_name('uncle bob')
|
||||
# person.changed? # => false
|
||||
#
|
||||
# Change the name:
|
||||
# person.name = 'Bob'
|
||||
# person.changed? # => true
|
||||
# person.name_changed? # => true
|
||||
# person.name_was # => 'uncle bob'
|
||||
# person.name_change # => ['uncle bob', 'Bob']
|
||||
# person.name = 'Bill'
|
||||
# person.name_change # => ['uncle bob', 'Bill']
|
||||
#
|
||||
# Save the changes:
|
||||
# person.save
|
||||
# person.changed? # => false
|
||||
# person.name_changed? # => false
|
||||
#
|
||||
# Assigning the same value leaves the attribute unchanged:
|
||||
# person.name = 'Bill'
|
||||
# person.name_changed? # => false
|
||||
# person.name_change # => nil
|
||||
#
|
||||
# Which attributes have changed?
|
||||
# person.name = 'bob'
|
||||
# person.changed # => ['name']
|
||||
# person.changes # => { 'name' => ['Bill', 'bob'] }
|
||||
#
|
||||
# Before modifying an attribute in-place:
|
||||
# person.name_will_change!
|
||||
# person.name << 'by'
|
||||
# person.name_change # => ['uncle bob', 'uncle bobby']
|
||||
module Dirty
|
||||
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
||||
|
||||
def self.included(base)
|
||||
base.attribute_method_suffix *DIRTY_SUFFIXES
|
||||
base.alias_method_chain :write_attribute, :dirty
|
||||
base.alias_method_chain :save, :dirty
|
||||
base.alias_method_chain :save!, :dirty
|
||||
base.alias_method_chain :update, :dirty
|
||||
base.alias_method_chain :reload, :dirty
|
||||
|
||||
base.superclass_delegating_accessor :partial_updates
|
||||
base.partial_updates = true
|
||||
|
||||
base.send(:extend, ClassMethods)
|
||||
end
|
||||
|
||||
# Do any attributes have unsaved changes?
|
||||
# person.changed? # => false
|
||||
# person.name = 'bob'
|
||||
# person.changed? # => true
|
||||
def changed?
|
||||
!changed_attributes.empty?
|
||||
end
|
||||
|
||||
# List of attributes with unsaved changes.
|
||||
# person.changed # => []
|
||||
# person.name = 'bob'
|
||||
# person.changed # => ['name']
|
||||
def changed
|
||||
changed_attributes.keys
|
||||
end
|
||||
|
||||
# Map of changed attrs => [original value, new value].
|
||||
# person.changes # => {}
|
||||
# person.name = 'bob'
|
||||
# person.changes # => { 'name' => ['bill', 'bob'] }
|
||||
def changes
|
||||
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
||||
end
|
||||
|
||||
# Attempts to +save+ the record and clears changed attributes if successful.
|
||||
def save_with_dirty(*args) #:nodoc:
|
||||
if status = save_without_dirty(*args)
|
||||
changed_attributes.clear
|
||||
end
|
||||
status
|
||||
end
|
||||
|
||||
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
||||
def save_with_dirty!(*args) #:nodoc:
|
||||
status = save_without_dirty!(*args)
|
||||
changed_attributes.clear
|
||||
status
|
||||
end
|
||||
|
||||
# <tt>reload</tt> the record and clears changed attributes.
|
||||
def reload_with_dirty(*args) #:nodoc:
|
||||
record = reload_without_dirty(*args)
|
||||
changed_attributes.clear
|
||||
record
|
||||
end
|
||||
|
||||
private
|
||||
# Map of change <tt>attr => original value</tt>.
|
||||
def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||
def attribute_will_change!(attr)
|
||||
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
||||
end
|
||||
|
||||
# Wrap write_attribute to remember original attribute value.
|
||||
def write_attribute_with_dirty(attr, value)
|
||||
attr = attr.to_s
|
||||
|
||||
# The attribute already has an unsaved change.
|
||||
if changed_attributes.include?(attr)
|
||||
old = changed_attributes[attr]
|
||||
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
||||
else
|
||||
old = clone_attribute_value(:read_attribute, attr)
|
||||
changed_attributes[attr] = old if field_changed?(attr, old, value)
|
||||
end
|
||||
|
||||
# Carry on.
|
||||
write_attribute_without_dirty(attr, value)
|
||||
end
|
||||
|
||||
def update_with_dirty
|
||||
if partial_updates?
|
||||
# Serialized attributes should always be written in case they've been
|
||||
# changed in place.
|
||||
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
|
||||
else
|
||||
update_without_dirty
|
||||
end
|
||||
end
|
||||
|
||||
def field_changed?(attr, old, value)
|
||||
if column = column_for_attribute(attr)
|
||||
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
||||
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
||||
# Hence we don't record it as a change if the value changes from nil to ''.
|
||||
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
||||
# be typecast back to 0 (''.to_i => 0)
|
||||
value = nil
|
||||
else
|
||||
value = column.type_cast(value)
|
||||
end
|
||||
end
|
||||
|
||||
old != value
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
base.metaclass.alias_method_chain(:alias_attribute, :dirty)
|
||||
end
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
module ActiveRecord
|
||||
class DynamicFinderMatch
|
||||
def self.match(method)
|
||||
df_match = self.new(method)
|
||||
df_match.finder ? df_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@finder = :first
|
||||
case method.to_s
|
||||
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
||||
@finder = :last if $1 == 'last_by'
|
||||
@finder = :all if $1 == 'all_by'
|
||||
names = $2
|
||||
when /^find_by_([_a-zA-Z]\w*)\!$/
|
||||
@bang = true
|
||||
names = $1
|
||||
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
||||
@instantiator = $1 == 'initialize' ? :new : :create
|
||||
names = $2
|
||||
else
|
||||
@finder = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :finder, :attribute_names, :instantiator
|
||||
|
||||
def finder?
|
||||
!@finder.nil? && @instantiator.nil?
|
||||
end
|
||||
|
||||
def instantiator?
|
||||
@finder == :first && !@instantiator.nil?
|
||||
end
|
||||
|
||||
def bang?
|
||||
@bang
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
module ActiveRecord
|
||||
class DynamicScopeMatch
|
||||
def self.match(method)
|
||||
ds_match = self.new(method)
|
||||
ds_match.scope ? ds_match : nil
|
||||
end
|
||||
|
||||
def initialize(method)
|
||||
@scope = true
|
||||
case method.to_s
|
||||
when /^scoped_by_([_a-zA-Z]\w*)$/
|
||||
names = $1
|
||||
else
|
||||
@scope = nil
|
||||
end
|
||||
@attribute_names = names && names.split('_and_')
|
||||
end
|
||||
|
||||
attr_reader :scope, :attribute_names
|
||||
|
||||
def scope?
|
||||
!@scope.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,996 +0,0 @@
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'csv'
|
||||
require 'zlib'
|
||||
require 'active_support/dependencies'
|
||||
require 'active_support/test_case'
|
||||
|
||||
if RUBY_VERSION < '1.9'
|
||||
module YAML #:nodoc:
|
||||
class Omap #:nodoc:
|
||||
def keys; map { |k, v| k } end
|
||||
def values; map { |k, v| v } end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if defined? ActiveRecord
|
||||
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
|
||||
end
|
||||
else
|
||||
class FixtureClassNotFound < StandardError #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
# Fixtures are a way of organizing data that you want to test against; in short, sample data.
|
||||
#
|
||||
# = Fixture formats
|
||||
#
|
||||
# Fixtures come in 3 flavors:
|
||||
#
|
||||
# 1. YAML fixtures
|
||||
# 2. CSV fixtures
|
||||
# 3. Single-file fixtures
|
||||
#
|
||||
# == YAML fixtures
|
||||
#
|
||||
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
|
||||
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
|
||||
#
|
||||
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
|
||||
# by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
|
||||
# put your files in <tt><your-rails-app>/test/fixtures/</tt>). The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
|
||||
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this:
|
||||
#
|
||||
# rubyonrails:
|
||||
# id: 1
|
||||
# name: Ruby on Rails
|
||||
# url: http://www.rubyonrails.org
|
||||
#
|
||||
# google:
|
||||
# id: 2
|
||||
# name: Google
|
||||
# url: http://www.google.com
|
||||
#
|
||||
# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an
|
||||
# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
|
||||
# pleasure.
|
||||
#
|
||||
# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html
|
||||
# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
|
||||
# This is commonly needed for tree structures. Example:
|
||||
#
|
||||
# --- !omap
|
||||
# - parent:
|
||||
# id: 1
|
||||
# parent_id: NULL
|
||||
# title: Parent
|
||||
# - child:
|
||||
# id: 2
|
||||
# parent_id: 1
|
||||
# title: Child
|
||||
#
|
||||
# == CSV fixtures
|
||||
#
|
||||
# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored
|
||||
# in a single file, but instead end with the <tt>.csv</tt> file extension
|
||||
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
|
||||
#
|
||||
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
|
||||
# humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised
|
||||
# of the actual data (1 per line). Here's an example:
|
||||
#
|
||||
# id, name, url
|
||||
# 1, Ruby On Rails, http://www.rubyonrails.org
|
||||
# 2, Google, http://www.google.com
|
||||
#
|
||||
# Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you
|
||||
# need to use a double quote character, you must escape it with another double quote.
|
||||
#
|
||||
# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the
|
||||
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
|
||||
# number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
|
||||
# "web_site_2".
|
||||
#
|
||||
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
|
||||
# have existing data somewhere already.
|
||||
#
|
||||
# == Single-file fixtures
|
||||
#
|
||||
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
|
||||
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
|
||||
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
|
||||
# put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> --
|
||||
# like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model).
|
||||
#
|
||||
# Each text file placed in this directory represents a "record". Usually these types of fixtures are named without
|
||||
# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension. Here's what the
|
||||
# above example might look like:
|
||||
#
|
||||
# web_sites/google
|
||||
# web_sites/yahoo.txt
|
||||
# web_sites/ruby-on-rails
|
||||
#
|
||||
# The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax
|
||||
# of "name => value". Here's an example of the ruby-on-rails fixture above:
|
||||
#
|
||||
# id => 1
|
||||
# name => Ruby on Rails
|
||||
# url => http://www.rubyonrails.org
|
||||
#
|
||||
# = Using fixtures in testcases
|
||||
#
|
||||
# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
|
||||
# fixtures, but first let's take a look at a sample unit test:
|
||||
#
|
||||
# require 'test_helper'
|
||||
#
|
||||
# class WebSiteTest < ActiveSupport::TestCase
|
||||
# test "web_site_count" do
|
||||
# assert_equal 2, WebSite.count
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed.
|
||||
# The testing environment will automatically load the all fixtures into the database before each test.
|
||||
# To ensure consistent data, the environment deletes the fixtures before running the load.
|
||||
#
|
||||
# In addition to being available in the database, the fixture's data may also be accessed by
|
||||
# using a special dynamic method, which has the same name as the model, and accepts the
|
||||
# name of the fixture to instantiate:
|
||||
#
|
||||
# test "find" do
|
||||
# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
|
||||
# end
|
||||
#
|
||||
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
|
||||
#
|
||||
# test "find_alt_method_1" do
|
||||
# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
|
||||
# end
|
||||
#
|
||||
# test "find_alt_method_2" do
|
||||
# assert_equal "Ruby on Rails", @rubyonrails.news
|
||||
# end
|
||||
#
|
||||
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
|
||||
# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
|
||||
#
|
||||
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
|
||||
# self.use_instantiated_fixtures = true
|
||||
#
|
||||
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
|
||||
# self.use_instantiated_fixtures = :no_instances
|
||||
#
|
||||
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
|
||||
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
|
||||
# large sets of fixtured data.
|
||||
#
|
||||
# = Dynamic fixtures with ERb
|
||||
#
|
||||
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
|
||||
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
|
||||
#
|
||||
# <% for i in 1..1000 %>
|
||||
# fix_<%= i %>:
|
||||
# id: <%= i %>
|
||||
# name: guy_<%= 1 %>
|
||||
# <% end %>
|
||||
#
|
||||
# This will create 1000 very simple YAML fixtures.
|
||||
#
|
||||
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
|
||||
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
|
||||
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
|
||||
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
|
||||
#
|
||||
# = Transactional fixtures
|
||||
#
|
||||
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
|
||||
#
|
||||
# class FooTest < ActiveSupport::TestCase
|
||||
# self.use_transactional_fixtures = true
|
||||
#
|
||||
# test "godzilla" do
|
||||
# assert !Foo.find(:all).empty?
|
||||
# Foo.destroy_all
|
||||
# assert Foo.find(:all).empty?
|
||||
# end
|
||||
#
|
||||
# test "godzilla aftermath" do
|
||||
# assert !Foo.find(:all).empty?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
|
||||
# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
|
||||
#
|
||||
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
|
||||
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
|
||||
#
|
||||
# When *not* to use transactional fixtures:
|
||||
#
|
||||
# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
|
||||
# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
|
||||
# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
|
||||
# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
|
||||
# Use InnoDB, MaxDB, or NDB instead.
|
||||
#
|
||||
# = Advanced YAML Fixtures
|
||||
#
|
||||
# YAML fixtures that don't specify an ID get some extra features:
|
||||
#
|
||||
# * Stable, autogenerated IDs
|
||||
# * Label references for associations (belongs_to, has_one, has_many)
|
||||
# * HABTM associations as inline lists
|
||||
# * Autofilled timestamp columns
|
||||
# * Fixture label interpolation
|
||||
# * Support for YAML defaults
|
||||
#
|
||||
# == Stable, autogenerated IDs
|
||||
#
|
||||
# Here, have a monkey fixture:
|
||||
#
|
||||
# george:
|
||||
# id: 1
|
||||
# name: George the Monkey
|
||||
#
|
||||
# reginald:
|
||||
# id: 2
|
||||
# name: Reginald the Pirate
|
||||
#
|
||||
# Each of these fixtures has two unique identifiers: one for the database
|
||||
# and one for the humans. Why don't we generate the primary key instead?
|
||||
# Hashing each fixture's label yields a consistent ID:
|
||||
#
|
||||
# george: # generated id: 503576764
|
||||
# name: George the Monkey
|
||||
#
|
||||
# reginald: # generated id: 324201669
|
||||
# name: Reginald the Pirate
|
||||
#
|
||||
# Active Record looks at the fixture's model class, discovers the correct
|
||||
# primary key, and generates it right before inserting the fixture
|
||||
# into the database.
|
||||
#
|
||||
# The generated ID for a given label is constant, so we can discover
|
||||
# any fixture's ID without loading anything, as long as we know the label.
|
||||
#
|
||||
# == Label references for associations (belongs_to, has_one, has_many)
|
||||
#
|
||||
# Specifying foreign keys in fixtures can be very fragile, not to
|
||||
# mention difficult to read. Since Active Record can figure out the ID of
|
||||
# any fixture from its label, you can specify FK's by label instead of ID.
|
||||
#
|
||||
# === belongs_to
|
||||
#
|
||||
# Let's break out some more monkeys and pirates.
|
||||
#
|
||||
# ### in pirates.yml
|
||||
#
|
||||
# reginald:
|
||||
# id: 1
|
||||
# name: Reginald the Pirate
|
||||
# monkey_id: 1
|
||||
#
|
||||
# ### in monkeys.yml
|
||||
#
|
||||
# george:
|
||||
# id: 1
|
||||
# name: George the Monkey
|
||||
# pirate_id: 1
|
||||
#
|
||||
# Add a few more monkeys and pirates and break this into multiple files,
|
||||
# and it gets pretty hard to keep track of what's going on. Let's
|
||||
# use labels instead of IDs:
|
||||
#
|
||||
# ### in pirates.yml
|
||||
#
|
||||
# reginald:
|
||||
# name: Reginald the Pirate
|
||||
# monkey: george
|
||||
#
|
||||
# ### in monkeys.yml
|
||||
#
|
||||
# george:
|
||||
# name: George the Monkey
|
||||
# pirate: reginald
|
||||
#
|
||||
# Pow! All is made clear. Active Record reflects on the fixture's model class,
|
||||
# finds all the +belongs_to+ associations, and allows you to specify
|
||||
# a target *label* for the *association* (monkey: george) rather than
|
||||
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
|
||||
#
|
||||
# ==== Polymorphic belongs_to
|
||||
#
|
||||
# Supporting polymorphic relationships is a little bit more complicated, since
|
||||
# Active Record needs to know what type your association is pointing at. Something
|
||||
# like this should look familiar:
|
||||
#
|
||||
# ### in fruit.rb
|
||||
#
|
||||
# belongs_to :eater, :polymorphic => true
|
||||
#
|
||||
# ### in fruits.yml
|
||||
#
|
||||
# apple:
|
||||
# id: 1
|
||||
# name: apple
|
||||
# eater_id: 1
|
||||
# eater_type: Monkey
|
||||
#
|
||||
# Can we do better? You bet!
|
||||
#
|
||||
# apple:
|
||||
# eater: george (Monkey)
|
||||
#
|
||||
# Just provide the polymorphic target type and Active Record will take care of the rest.
|
||||
#
|
||||
# === has_and_belongs_to_many
|
||||
#
|
||||
# Time to give our monkey some fruit.
|
||||
#
|
||||
# ### in monkeys.yml
|
||||
#
|
||||
# george:
|
||||
# id: 1
|
||||
# name: George the Monkey
|
||||
# pirate_id: 1
|
||||
#
|
||||
# ### in fruits.yml
|
||||
#
|
||||
# apple:
|
||||
# id: 1
|
||||
# name: apple
|
||||
#
|
||||
# orange:
|
||||
# id: 2
|
||||
# name: orange
|
||||
#
|
||||
# grape:
|
||||
# id: 3
|
||||
# name: grape
|
||||
#
|
||||
# ### in fruits_monkeys.yml
|
||||
#
|
||||
# apple_george:
|
||||
# fruit_id: 1
|
||||
# monkey_id: 1
|
||||
#
|
||||
# orange_george:
|
||||
# fruit_id: 2
|
||||
# monkey_id: 1
|
||||
#
|
||||
# grape_george:
|
||||
# fruit_id: 3
|
||||
# monkey_id: 1
|
||||
#
|
||||
# Let's make the HABTM fixture go away.
|
||||
#
|
||||
# ### in monkeys.yml
|
||||
#
|
||||
# george:
|
||||
# name: George the Monkey
|
||||
# pirate: reginald
|
||||
# fruits: apple, orange, grape
|
||||
#
|
||||
# ### in fruits.yml
|
||||
#
|
||||
# apple:
|
||||
# name: apple
|
||||
#
|
||||
# orange:
|
||||
# name: orange
|
||||
#
|
||||
# grape:
|
||||
# name: grape
|
||||
#
|
||||
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
|
||||
# on George's fixture, but we could've just as easily specified a list
|
||||
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
|
||||
# the fixture's model class and discovers the +has_and_belongs_to_many+
|
||||
# associations.
|
||||
#
|
||||
# == Autofilled timestamp columns
|
||||
#
|
||||
# If your table/model specifies any of Active Record's
|
||||
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
|
||||
# they will automatically be set to <tt>Time.now</tt>.
|
||||
#
|
||||
# If you've set specific values, they'll be left alone.
|
||||
#
|
||||
# == Fixture label interpolation
|
||||
#
|
||||
# The label of the current fixture is always available as a column value:
|
||||
#
|
||||
# geeksomnia:
|
||||
# name: Geeksomnia's Account
|
||||
# subdomain: $LABEL
|
||||
#
|
||||
# Also, sometimes (like when porting older join table fixtures) you'll need
|
||||
# to be able to get ahold of the identifier for a given label. ERB
|
||||
# to the rescue:
|
||||
#
|
||||
# george_reginald:
|
||||
# monkey_id: <%= Fixtures.identify(:reginald) %>
|
||||
# pirate_id: <%= Fixtures.identify(:george) %>
|
||||
#
|
||||
# == Support for YAML defaults
|
||||
#
|
||||
# You probably already know how to use YAML to set and reuse defaults in
|
||||
# your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
|
||||
#
|
||||
# DEFAULTS: &DEFAULTS
|
||||
# created_on: <%= 3.weeks.ago.to_s(:db) %>
|
||||
#
|
||||
# first:
|
||||
# name: Smurf
|
||||
# <<: *DEFAULTS
|
||||
#
|
||||
# second:
|
||||
# name: Fraggle
|
||||
# <<: *DEFAULTS
|
||||
#
|
||||
# Any fixture labeled "DEFAULTS" is safely ignored.
|
||||
|
||||
class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
|
||||
MAX_ID = 2 ** 30 - 1
|
||||
DEFAULT_FILTER_RE = /\.ya?ml$/
|
||||
|
||||
@@all_cached_fixtures = {}
|
||||
|
||||
def self.reset_cache(connection = nil)
|
||||
connection ||= ActiveRecord::Base.connection
|
||||
@@all_cached_fixtures[connection.object_id] = {}
|
||||
end
|
||||
|
||||
def self.cache_for_connection(connection)
|
||||
@@all_cached_fixtures[connection.object_id] ||= {}
|
||||
@@all_cached_fixtures[connection.object_id]
|
||||
end
|
||||
|
||||
def self.fixture_is_cached?(connection, table_name)
|
||||
cache_for_connection(connection)[table_name]
|
||||
end
|
||||
|
||||
def self.cached_fixtures(connection, keys_to_fetch = nil)
|
||||
if keys_to_fetch
|
||||
fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
|
||||
else
|
||||
fixtures = cache_for_connection(connection).values
|
||||
end
|
||||
fixtures.size > 1 ? fixtures : fixtures.first
|
||||
end
|
||||
|
||||
def self.cache_fixtures(connection, fixtures_map)
|
||||
cache_for_connection(connection).update(fixtures_map)
|
||||
end
|
||||
|
||||
def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
|
||||
object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
|
||||
if load_instances
|
||||
ActiveRecord::Base.silence do
|
||||
fixtures.each do |name, fixture|
|
||||
begin
|
||||
object.instance_variable_set "@#{name}", fixture.find
|
||||
rescue FixtureClassNotFound
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.instantiate_all_loaded_fixtures(object, load_instances = true)
|
||||
all_loaded_fixtures.each do |table_name, fixtures|
|
||||
Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
|
||||
end
|
||||
end
|
||||
|
||||
cattr_accessor :all_loaded_fixtures
|
||||
self.all_loaded_fixtures = {}
|
||||
|
||||
def self.create_fixtures(fixtures_directory, table_names, class_names = {})
|
||||
table_names = [table_names].flatten.map { |n| n.to_s }
|
||||
connection = block_given? ? yield : ActiveRecord::Base.connection
|
||||
|
||||
table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
|
||||
|
||||
unless table_names_to_fetch.empty?
|
||||
ActiveRecord::Base.silence do
|
||||
connection.disable_referential_integrity do
|
||||
fixtures_map = {}
|
||||
|
||||
fixtures = table_names_to_fetch.map do |table_name|
|
||||
fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
|
||||
end
|
||||
|
||||
all_loaded_fixtures.update(fixtures_map)
|
||||
|
||||
connection.transaction(:requires_new => true) do
|
||||
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
|
||||
fixtures.each { |fixture| fixture.insert_fixtures }
|
||||
|
||||
# Cap primary key sequences to max(pk).
|
||||
if connection.respond_to?(:reset_pk_sequence!)
|
||||
table_names.each do |table_name|
|
||||
connection.reset_pk_sequence!(table_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cache_fixtures(connection, fixtures_map)
|
||||
end
|
||||
end
|
||||
end
|
||||
cached_fixtures(connection, table_names)
|
||||
end
|
||||
|
||||
# Returns a consistent, platform-independent identifier for +label+.
|
||||
# Identifiers are positive integers less than 2^32.
|
||||
def self.identify(label)
|
||||
Zlib.crc32(label.to_s) % MAX_ID
|
||||
end
|
||||
|
||||
attr_reader :table_name, :name
|
||||
|
||||
def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
|
||||
@connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
|
||||
@name = table_name # preserve fixture base name
|
||||
@class_name = class_name ||
|
||||
(ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
|
||||
@table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
|
||||
@table_name = class_name.table_name if class_name.respond_to?(:table_name)
|
||||
@connection = class_name.connection if class_name.respond_to?(:connection)
|
||||
read_fixture_files
|
||||
end
|
||||
|
||||
def delete_existing_fixtures
|
||||
@connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
|
||||
end
|
||||
|
||||
def insert_fixtures
|
||||
now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
||||
now = now.to_s(:db)
|
||||
|
||||
# allow a standard key to be used for doing defaults in YAML
|
||||
if is_a?(Hash)
|
||||
delete('DEFAULTS')
|
||||
else
|
||||
delete(assoc('DEFAULTS'))
|
||||
end
|
||||
|
||||
# track any join tables we need to insert later
|
||||
habtm_fixtures = Hash.new do |h, habtm|
|
||||
h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
|
||||
end
|
||||
|
||||
each do |label, fixture|
|
||||
row = fixture.to_hash
|
||||
|
||||
if model_class && model_class < ActiveRecord::Base
|
||||
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
|
||||
if model_class.record_timestamps
|
||||
timestamp_column_names.each do |name|
|
||||
row[name] = now unless row.key?(name)
|
||||
end
|
||||
end
|
||||
|
||||
# interpolate the fixture label
|
||||
row.each do |key, value|
|
||||
row[key] = label if value == "$LABEL"
|
||||
end
|
||||
|
||||
# generate a primary key if necessary
|
||||
if has_primary_key_column? && !row.include?(primary_key_name)
|
||||
row[primary_key_name] = Fixtures.identify(label)
|
||||
end
|
||||
|
||||
# If STI is used, find the correct subclass for association reflection
|
||||
reflection_class =
|
||||
if row.include?(inheritance_column_name)
|
||||
row[inheritance_column_name].constantize rescue model_class
|
||||
else
|
||||
model_class
|
||||
end
|
||||
|
||||
reflection_class.reflect_on_all_associations.each do |association|
|
||||
case association.macro
|
||||
when :belongs_to
|
||||
# Do not replace association name with association foreign key if they are named the same
|
||||
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
|
||||
|
||||
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
|
||||
if association.options[:polymorphic]
|
||||
if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
|
||||
target_type = $1
|
||||
target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
|
||||
|
||||
# support polymorphic belongs_to as "label (Type)"
|
||||
row[target_type_name] = target_type
|
||||
end
|
||||
end
|
||||
|
||||
row[fk_name] = Fixtures.identify(value)
|
||||
end
|
||||
when :has_and_belongs_to_many
|
||||
if (targets = row.delete(association.name.to_s))
|
||||
targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
|
||||
join_fixtures = habtm_fixtures[association]
|
||||
|
||||
targets.each do |target|
|
||||
join_fixtures["#{label}_#{target}"] = Fixture.new(
|
||||
{ association.primary_key_name => row[primary_key_name],
|
||||
association.association_foreign_key => Fixtures.identify(target) },
|
||||
nil, @connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@connection.insert_fixture(fixture, @table_name)
|
||||
end
|
||||
|
||||
# insert any HABTM join tables we discovered
|
||||
habtm_fixtures.values.each do |fixture|
|
||||
fixture.delete_existing_fixtures
|
||||
fixture.insert_fixtures
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
class HabtmFixtures < ::Fixtures #:nodoc:
|
||||
def read_fixture_files; end
|
||||
end
|
||||
|
||||
def model_class
|
||||
unless defined?(@model_class)
|
||||
@model_class =
|
||||
if @class_name.nil? || @class_name.is_a?(Class)
|
||||
@class_name
|
||||
else
|
||||
@class_name.constantize rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
@model_class
|
||||
end
|
||||
|
||||
def primary_key_name
|
||||
@primary_key_name ||= model_class && model_class.primary_key
|
||||
end
|
||||
|
||||
def has_primary_key_column?
|
||||
@has_primary_key_column ||= model_class && primary_key_name &&
|
||||
model_class.columns.find { |c| c.name == primary_key_name }
|
||||
end
|
||||
|
||||
def timestamp_column_names
|
||||
@timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
|
||||
column_names.include?(name)
|
||||
end
|
||||
end
|
||||
|
||||
def inheritance_column_name
|
||||
@inheritance_column_name ||= model_class && model_class.inheritance_column
|
||||
end
|
||||
|
||||
def column_names
|
||||
@column_names ||= @connection.columns(@table_name).collect(&:name)
|
||||
end
|
||||
|
||||
def read_fixture_files
|
||||
if File.file?(yaml_file_path)
|
||||
read_yaml_fixture_files
|
||||
elsif File.file?(csv_file_path)
|
||||
read_csv_fixture_files
|
||||
end
|
||||
end
|
||||
|
||||
def read_yaml_fixture_files
|
||||
yaml_string = ""
|
||||
Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
|
||||
yaml_string << IO.read(subfixture_path)
|
||||
end
|
||||
yaml_string << IO.read(yaml_file_path)
|
||||
|
||||
if yaml = parse_yaml_string(yaml_string)
|
||||
# If the file is an ordered map, extract its children.
|
||||
yaml_value =
|
||||
if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
|
||||
yaml.value
|
||||
else
|
||||
[yaml]
|
||||
end
|
||||
|
||||
yaml_value.each do |fixture|
|
||||
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
|
||||
fixture.each do |name, data|
|
||||
unless data
|
||||
raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
|
||||
end
|
||||
|
||||
self[name] = Fixture.new(data, model_class, @connection)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read_csv_fixture_files
|
||||
reader = CSV.parse(erb_render(IO.read(csv_file_path)))
|
||||
header = reader.shift
|
||||
i = 0
|
||||
reader.each do |row|
|
||||
data = {}
|
||||
row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
|
||||
self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection)
|
||||
end
|
||||
end
|
||||
|
||||
def yaml_file_path
|
||||
"#{@fixture_path}.yml"
|
||||
end
|
||||
|
||||
def csv_file_path
|
||||
@fixture_path + ".csv"
|
||||
end
|
||||
|
||||
def yaml_fixtures_key(path)
|
||||
File.basename(@fixture_path).split(".").first
|
||||
end
|
||||
|
||||
def parse_yaml_string(fixture_content)
|
||||
YAML::load(erb_render(fixture_content))
|
||||
rescue => error
|
||||
raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}"
|
||||
end
|
||||
|
||||
def erb_render(fixture_content)
|
||||
ERB.new(fixture_content).result
|
||||
end
|
||||
end
|
||||
|
||||
class Fixture #:nodoc:
|
||||
include Enumerable
|
||||
|
||||
class FixtureError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
class FormatError < FixtureError #:nodoc:
|
||||
end
|
||||
|
||||
attr_reader :model_class
|
||||
|
||||
def initialize(fixture, model_class, connection = ActiveRecord::Base.connection)
|
||||
@connection = connection
|
||||
@fixture = fixture
|
||||
@model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
|
||||
end
|
||||
|
||||
def class_name
|
||||
@model_class.name if @model_class
|
||||
end
|
||||
|
||||
def each
|
||||
@fixture.each { |item| yield item }
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@fixture[key]
|
||||
end
|
||||
|
||||
def to_hash
|
||||
@fixture
|
||||
end
|
||||
|
||||
def key_list
|
||||
columns = @fixture.keys.collect{ |column_name| @connection.quote_column_name(column_name) }
|
||||
columns.join(", ")
|
||||
end
|
||||
|
||||
def value_list
|
||||
list = @fixture.inject([]) do |fixtures, (key, value)|
|
||||
col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
|
||||
fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
|
||||
end
|
||||
list * ', '
|
||||
end
|
||||
|
||||
def find
|
||||
if model_class
|
||||
model_class.find(self[model_class.primary_key])
|
||||
else
|
||||
raise FixtureClassNotFound, "No class attached to find."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveRecord
|
||||
module TestFixtures
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
setup :setup_fixtures
|
||||
teardown :teardown_fixtures
|
||||
|
||||
superclass_delegating_accessor :fixture_path
|
||||
superclass_delegating_accessor :fixture_table_names
|
||||
superclass_delegating_accessor :fixture_class_names
|
||||
superclass_delegating_accessor :use_transactional_fixtures
|
||||
superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
|
||||
superclass_delegating_accessor :pre_loaded_fixtures
|
||||
|
||||
self.fixture_table_names = []
|
||||
self.use_transactional_fixtures = false
|
||||
self.use_instantiated_fixtures = true
|
||||
self.pre_loaded_fixtures = false
|
||||
|
||||
self.fixture_class_names = {}
|
||||
end
|
||||
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def set_fixture_class(class_names = {})
|
||||
self.fixture_class_names = self.fixture_class_names.merge(class_names)
|
||||
end
|
||||
|
||||
def fixtures(*table_names)
|
||||
if table_names.first == :all
|
||||
table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
|
||||
table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
|
||||
else
|
||||
table_names = table_names.flatten.map { |n| n.to_s }
|
||||
end
|
||||
|
||||
self.fixture_table_names |= table_names
|
||||
require_fixture_classes(table_names)
|
||||
setup_fixture_accessors(table_names)
|
||||
end
|
||||
|
||||
def try_to_load_dependency(file_name)
|
||||
require_dependency file_name
|
||||
rescue LoadError => e
|
||||
# Let's hope the developer has included it himself
|
||||
|
||||
# Let's warn in case this is a subdependency, otherwise
|
||||
# subdependency error messages are totally cryptic
|
||||
if ActiveRecord::Base.logger
|
||||
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
|
||||
end
|
||||
end
|
||||
|
||||
def require_fixture_classes(table_names = nil)
|
||||
(table_names || fixture_table_names).each do |table_name|
|
||||
file_name = table_name.to_s
|
||||
file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
|
||||
try_to_load_dependency(file_name)
|
||||
end
|
||||
end
|
||||
|
||||
def setup_fixture_accessors(table_names = nil)
|
||||
table_names = [table_names] if table_names && !table_names.respond_to?(:each)
|
||||
(table_names || fixture_table_names).each do |table_name|
|
||||
table_name = table_name.to_s.tr('.', '_')
|
||||
|
||||
define_method(table_name) do |*fixtures|
|
||||
force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
|
||||
|
||||
@fixture_cache[table_name] ||= {}
|
||||
|
||||
instances = fixtures.map do |fixture|
|
||||
@fixture_cache[table_name].delete(fixture) if force_reload
|
||||
|
||||
if @loaded_fixtures[table_name][fixture.to_s]
|
||||
@fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
|
||||
else
|
||||
raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
|
||||
end
|
||||
end
|
||||
|
||||
instances.size == 1 ? instances.first : instances
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def uses_transaction(*methods)
|
||||
@uses_transaction = [] unless defined?(@uses_transaction)
|
||||
@uses_transaction.concat methods.map(&:to_s)
|
||||
end
|
||||
|
||||
def uses_transaction?(method)
|
||||
@uses_transaction = [] unless defined?(@uses_transaction)
|
||||
@uses_transaction.include?(method.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def run_in_transaction?
|
||||
use_transactional_fixtures &&
|
||||
!self.class.uses_transaction?(method_name)
|
||||
end
|
||||
|
||||
def setup_fixtures
|
||||
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
|
||||
|
||||
if pre_loaded_fixtures && !use_transactional_fixtures
|
||||
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
|
||||
end
|
||||
|
||||
@fixture_cache = {}
|
||||
@@already_loaded_fixtures ||= {}
|
||||
|
||||
# Load fixtures once and begin transaction.
|
||||
if run_in_transaction?
|
||||
if @@already_loaded_fixtures[self.class]
|
||||
@loaded_fixtures = @@already_loaded_fixtures[self.class]
|
||||
else
|
||||
load_fixtures
|
||||
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
||||
end
|
||||
ActiveRecord::Base.connection.increment_open_transactions
|
||||
ActiveRecord::Base.connection.transaction_joinable = false
|
||||
ActiveRecord::Base.connection.begin_db_transaction
|
||||
# Load fixtures for every test.
|
||||
else
|
||||
Fixtures.reset_cache
|
||||
@@already_loaded_fixtures[self.class] = nil
|
||||
load_fixtures
|
||||
end
|
||||
|
||||
# Instantiate fixtures for every test if requested.
|
||||
instantiate_fixtures if use_instantiated_fixtures
|
||||
end
|
||||
|
||||
def teardown_fixtures
|
||||
return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
|
||||
|
||||
unless run_in_transaction?
|
||||
Fixtures.reset_cache
|
||||
end
|
||||
|
||||
# Rollback changes if a transaction is active.
|
||||
if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
|
||||
ActiveRecord::Base.connection.rollback_db_transaction
|
||||
ActiveRecord::Base.connection.decrement_open_transactions
|
||||
end
|
||||
ActiveRecord::Base.clear_active_connections!
|
||||
end
|
||||
|
||||
private
|
||||
def load_fixtures
|
||||
@loaded_fixtures = {}
|
||||
fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
|
||||
unless fixtures.nil?
|
||||
if fixtures.instance_of?(Fixtures)
|
||||
@loaded_fixtures[fixtures.name] = fixtures
|
||||
else
|
||||
fixtures.each { |f| @loaded_fixtures[f.name] = f }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
|
||||
@@required_fixture_classes = false
|
||||
|
||||
def instantiate_fixtures
|
||||
if pre_loaded_fixtures
|
||||
raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
|
||||
unless @@required_fixture_classes
|
||||
self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
|
||||
@@required_fixture_classes = true
|
||||
end
|
||||
Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
|
||||
else
|
||||
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
|
||||
@loaded_fixtures.each do |table_name, fixtures|
|
||||
Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_instances?
|
||||
use_instantiated_fixtures != :no_instances
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# Deprecates the use of the former message interpolation syntax in activerecord
|
||||
# as in "must have %d characters". The new syntax uses explicit variable names
|
||||
# as in "{{value}} must have {{count}} characters".
|
||||
|
||||
require 'i18n/backend/simple'
|
||||
module I18n
|
||||
module Backend
|
||||
class Simple
|
||||
DEPRECATED_INTERPOLATORS = { '%d' => '{{count}}', '%s' => '{{value}}' }
|
||||
|
||||
protected
|
||||
def interpolate_with_deprecated_syntax(locale, string, values = {})
|
||||
return string unless string.is_a?(String) && !values.empty?
|
||||
|
||||
string = string.gsub(/%d|%s/) do |s|
|
||||
instead = DEPRECATED_INTERPOLATORS[s]
|
||||
ActiveSupport::Deprecation.warn "using #{s} in messages is deprecated; use #{instead} instead."
|
||||
instead
|
||||
end
|
||||
|
||||
interpolate_without_deprecated_syntax(locale, string, values)
|
||||
end
|
||||
alias_method_chain :interpolate, :deprecated_syntax
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
en:
|
||||
activerecord:
|
||||
errors:
|
||||
# The values :model, :attribute and :value are always available for interpolation
|
||||
# The value :count is available when applicable. Can be used for pluralization.
|
||||
messages:
|
||||
inclusion: "is not included in the list"
|
||||
exclusion: "is reserved"
|
||||
invalid: "is invalid"
|
||||
confirmation: "doesn't match confirmation"
|
||||
accepted: "must be accepted"
|
||||
empty: "can't be empty"
|
||||
blank: "can't be blank"
|
||||
too_long: "is too long (maximum is %{count} characters)"
|
||||
too_short: "is too short (minimum is %{count} characters)"
|
||||
wrong_length: "is the wrong length (should be %{count} characters)"
|
||||
taken: "has already been taken"
|
||||
not_a_number: "is not a number"
|
||||
greater_than: "must be greater than %{count}"
|
||||
greater_than_or_equal_to: "must be greater than or equal to %{count}"
|
||||
equal_to: "must be equal to %{count}"
|
||||
less_than: "must be less than %{count}"
|
||||
less_than_or_equal_to: "must be less than or equal to %{count}"
|
||||
odd: "must be odd"
|
||||
even: "must be even"
|
||||
record_invalid: "Validation failed: %{errors}"
|
||||
# Append your own errors here or at the model/attributes scope.
|
||||
|
||||
full_messages:
|
||||
format: "%{attribute} %{message}"
|
||||
|
||||
# You can define own errors for models or model attributes.
|
||||
# The values :model, :attribute and :value are always available for interpolation.
|
||||
#
|
||||
# For example,
|
||||
# models:
|
||||
# user:
|
||||
# blank: "This is a custom blank message for %{model}: %{attribute}"
|
||||
# attributes:
|
||||
# login:
|
||||
# blank: "This is a custom blank message for User login"
|
||||
# Will define custom blank validation message for User model and
|
||||
# custom blank validation message for login attribute of User model.
|
||||
#models:
|
||||
|
||||
# Translate model names. Used in Model.human_name().
|
||||
#models:
|
||||
# For example,
|
||||
# user: "Dude"
|
||||
# will translate User model name to "Dude"
|
||||
|
||||
# Translate model attribute names. Used in Model.human_attribute_name(attribute).
|
||||
#attributes:
|
||||
# For example,
|
||||
# user:
|
||||
# login: "Handle"
|
||||
# will translate User attribute "login" as "Handle"
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Locking
|
||||
# == What is Optimistic Locking
|
||||
#
|
||||
# Optimistic locking allows multiple users to access the same record for edits, and assumes a minimum of
|
||||
# conflicts with the data. It does this by checking whether another process has made changes to a record since
|
||||
# it was opened, an ActiveRecord::StaleObjectError is thrown if that has occurred and the update is ignored.
|
||||
#
|
||||
# Check out ActiveRecord::Locking::Pessimistic for an alternative.
|
||||
#
|
||||
# == Usage
|
||||
#
|
||||
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
|
||||
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
|
||||
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
|
||||
#
|
||||
# p1 = Person.find(1)
|
||||
# p2 = Person.find(1)
|
||||
#
|
||||
# p1.first_name = "Michael"
|
||||
# p1.save
|
||||
#
|
||||
# p2.first_name = "should fail"
|
||||
# p2.save # Raises a ActiveRecord::StaleObjectError
|
||||
#
|
||||
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
||||
# or otherwise apply the business logic needed to resolve the conflict.
|
||||
#
|
||||
# You must ensure that your database schema defaults the lock_version column to 0.
|
||||
#
|
||||
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
|
||||
# To override the name of the lock_version column, invoke the <tt>set_locking_column</tt> method.
|
||||
# This method uses the same syntax as <tt>set_table_name</tt>
|
||||
module Optimistic
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend ClassMethods
|
||||
|
||||
base.cattr_accessor :lock_optimistically, :instance_writer => false
|
||||
base.lock_optimistically = true
|
||||
|
||||
base.alias_method_chain :update, :lock
|
||||
base.alias_method_chain :attributes_from_column_definition, :lock
|
||||
|
||||
class << base
|
||||
alias_method :locking_column=, :set_locking_column
|
||||
end
|
||||
end
|
||||
|
||||
def locking_enabled? #:nodoc:
|
||||
self.class.locking_enabled?
|
||||
end
|
||||
|
||||
private
|
||||
def attributes_from_column_definition_with_lock
|
||||
result = attributes_from_column_definition_without_lock
|
||||
|
||||
# If the locking column has no default value set,
|
||||
# start the lock version at zero. Note we can't use
|
||||
# locking_enabled? at this point as @attributes may
|
||||
# not have been initialized yet
|
||||
|
||||
if lock_optimistically && result.include?(self.class.locking_column)
|
||||
result[self.class.locking_column] ||= 0
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
|
||||
return update_without_lock(attribute_names) unless locking_enabled?
|
||||
return 0 if attribute_names.empty?
|
||||
|
||||
lock_col = self.class.locking_column
|
||||
previous_value = send(lock_col).to_i
|
||||
send(lock_col + '=', previous_value + 1)
|
||||
|
||||
attribute_names += [lock_col]
|
||||
attribute_names.uniq!
|
||||
|
||||
begin
|
||||
affected_rows = connection.update(<<-end_sql, "#{self.class.name} Update with optimistic locking")
|
||||
UPDATE #{self.class.quoted_table_name}
|
||||
SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false, attribute_names))}
|
||||
WHERE #{self.class.primary_key} = #{quote_value(id)}
|
||||
AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}
|
||||
end_sql
|
||||
|
||||
unless affected_rows == 1
|
||||
raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
|
||||
end
|
||||
|
||||
affected_rows
|
||||
|
||||
# If something went wrong, revert the version.
|
||||
rescue Exception
|
||||
send(lock_col + '=', previous_value)
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
DEFAULT_LOCKING_COLUMN = 'lock_version'
|
||||
|
||||
def self.extended(base)
|
||||
class <<base
|
||||
alias_method_chain :update_counters, :lock
|
||||
end
|
||||
end
|
||||
|
||||
# Is optimistic locking enabled for this table? Returns true if the
|
||||
# +lock_optimistically+ flag is set to true (which it is, by default)
|
||||
# and the table includes the +locking_column+ column (defaults to
|
||||
# +lock_version+).
|
||||
def locking_enabled?
|
||||
lock_optimistically && columns_hash[locking_column]
|
||||
end
|
||||
|
||||
# Set the column to use for optimistic locking. Defaults to +lock_version+.
|
||||
def set_locking_column(value = nil, &block)
|
||||
define_attr_method :locking_column, value, &block
|
||||
value
|
||||
end
|
||||
|
||||
# The version column used for optimistic locking. Defaults to +lock_version+.
|
||||
def locking_column
|
||||
reset_locking_column
|
||||
end
|
||||
|
||||
# Quote the column name used for optimistic locking.
|
||||
def quoted_locking_column
|
||||
connection.quote_column_name(locking_column)
|
||||
end
|
||||
|
||||
# Reset the column used for optimistic locking back to the +lock_version+ default.
|
||||
def reset_locking_column
|
||||
set_locking_column DEFAULT_LOCKING_COLUMN
|
||||
end
|
||||
|
||||
# Make sure the lock version column gets updated when counters are
|
||||
# updated.
|
||||
def update_counters_with_lock(id, counters)
|
||||
counters = counters.merge(locking_column => 1) if locking_enabled?
|
||||
update_counters_without_lock(id, counters)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Locking
|
||||
# Locking::Pessimistic provides support for row-level locking using
|
||||
# SELECT ... FOR UPDATE and other lock types.
|
||||
#
|
||||
# Pass <tt>:lock => true</tt> to ActiveRecord::Base.find to obtain an exclusive
|
||||
# lock on the selected rows:
|
||||
# # select * from accounts where id=1 for update
|
||||
# Account.find(1, :lock => true)
|
||||
#
|
||||
# Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
|
||||
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'.
|
||||
#
|
||||
# Example:
|
||||
# Account.transaction do
|
||||
# # select * from accounts where name = 'shugo' limit 1 for update
|
||||
# shugo = Account.find(:first, :conditions => "name = 'shugo'", :lock => true)
|
||||
# yuko = Account.find(:first, :conditions => "name = 'yuko'", :lock => true)
|
||||
# shugo.balance -= 100
|
||||
# shugo.save!
|
||||
# yuko.balance += 100
|
||||
# yuko.save!
|
||||
# end
|
||||
#
|
||||
# You can also use ActiveRecord::Base#lock! method to lock one record by id.
|
||||
# This may be better if you don't need to lock every row. Example:
|
||||
# Account.transaction do
|
||||
# # select * from accounts where ...
|
||||
# accounts = Account.find(:all, :conditions => ...)
|
||||
# account1 = accounts.detect { |account| ... }
|
||||
# account2 = accounts.detect { |account| ... }
|
||||
# # select * from accounts where id=? for update
|
||||
# account1.lock!
|
||||
# account2.lock!
|
||||
# account1.balance -= 100
|
||||
# account1.save!
|
||||
# account2.balance += 100
|
||||
# account2.save!
|
||||
# end
|
||||
#
|
||||
# Database-specific information on row locking:
|
||||
# MySQL: http://dev.mysql.com/doc/refman/5.1/en/innodb-locking-reads.html
|
||||
# PostgreSQL: http://www.postgresql.org/docs/8.1/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE
|
||||
module Pessimistic
|
||||
# Obtain a row lock on this record. Reloads the record to obtain the requested
|
||||
# lock. Pass an SQL locking clause to append the end of the SELECT statement
|
||||
# or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns
|
||||
# the locked record.
|
||||
def lock!(lock = true)
|
||||
reload(:lock => lock) unless new_record?
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,566 +0,0 @@
|
||||
module ActiveRecord
|
||||
class IrreversibleMigration < ActiveRecordError#:nodoc:
|
||||
end
|
||||
|
||||
class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
|
||||
def initialize(version)
|
||||
super("Multiple migrations have the version number #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
|
||||
def initialize(name)
|
||||
super("Multiple migrations have the name #{name}")
|
||||
end
|
||||
end
|
||||
|
||||
class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
|
||||
def initialize(version)
|
||||
super("No migration with version number #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
class IllegalMigrationNameError < ActiveRecordError#:nodoc:
|
||||
def initialize(name)
|
||||
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
|
||||
end
|
||||
end
|
||||
|
||||
# Migrations can manage the evolution of a schema used by several physical databases. It's a solution
|
||||
# to the common problem of adding a field to make a new feature work in your local database, but being unsure of how to
|
||||
# push that change to other developers and to the production server. With migrations, you can describe the transformations
|
||||
# in self-contained classes that can be checked into version control systems and executed against another database that
|
||||
# might be one, two, or five versions behind.
|
||||
#
|
||||
# Example of a simple migration:
|
||||
#
|
||||
# class AddSsl < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# add_column :accounts, :ssl_enabled, :boolean, :default => 1
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# remove_column :accounts, :ssl_enabled
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This migration will add a boolean flag to the accounts table and remove it if you're backing out of the migration.
|
||||
# It shows how all migrations have two class methods +up+ and +down+ that describes the transformations required to implement
|
||||
# or remove the migration. These methods can consist of both the migration specific methods like add_column and remove_column,
|
||||
# but may also contain regular Ruby code for generating data needed for the transformations.
|
||||
#
|
||||
# Example of a more complex migration that also needs to initialize data:
|
||||
#
|
||||
# class AddSystemSettings < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# create_table :system_settings do |t|
|
||||
# t.string :name
|
||||
# t.string :label
|
||||
# t.text :value
|
||||
# t.string :type
|
||||
# t.integer :position
|
||||
# end
|
||||
#
|
||||
# SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# drop_table :system_settings
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This migration first adds the system_settings table, then creates the very first row in it using the Active Record model
|
||||
# that relies on the table. It also uses the more advanced create_table syntax where you can specify a complete table schema
|
||||
# in one block call.
|
||||
#
|
||||
# == Available transformations
|
||||
#
|
||||
# * <tt>create_table(name, options)</tt> Creates a table called +name+ and makes the table object available to a block
|
||||
# that can then add columns to it, following the same format as add_column. See example above. The options hash is for
|
||||
# fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create table definition.
|
||||
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
|
||||
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ to +new_name+.
|
||||
# * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column to the table called +table_name+
|
||||
# named +column_name+ specified to be one of the following types:
|
||||
# <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
|
||||
# <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be specified by passing an
|
||||
# +options+ hash like <tt>{ :default => 11 }</tt>. Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. <tt>{ :limit => 50, :null => false }</tt>)
|
||||
# -- see ActiveRecord::ConnectionAdapters::TableDefinition#column for details.
|
||||
# * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames a column but keeps the type and content.
|
||||
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes the column to a different type using the same
|
||||
# parameters as add_column.
|
||||
# * <tt>remove_column(table_name, column_name)</tt>: Removes the column named +column_name+ from the table called +table_name+.
|
||||
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index with the name of the column. Other options include
|
||||
# <tt>:name</tt> and <tt>:unique</tt> (e.g. <tt>{ :name => "users_name_index", :unique => true }</tt>).
|
||||
# * <tt>remove_index(table_name, index_name)</tt>: Removes the index specified by +index_name+.
|
||||
#
|
||||
# == Irreversible transformations
|
||||
#
|
||||
# Some transformations are destructive in a manner that cannot be reversed. Migrations of that kind should raise
|
||||
# an <tt>ActiveRecord::IrreversibleMigration</tt> exception in their +down+ method.
|
||||
#
|
||||
# == Running migrations from within Rails
|
||||
#
|
||||
# The Rails package has several tools to help create and apply migrations.
|
||||
#
|
||||
# To generate a new migration, you can use
|
||||
# script/generate migration MyNewMigration
|
||||
#
|
||||
# where MyNewMigration is the name of your migration. The generator will
|
||||
# create an empty migration file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
|
||||
# directory where <tt>nnn</tt> is the next largest migration number.
|
||||
#
|
||||
# You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
|
||||
# MyNewMigration.
|
||||
#
|
||||
# There is a special syntactic shortcut to generate migrations that add fields to a table.
|
||||
# script/generate migration add_fieldname_to_tablename fieldname:string
|
||||
#
|
||||
# This will generate the file <tt>nnn_add_fieldname_to_tablename</tt>, which will look like this:
|
||||
# class AddFieldnameToTablename < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# add_column :tablenames, :fieldname, :string
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# remove_column :tablenames, :fieldname
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# To run migrations against the currently configured database, use
|
||||
# <tt>rake db:migrate</tt>. This will update the database by running all of the
|
||||
# pending migrations, creating the <tt>schema_migrations</tt> table
|
||||
# (see "About the schema_migrations table" section below) if missing. It will also
|
||||
# invoke the db:schema:dump task, which will update your db/schema.rb file
|
||||
# to match the structure of your database.
|
||||
#
|
||||
# To roll the database back to a previous migration version, use
|
||||
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
|
||||
# you wish to downgrade. If any of the migrations throw an
|
||||
# <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
|
||||
# have some manual work to do.
|
||||
#
|
||||
# == Database support
|
||||
#
|
||||
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
|
||||
# SQL Server, Sybase, and Oracle (all supported databases except DB2).
|
||||
#
|
||||
# == More examples
|
||||
#
|
||||
# Not all migrations change the schema. Some just fix the data:
|
||||
#
|
||||
# class RemoveEmptyTags < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# Tag.find(:all).each { |tag| tag.destroy if tag.pages.empty? }
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# # not much we can do to restore deleted data
|
||||
# raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Others remove columns when they migrate up instead of down:
|
||||
#
|
||||
# class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# remove_column :items, :incomplete_items_count
|
||||
# remove_column :items, :completed_items_count
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# add_column :items, :incomplete_items_count
|
||||
# add_column :items, :completed_items_count
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# And sometimes you need to do something in SQL not abstracted directly by migrations:
|
||||
#
|
||||
# class MakeJoinUnique < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Using a model after changing its table
|
||||
#
|
||||
# Sometimes you'll want to add a column in a migration and populate it immediately after. In that case, you'll need
|
||||
# to make a call to Base#reset_column_information in order to ensure that the model has the latest column data from
|
||||
# after the new column was added. Example:
|
||||
#
|
||||
# class AddPeopleSalary < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# add_column :people, :salary, :integer
|
||||
# Person.reset_column_information
|
||||
# Person.find(:all).each do |p|
|
||||
# p.update_attribute :salary, SalaryCalculator.compute(p)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Controlling verbosity
|
||||
#
|
||||
# By default, migrations will describe the actions they are taking, writing
|
||||
# them to the console as they happen, along with benchmarks describing how
|
||||
# long each step took.
|
||||
#
|
||||
# You can quiet them down by setting ActiveRecord::Migration.verbose = false.
|
||||
#
|
||||
# You can also insert your own messages and benchmarks by using the +say_with_time+
|
||||
# method:
|
||||
#
|
||||
# def self.up
|
||||
# ...
|
||||
# say_with_time "Updating salaries..." do
|
||||
# Person.find(:all).each do |p|
|
||||
# p.update_attribute :salary, SalaryCalculator.compute(p)
|
||||
# end
|
||||
# end
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# The phrase "Updating salaries..." would then be printed, along with the
|
||||
# benchmark for the block when the block completes.
|
||||
#
|
||||
# == About the schema_migrations table
|
||||
#
|
||||
# Rails versions 2.0 and prior used to create a table called
|
||||
# <tt>schema_info</tt> when using migrations. This table contained the
|
||||
# version of the schema as of the last applied migration.
|
||||
#
|
||||
# Starting with Rails 2.1, the <tt>schema_info</tt> table is
|
||||
# (automatically) replaced by the <tt>schema_migrations</tt> table, which
|
||||
# contains the version numbers of all the migrations applied.
|
||||
#
|
||||
# As a result, it is now possible to add migration files that are numbered
|
||||
# lower than the current schema version: when migrating up, those
|
||||
# never-applied "interleaved" migrations will be automatically applied, and
|
||||
# when migrating down, never-applied "interleaved" migrations will be skipped.
|
||||
#
|
||||
# == Timestamped Migrations
|
||||
#
|
||||
# By default, Rails generates migrations that look like:
|
||||
#
|
||||
# 20080717013526_your_migration_name.rb
|
||||
#
|
||||
# The prefix is a generation timestamp (in UTC).
|
||||
#
|
||||
# If you'd prefer to use numeric prefixes, you can turn timestamped migrations
|
||||
# off by setting:
|
||||
#
|
||||
# config.active_record.timestamped_migrations = false
|
||||
#
|
||||
# In environment.rb.
|
||||
#
|
||||
class Migration
|
||||
@@verbose = true
|
||||
cattr_accessor :verbose
|
||||
|
||||
class << self
|
||||
def up_with_benchmarks #:nodoc:
|
||||
migrate(:up)
|
||||
end
|
||||
|
||||
def down_with_benchmarks #:nodoc:
|
||||
migrate(:down)
|
||||
end
|
||||
|
||||
# Execute this migration in the named direction
|
||||
def migrate(direction)
|
||||
return unless respond_to?(direction)
|
||||
|
||||
case direction
|
||||
when :up then announce "migrating"
|
||||
when :down then announce "reverting"
|
||||
end
|
||||
|
||||
result = nil
|
||||
time = Benchmark.measure { result = send("#{direction}_without_benchmarks") }
|
||||
|
||||
case direction
|
||||
when :up then announce "migrated (%.4fs)" % time.real; write
|
||||
when :down then announce "reverted (%.4fs)" % time.real; write
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Because the method added may do an alias_method, it can be invoked
|
||||
# recursively. We use @ignore_new_methods as a guard to indicate whether
|
||||
# it is safe for the call to proceed.
|
||||
def singleton_method_added(sym) #:nodoc:
|
||||
return if defined?(@ignore_new_methods) && @ignore_new_methods
|
||||
|
||||
begin
|
||||
@ignore_new_methods = true
|
||||
|
||||
case sym
|
||||
when :up, :down
|
||||
klass = (class << self; self; end)
|
||||
klass.send(:alias_method_chain, sym, "benchmarks")
|
||||
end
|
||||
ensure
|
||||
@ignore_new_methods = false
|
||||
end
|
||||
end
|
||||
|
||||
def write(text="")
|
||||
puts(text) if verbose
|
||||
end
|
||||
|
||||
def announce(message)
|
||||
text = "#{@version} #{name}: #{message}"
|
||||
length = [0, 75 - text.length].max
|
||||
write "== %s %s" % [text, "=" * length]
|
||||
end
|
||||
|
||||
def say(message, subitem=false)
|
||||
write "#{subitem ? " ->" : "--"} #{message}"
|
||||
end
|
||||
|
||||
def say_with_time(message)
|
||||
say(message)
|
||||
result = nil
|
||||
time = Benchmark.measure { result = yield }
|
||||
say "%.4fs" % time.real, :subitem
|
||||
say("#{result} rows", :subitem) if result.is_a?(Integer)
|
||||
result
|
||||
end
|
||||
|
||||
def suppress_messages
|
||||
save, self.verbose = verbose, false
|
||||
yield
|
||||
ensure
|
||||
self.verbose = save
|
||||
end
|
||||
|
||||
def connection
|
||||
ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
def method_missing(method, *arguments, &block)
|
||||
arg_list = arguments.map(&:inspect) * ', '
|
||||
|
||||
say_with_time "#{method}(#{arg_list})" do
|
||||
unless arguments.empty? || method == :execute
|
||||
arguments[0] = Migrator.proper_table_name(arguments.first)
|
||||
end
|
||||
connection.send(method, *arguments, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# MigrationProxy is used to defer loading of the actual migration classes
|
||||
# until they are needed
|
||||
class MigrationProxy
|
||||
|
||||
attr_accessor :name, :version, :filename
|
||||
|
||||
delegate :migrate, :announce, :write, :to=>:migration
|
||||
|
||||
private
|
||||
|
||||
def migration
|
||||
@migration ||= load_migration
|
||||
end
|
||||
|
||||
def load_migration
|
||||
load(filename)
|
||||
name.constantize
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class Migrator#:nodoc:
|
||||
class << self
|
||||
def migrate(migrations_path, target_version = nil)
|
||||
case
|
||||
when target_version.nil? then up(migrations_path, target_version)
|
||||
when current_version > target_version then down(migrations_path, target_version)
|
||||
else up(migrations_path, target_version)
|
||||
end
|
||||
end
|
||||
|
||||
def rollback(migrations_path, steps=1)
|
||||
migrator = self.new(:down, migrations_path)
|
||||
start_index = migrator.migrations.index(migrator.current_migration)
|
||||
|
||||
return unless start_index
|
||||
|
||||
finish = migrator.migrations[start_index + steps]
|
||||
down(migrations_path, finish ? finish.version : 0)
|
||||
end
|
||||
|
||||
def up(migrations_path, target_version = nil)
|
||||
self.new(:up, migrations_path, target_version).migrate
|
||||
end
|
||||
|
||||
def down(migrations_path, target_version = nil)
|
||||
self.new(:down, migrations_path, target_version).migrate
|
||||
end
|
||||
|
||||
def run(direction, migrations_path, target_version)
|
||||
self.new(direction, migrations_path, target_version).run
|
||||
end
|
||||
|
||||
def schema_migrations_table_name
|
||||
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
|
||||
end
|
||||
|
||||
def get_all_versions
|
||||
Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
|
||||
end
|
||||
|
||||
def current_version
|
||||
sm_table = schema_migrations_table_name
|
||||
if Base.connection.table_exists?(sm_table)
|
||||
get_all_versions.max || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def proper_table_name(name)
|
||||
# Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
|
||||
name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(direction, migrations_path, target_version = nil)
|
||||
raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations?
|
||||
Base.connection.initialize_schema_migrations_table
|
||||
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
|
||||
end
|
||||
|
||||
def current_version
|
||||
migrated.last || 0
|
||||
end
|
||||
|
||||
def current_migration
|
||||
migrations.detect { |m| m.version == current_version }
|
||||
end
|
||||
|
||||
def run
|
||||
target = migrations.detect { |m| m.version == @target_version }
|
||||
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
|
||||
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
|
||||
target.migrate(@direction)
|
||||
record_version_state_after_migrating(target.version)
|
||||
end
|
||||
end
|
||||
|
||||
def migrate
|
||||
current = migrations.detect { |m| m.version == current_version }
|
||||
target = migrations.detect { |m| m.version == @target_version }
|
||||
|
||||
if target.nil? && !@target_version.nil? && @target_version > 0
|
||||
raise UnknownMigrationVersionError.new(@target_version)
|
||||
end
|
||||
|
||||
start = up? ? 0 : (migrations.index(current) || 0)
|
||||
finish = migrations.index(target) || migrations.size - 1
|
||||
runnable = migrations[start..finish]
|
||||
|
||||
# skip the last migration if we're headed down, but not ALL the way down
|
||||
runnable.pop if down? && !target.nil?
|
||||
|
||||
runnable.each do |migration|
|
||||
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
|
||||
|
||||
# On our way up, we skip migrating the ones we've already migrated
|
||||
next if up? && migrated.include?(migration.version.to_i)
|
||||
|
||||
# On our way down, we skip reverting the ones we've never migrated
|
||||
if down? && !migrated.include?(migration.version.to_i)
|
||||
migration.announce 'never migrated, skipping'; migration.write
|
||||
next
|
||||
end
|
||||
|
||||
begin
|
||||
ddl_transaction do
|
||||
migration.migrate(@direction)
|
||||
record_version_state_after_migrating(migration.version)
|
||||
end
|
||||
rescue => e
|
||||
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
|
||||
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def migrations
|
||||
@migrations ||= begin
|
||||
files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
|
||||
|
||||
migrations = files.inject([]) do |klasses, file|
|
||||
version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
|
||||
|
||||
raise IllegalMigrationNameError.new(file) unless version
|
||||
version = version.to_i
|
||||
|
||||
if klasses.detect { |m| m.version == version }
|
||||
raise DuplicateMigrationVersionError.new(version)
|
||||
end
|
||||
|
||||
if klasses.detect { |m| m.name == name.camelize }
|
||||
raise DuplicateMigrationNameError.new(name.camelize)
|
||||
end
|
||||
|
||||
klasses << returning(MigrationProxy.new) do |migration|
|
||||
migration.name = name.camelize
|
||||
migration.version = version
|
||||
migration.filename = file
|
||||
end
|
||||
end
|
||||
|
||||
migrations = migrations.sort_by(&:version)
|
||||
down? ? migrations.reverse : migrations
|
||||
end
|
||||
end
|
||||
|
||||
def pending_migrations
|
||||
already_migrated = migrated
|
||||
migrations.reject { |m| already_migrated.include?(m.version.to_i) }
|
||||
end
|
||||
|
||||
def migrated
|
||||
@migrated_versions ||= self.class.get_all_versions
|
||||
end
|
||||
|
||||
private
|
||||
def record_version_state_after_migrating(version)
|
||||
sm_table = self.class.schema_migrations_table_name
|
||||
|
||||
@migrated_versions ||= []
|
||||
if down?
|
||||
@migrated_versions.delete(version.to_i)
|
||||
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
|
||||
else
|
||||
@migrated_versions.push(version.to_i).sort!
|
||||
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
|
||||
end
|
||||
end
|
||||
|
||||
def up?
|
||||
@direction == :up
|
||||
end
|
||||
|
||||
def down?
|
||||
@direction == :down
|
||||
end
|
||||
|
||||
# Wrap the migration in a transaction only if supported by the adapter.
|
||||
def ddl_transaction(&block)
|
||||
if Base.connection.supports_ddl_transactions?
|
||||
Base.transaction { block.call }
|
||||
else
|
||||
block.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,192 +0,0 @@
|
||||
module ActiveRecord
|
||||
module NamedScope
|
||||
# All subclasses of ActiveRecord::Base have one named scope:
|
||||
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
|
||||
#
|
||||
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
|
||||
# intermediate values (scopes) around as first-class objects is convenient.
|
||||
#
|
||||
# You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
extend ClassMethods
|
||||
named_scope :scoped, lambda { |scope| scope }
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def scopes
|
||||
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
||||
end
|
||||
|
||||
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
|
||||
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'}
|
||||
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
|
||||
# end
|
||||
#
|
||||
# The above calls to <tt>named_scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
||||
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
|
||||
#
|
||||
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it resembles the association object
|
||||
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
|
||||
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
|
||||
# as with the association objects, named \scopes act like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
|
||||
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really was an Array.
|
||||
#
|
||||
# These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
|
||||
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
|
||||
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
||||
#
|
||||
# All \scopes are available as class methods on the ActiveRecord::Base descendant upon which the \scopes were defined. But they are also available to
|
||||
# <tt>has_many</tt> associations. If,
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# has_many :shirts
|
||||
# end
|
||||
#
|
||||
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
||||
# only shirts.
|
||||
#
|
||||
# Named \scopes can also be procedural:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
||||
#
|
||||
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :red, :conditions => {:color => 'red'} do
|
||||
# def dom_id
|
||||
# 'red_shirts'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
#
|
||||
# For testing complex named \scopes, you can examine the scoping options using the
|
||||
# <tt>proxy_options</tt> method on the proxy itself.
|
||||
#
|
||||
# class Shirt < ActiveRecord::Base
|
||||
# named_scope :colored, lambda { |color|
|
||||
# { :conditions => { :color => color } }
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# expected_options = { :conditions => { :colored => 'red' } }
|
||||
# assert_equal expected_options, Shirt.colored('red').proxy_options
|
||||
def named_scope(name, options = {}, &block)
|
||||
name = name.to_sym
|
||||
scopes[name] = lambda do |parent_scope, *args|
|
||||
Scope.new(parent_scope, case options
|
||||
when Hash
|
||||
options
|
||||
when Proc
|
||||
options.call(*args)
|
||||
end, &block)
|
||||
end
|
||||
(class << self; self end).instance_eval do
|
||||
define_method name do |*args|
|
||||
scopes[name].call(self, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Scope
|
||||
attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
|
||||
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
|
||||
[].methods.each do |m|
|
||||
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
||||
delegate m, :to => :proxy_found
|
||||
end
|
||||
end
|
||||
|
||||
delegate :scopes, :with_scope, :scoped_methods, :to => :proxy_scope
|
||||
|
||||
def initialize(proxy_scope, options, &block)
|
||||
options ||= {}
|
||||
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
|
||||
extend Module.new(&block) if block_given?
|
||||
unless Scope === proxy_scope
|
||||
@current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
|
||||
end
|
||||
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
|
||||
end
|
||||
|
||||
def reload
|
||||
load_found; self
|
||||
end
|
||||
|
||||
def first(*args)
|
||||
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
||||
proxy_found.first(*args)
|
||||
else
|
||||
find(:first, *args)
|
||||
end
|
||||
end
|
||||
|
||||
def last(*args)
|
||||
if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
|
||||
proxy_found.last(*args)
|
||||
else
|
||||
find(:last, *args)
|
||||
end
|
||||
end
|
||||
|
||||
def size
|
||||
@found ? @found.length : count
|
||||
end
|
||||
|
||||
def empty?
|
||||
@found ? @found.empty? : count.zero?
|
||||
end
|
||||
|
||||
def respond_to?(method, include_private = false)
|
||||
super || @proxy_scope.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
def any?
|
||||
if block_given?
|
||||
proxy_found.any? { |*block_args| yield(*block_args) }
|
||||
else
|
||||
!empty?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def proxy_found
|
||||
@found || load_found
|
||||
end
|
||||
|
||||
private
|
||||
def method_missing(method, *args, &block)
|
||||
if scopes.include?(method)
|
||||
scopes[method].call(self, *args)
|
||||
else
|
||||
with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
|
||||
method = :new if method == :build
|
||||
if current_scoped_methods_when_defined && !scoped_methods.include?(current_scoped_methods_when_defined)
|
||||
with_scope current_scoped_methods_when_defined do
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
else
|
||||
proxy_scope.send(method, *args, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_found
|
||||
@found = find(:all)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,392 +0,0 @@
|
||||
module ActiveRecord
|
||||
module NestedAttributes #:nodoc:
|
||||
class TooManyRecords < ActiveRecordError
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
|
||||
base.nested_attributes_options = {}
|
||||
end
|
||||
|
||||
# == Nested Attributes
|
||||
#
|
||||
# Nested attributes allow you to save attributes on associated records
|
||||
# through the parent. By default nested attribute updating is turned off,
|
||||
# you can enable it using the accepts_nested_attributes_for class method.
|
||||
# When you enable nested attributes an attribute writer is defined on
|
||||
# the model.
|
||||
#
|
||||
# The attribute writer is named after the association, which means that
|
||||
# in the following example, two new methods are added to your model:
|
||||
# <tt>author_attributes=(attributes)</tt> and
|
||||
# <tt>pages_attributes=(attributes)</tt>.
|
||||
#
|
||||
# class Book < ActiveRecord::Base
|
||||
# has_one :author
|
||||
# has_many :pages
|
||||
#
|
||||
# accepts_nested_attributes_for :author, :pages
|
||||
# end
|
||||
#
|
||||
# Note that the <tt>:autosave</tt> option is automatically enabled on every
|
||||
# association that accepts_nested_attributes_for is used for.
|
||||
#
|
||||
# === One-to-one
|
||||
#
|
||||
# Consider a Member model that has one Avatar:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_one :avatar
|
||||
# accepts_nested_attributes_for :avatar
|
||||
# end
|
||||
#
|
||||
# Enabling nested attributes on a one-to-one association allows you to
|
||||
# create the member and avatar in one go:
|
||||
#
|
||||
# params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
|
||||
# member = Member.create(params)
|
||||
# member.avatar.id # => 2
|
||||
# member.avatar.icon # => 'smiling'
|
||||
#
|
||||
# It also allows you to update the avatar through the member:
|
||||
#
|
||||
# params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
|
||||
# member.update_attributes params['member']
|
||||
# member.avatar.icon # => 'sad'
|
||||
#
|
||||
# By default you will only be able to set and update attributes on the
|
||||
# associated model. If you want to destroy the associated model through the
|
||||
# attributes hash, you have to enable it first using the
|
||||
# <tt>:allow_destroy</tt> option.
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_one :avatar
|
||||
# accepts_nested_attributes_for :avatar, :allow_destroy => true
|
||||
# end
|
||||
#
|
||||
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
|
||||
# value that evaluates to +true+, you will destroy the associated model:
|
||||
#
|
||||
# member.avatar_attributes = { :id => '2', :_destroy => '1' }
|
||||
# member.avatar.marked_for_destruction? # => true
|
||||
# member.save
|
||||
# member.avatar #=> nil
|
||||
#
|
||||
# Note that the model will _not_ be destroyed until the parent is saved.
|
||||
#
|
||||
# === One-to-many
|
||||
#
|
||||
# Consider a member that has a number of posts:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts
|
||||
# end
|
||||
#
|
||||
# You can now set or update attributes on an associated post model through
|
||||
# the attribute hash.
|
||||
#
|
||||
# For each hash that does _not_ have an <tt>id</tt> key a new record will
|
||||
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
|
||||
# that evaluates to +true+.
|
||||
#
|
||||
# params = { :member => {
|
||||
# :name => 'joe', :posts_attributes => [
|
||||
# { :title => 'Kari, the awesome Ruby documentation browser!' },
|
||||
# { :title => 'The egalitarian assumption of the modern citizen' },
|
||||
# { :title => '', :_destroy => '1' } # this will be ignored
|
||||
# ]
|
||||
# }}
|
||||
#
|
||||
# member = Member.create(params['member'])
|
||||
# member.posts.length # => 2
|
||||
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
|
||||
#
|
||||
# You may also set a :reject_if proc to silently ignore any new record
|
||||
# hashes if they fail to pass your criteria. For example, the previous
|
||||
# example could be rewritten as:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
|
||||
# end
|
||||
#
|
||||
# params = { :member => {
|
||||
# :name => 'joe', :posts_attributes => [
|
||||
# { :title => 'Kari, the awesome Ruby documentation browser!' },
|
||||
# { :title => 'The egalitarian assumption of the modern citizen' },
|
||||
# { :title => '' } # this will be ignored because of the :reject_if proc
|
||||
# ]
|
||||
# }}
|
||||
#
|
||||
# member = Member.create(params['member'])
|
||||
# member.posts.length # => 2
|
||||
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
|
||||
#
|
||||
# Alternatively, :reject_if also accepts a symbol for using methods:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => :new_record?
|
||||
# end
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :reject_if => :reject_posts
|
||||
#
|
||||
# def reject_posts(attributed)
|
||||
# attributed['title].blank?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If the hash contains an <tt>id</tt> key that matches an already
|
||||
# associated record, the matching record will be modified:
|
||||
#
|
||||
# member.attributes = {
|
||||
# :name => 'Joe',
|
||||
# :posts_attributes => [
|
||||
# { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
|
||||
# { :id => 2, :title => '[UPDATED] other post' }
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
|
||||
# member.posts.second.title # => '[UPDATED] other post'
|
||||
#
|
||||
# By default the associated records are protected from being destroyed. If
|
||||
# you want to destroy any of the associated records through the attributes
|
||||
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
|
||||
# option. This will allow you to also use the <tt>_destroy</tt> key to
|
||||
# destroy existing records:
|
||||
#
|
||||
# class Member < ActiveRecord::Base
|
||||
# has_many :posts
|
||||
# accepts_nested_attributes_for :posts, :allow_destroy => true
|
||||
# end
|
||||
#
|
||||
# params = { :member => {
|
||||
# :posts_attributes => [{ :id => '2', :_destroy => '1' }]
|
||||
# }}
|
||||
#
|
||||
# member.attributes = params['member']
|
||||
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
|
||||
# member.posts.length #=> 2
|
||||
# member.save
|
||||
# member.posts.length # => 1
|
||||
#
|
||||
# === Saving
|
||||
#
|
||||
# All changes to models, including the destruction of those marked for
|
||||
# destruction, are saved and destroyed automatically and atomically when
|
||||
# the parent model is saved. This happens inside the transaction initiated
|
||||
# by the parents save method. See ActiveRecord::AutosaveAssociation.
|
||||
module ClassMethods
|
||||
# Defines an attributes writer for the specified association(s). If you
|
||||
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
|
||||
# will need to add the attribute writer to the allowed list.
|
||||
#
|
||||
# Supported options:
|
||||
# [:allow_destroy]
|
||||
# If true, destroys any members from the attributes hash with a
|
||||
# <tt>_destroy</tt> key and a value that evaluates to +true+
|
||||
# (eg. 1, '1', true, or 'true'). This option is off by default.
|
||||
# [:reject_if]
|
||||
# Allows you to specify a Proc or a Symbol pointing to a method
|
||||
# that checks whether a record should be built for a certain attribute
|
||||
# hash. The hash is passed to the supplied Proc or the method
|
||||
# and it should return either +true+ or +false+. When no :reject_if
|
||||
# is specified, a record will be built for all attribute hashes that
|
||||
# do not have a <tt>_destroy</tt> value that evaluates to true.
|
||||
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
|
||||
# that will reject a record where all the attributes are blank.
|
||||
# [:limit]
|
||||
# Allows you to specify the maximum number of the associated records that
|
||||
# can be processes with the nested attributes. If the size of the
|
||||
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
||||
# exception is raised. If omitted, any number associations can be processed.
|
||||
# Note that the :limit option is only applicable to one-to-many associations.
|
||||
#
|
||||
# Examples:
|
||||
# # creates avatar_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
|
||||
# # creates avatar_attributes= and posts_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
|
||||
def accepts_nested_attributes_for(*attr_names)
|
||||
options = { :allow_destroy => false }
|
||||
options.update(attr_names.extract_options!)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
type = case reflection.macro
|
||||
when :has_one, :belongs_to
|
||||
:one_to_one
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
:collection
|
||||
end
|
||||
|
||||
reflection.options[:autosave] = true
|
||||
self.nested_attributes_options[association_name.to_sym] = options
|
||||
|
||||
# def pirate_attributes=(attributes)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
|
||||
# end
|
||||
class_eval %{
|
||||
def #{association_name}_attributes=(attributes)
|
||||
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
|
||||
add_autosave_association_callbacks(reflection)
|
||||
else
|
||||
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
|
||||
# used in conjunction with fields_for to build a form element for the
|
||||
# destruction of this association.
|
||||
#
|
||||
# See ActionView::Helpers::FormHelper::fields_for for more info.
|
||||
def _destroy
|
||||
marked_for_destruction?
|
||||
end
|
||||
|
||||
# Deal with deprecated _delete.
|
||||
#
|
||||
def _delete #:nodoc:
|
||||
ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
|
||||
_destroy
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attribute hash keys that should not be assigned as normal attributes.
|
||||
# These hash keys are nested attributes implementation details.
|
||||
#
|
||||
# TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
|
||||
# removed.
|
||||
UNASSIGNABLE_KEYS = %w( id _destroy _delete )
|
||||
|
||||
# Assigns the given attributes to the association.
|
||||
#
|
||||
# If the given attributes include an <tt>:id</tt> that matches the existing
|
||||
# record’s id, then the existing record will be modified. Otherwise a new
|
||||
# record will be built.
|
||||
#
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
||||
# <tt>:_destroy</tt> key set to a truthy value, then the existing record
|
||||
# will be marked for destruction.
|
||||
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
attributes = attributes.with_indifferent_access
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
end
|
||||
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
end
|
||||
end
|
||||
|
||||
# Assigns the given attributes to the collection association.
|
||||
#
|
||||
# Hashes with an <tt>:id</tt> value matching an existing associated record
|
||||
# will update that record. Hashes without an <tt>:id</tt> value will build
|
||||
# a new record for the association. Hashes with a matching <tt>:id</tt>
|
||||
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
|
||||
# matched record for destruction.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# assign_nested_attributes_for_collection_association(:people, {
|
||||
# '1' => { :id => '1', :name => 'Peter' },
|
||||
# '2' => { :name => 'John' },
|
||||
# '3' => { :id => '2', :_destroy => true }
|
||||
# })
|
||||
#
|
||||
# Will update the name of the Person with ID 1, build a new associated
|
||||
# person with the name `John', and mark the associatied Person with ID 2
|
||||
# for destruction.
|
||||
#
|
||||
# Also accepts an Array of attribute hashes:
|
||||
#
|
||||
# assign_nested_attributes_for_collection_association(:people, [
|
||||
# { :id => '1', :name => 'Peter' },
|
||||
# { :name => 'John' },
|
||||
# { :id => '2', :_destroy => true }
|
||||
# ])
|
||||
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
|
||||
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
|
||||
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
|
||||
end
|
||||
|
||||
if options[:limit] && attributes_collection.size > options[:limit]
|
||||
raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
|
||||
end
|
||||
|
||||
if attributes_collection.is_a? Hash
|
||||
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
||||
end
|
||||
|
||||
attributes_collection.each do |attributes|
|
||||
attributes = attributes.with_indifferent_access
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
||||
end
|
||||
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Updates a record with the +attributes+ or marks it for destruction if
|
||||
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
|
||||
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
|
||||
if has_destroy_flag?(attributes) && allow_destroy
|
||||
record.mark_for_destruction
|
||||
else
|
||||
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
||||
end
|
||||
end
|
||||
|
||||
# Determines if a hash contains a truthy _destroy key.
|
||||
def has_destroy_flag?(hash)
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
|
||||
ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
|
||||
end
|
||||
|
||||
# Determines if a new record should be build by checking for
|
||||
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
|
||||
# association and evaluates to +true+.
|
||||
def reject_new_record?(association_name, attributes)
|
||||
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
|
||||
end
|
||||
|
||||
def call_reject_if(association_name, attributes)
|
||||
callback = self.nested_attributes_options[association_name][:reject_if]
|
||||
|
||||
case callback
|
||||
when Symbol
|
||||
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
|
||||
when Proc
|
||||
callback.try(:call, attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,197 +0,0 @@
|
||||
require 'singleton'
|
||||
require 'set'
|
||||
|
||||
module ActiveRecord
|
||||
module Observing # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Activates the observers assigned. Examples:
|
||||
#
|
||||
# # Calls PersonObserver.instance
|
||||
# ActiveRecord::Base.observers = :person_observer
|
||||
#
|
||||
# # Calls Cacher.instance and GarbageCollector.instance
|
||||
# ActiveRecord::Base.observers = :cacher, :garbage_collector
|
||||
#
|
||||
# # Same as above, just using explicit class references
|
||||
# ActiveRecord::Base.observers = Cacher, GarbageCollector
|
||||
#
|
||||
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
|
||||
# called during startup, and before each development request.
|
||||
def observers=(*observers)
|
||||
@observers = observers.flatten
|
||||
end
|
||||
|
||||
# Gets the current observers.
|
||||
def observers
|
||||
@observers ||= []
|
||||
end
|
||||
|
||||
# Instantiate the global Active Record observers.
|
||||
def instantiate_observers
|
||||
return if @observers.blank?
|
||||
@observers.each do |observer|
|
||||
if observer.respond_to?(:to_sym) # Symbol or String
|
||||
observer.to_s.camelize.constantize.instance
|
||||
elsif observer.respond_to?(:instance)
|
||||
observer.instance
|
||||
else
|
||||
raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Notify observers when the observed class is subclassed.
|
||||
def inherited(subclass)
|
||||
super
|
||||
changed
|
||||
notify_observers :observed_class_inherited, subclass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Observer classes respond to lifecycle callbacks to implement trigger-like
|
||||
# behavior outside the original class. This is a great way to reduce the
|
||||
# clutter that normally comes when the model class is burdened with
|
||||
# functionality that doesn't pertain to the core responsibility of the
|
||||
# class. Example:
|
||||
#
|
||||
# class CommentObserver < ActiveRecord::Observer
|
||||
# def after_save(comment)
|
||||
# Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer sends an email when a Comment#save is finished.
|
||||
#
|
||||
# class ContactObserver < ActiveRecord::Observer
|
||||
# def after_create(contact)
|
||||
# contact.logger.info('New contact added!')
|
||||
# end
|
||||
#
|
||||
# def after_destroy(contact)
|
||||
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This Observer uses logger to log when specific callbacks are triggered.
|
||||
#
|
||||
# == Observing a class that can't be inferred
|
||||
#
|
||||
# Observers will by default be mapped to the class with which they share a name. So CommentObserver will
|
||||
# be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
|
||||
# differently than the class you're interested in observing, you can use the Observer.observe class method which takes
|
||||
# either the concrete class (Product) or a symbol for that class (:product):
|
||||
#
|
||||
# class AuditObserver < ActiveRecord::Observer
|
||||
# observe :account
|
||||
#
|
||||
# def after_update(account)
|
||||
# AuditTrail.new(account, "UPDATED")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
|
||||
#
|
||||
# class AuditObserver < ActiveRecord::Observer
|
||||
# observe :account, :balance
|
||||
#
|
||||
# def after_update(record)
|
||||
# AuditTrail.new(record, "UPDATED")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
|
||||
#
|
||||
# == Available callback methods
|
||||
#
|
||||
# The observer can implement callback methods for each of the methods described in the Callbacks module.
|
||||
#
|
||||
# == Storing Observers in Rails
|
||||
#
|
||||
# If you're using Active Record within Rails, observer classes are usually stored in app/models with the
|
||||
# naming convention of app/models/audit_observer.rb.
|
||||
#
|
||||
# == Configuration
|
||||
#
|
||||
# In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration setting in your
|
||||
# <tt>config/environment.rb</tt> file.
|
||||
#
|
||||
# config.active_record.observers = :comment_observer, :signup_observer
|
||||
#
|
||||
# Observers will not be invoked unless you define these in your application configuration.
|
||||
#
|
||||
# == Loading
|
||||
#
|
||||
# Observers register themselves in the model class they observe, since it is the class that
|
||||
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
|
||||
# corresponding model class is loaded.
|
||||
#
|
||||
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
|
||||
# application initializers. Now observers are loaded after application initializers,
|
||||
# so observed models can make use of extensions.
|
||||
#
|
||||
# If by any chance you are using observed models in the initialization you can still
|
||||
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
|
||||
# singletons and that call instantiates and registers them.
|
||||
#
|
||||
class Observer
|
||||
include Singleton
|
||||
|
||||
class << self
|
||||
# Attaches the observer to the supplied model classes.
|
||||
def observe(*models)
|
||||
models.flatten!
|
||||
models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
|
||||
define_method(:observed_classes) { Set.new(models) }
|
||||
end
|
||||
|
||||
# The class observed by default is inferred from the observer's class name:
|
||||
# assert_equal Person, PersonObserver.observed_class
|
||||
def observed_class
|
||||
if observed_class_name = name[/(.*)Observer/, 1]
|
||||
observed_class_name.constantize
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Start observing the declared classes and their subclasses.
|
||||
def initialize
|
||||
Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass }
|
||||
end
|
||||
|
||||
# Send observed_method(object) if the method exists.
|
||||
def update(observed_method, object) #:nodoc:
|
||||
send(observed_method, object) if respond_to?(observed_method)
|
||||
end
|
||||
|
||||
# Special method sent by the observed class when it is inherited.
|
||||
# Passes the new subclass.
|
||||
def observed_class_inherited(subclass) #:nodoc:
|
||||
self.class.observe(observed_classes + [subclass])
|
||||
add_observer!(subclass)
|
||||
end
|
||||
|
||||
protected
|
||||
def observed_classes
|
||||
Set.new([self.class.observed_class].compact.flatten)
|
||||
end
|
||||
|
||||
def observed_subclasses
|
||||
observed_classes.sum([]) { |klass| klass.send(:subclasses) }
|
||||
end
|
||||
|
||||
def add_observer!(klass)
|
||||
klass.add_observer(self)
|
||||
if respond_to?(:after_find) && !klass.method_defined?(:after_find)
|
||||
klass.class_eval 'def after_find() end'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
module ActiveRecord
|
||||
class QueryCache
|
||||
module ClassMethods
|
||||
# Enable the query cache within the block if Active Record is configured.
|
||||
def cache(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.cache(&block)
|
||||
end
|
||||
end
|
||||
|
||||
# Disable the query cache within the block if Active Record is configured.
|
||||
def uncached(&block)
|
||||
if ActiveRecord::Base.configurations.blank?
|
||||
yield
|
||||
else
|
||||
connection.uncached(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
ActiveRecord::Base.cache do
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,320 +0,0 @@
|
||||
module ActiveRecord
|
||||
module Reflection # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
|
||||
# This information can, for example, be used in a form builder that took an Active Record object and created input
|
||||
# fields for all of the attributes depending on their type and displayed the associations to other objects.
|
||||
#
|
||||
# You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
|
||||
module ClassMethods
|
||||
def create_reflection(macro, name, options, active_record)
|
||||
case macro
|
||||
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
||||
klass = options[:through] ? ThroughReflection : AssociationReflection
|
||||
reflection = klass.new(macro, name, options, active_record)
|
||||
when :composed_of
|
||||
reflection = AggregateReflection.new(macro, name, options, active_record)
|
||||
end
|
||||
write_inheritable_hash :reflections, name => reflection
|
||||
reflection
|
||||
end
|
||||
|
||||
# Returns a hash containing all AssociationReflection objects for the current class
|
||||
# Example:
|
||||
#
|
||||
# Invoice.reflections
|
||||
# Account.reflections
|
||||
#
|
||||
def reflections
|
||||
read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
|
||||
end
|
||||
|
||||
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
||||
def reflect_on_all_aggregations
|
||||
reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
|
||||
end
|
||||
|
||||
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
|
||||
#
|
||||
# Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
|
||||
#
|
||||
def reflect_on_aggregation(aggregation)
|
||||
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
|
||||
end
|
||||
|
||||
# Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a
|
||||
# certain association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, <tt>:belongs_to</tt>) for that as the first parameter.
|
||||
# Example:
|
||||
#
|
||||
# Account.reflect_on_all_associations # returns an array of all associations
|
||||
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
|
||||
#
|
||||
def reflect_on_all_associations(macro = nil)
|
||||
association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
|
||||
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
|
||||
end
|
||||
|
||||
# Returns the AssociationReflection object for the named +association+ (use the symbol). Example:
|
||||
#
|
||||
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
||||
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
||||
#
|
||||
def reflect_on_association(association)
|
||||
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
|
||||
end
|
||||
|
||||
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
||||
def reflect_on_all_autosave_associations
|
||||
reflections.values.select { |reflection| reflection.options[:autosave] }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
|
||||
# those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
||||
class MacroReflection
|
||||
attr_reader :active_record
|
||||
|
||||
def initialize(macro, name, options, active_record)
|
||||
@macro, @name, @options, @active_record = macro, name, options, active_record
|
||||
end
|
||||
|
||||
# Returns the name of the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return
|
||||
# <tt>:balance</tt> or for <tt>has_many :clients</tt> it will return <tt>:clients</tt>.
|
||||
def name
|
||||
@name
|
||||
end
|
||||
|
||||
# Returns the macro type. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt>
|
||||
# or for <tt>has_many :clients</tt> will return <tt>:has_many</tt>.
|
||||
def macro
|
||||
@macro
|
||||
end
|
||||
|
||||
# Returns the hash of options used for the macro. For example, it would return <tt>{ :class_name => "Money" }</tt> for
|
||||
# <tt>composed_of :balance, :class_name => 'Money'</tt> or +{}+ for <tt>has_many :clients</tt>.
|
||||
def options
|
||||
@options
|
||||
end
|
||||
|
||||
# Returns the class for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money
|
||||
# class and <tt>has_many :clients</tt> returns the Client class.
|
||||
def klass
|
||||
@klass ||= class_name.constantize
|
||||
end
|
||||
|
||||
# Returns the class name for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
|
||||
# and <tt>has_many :clients</tt> returns <tt>'Client'</tt>.
|
||||
def class_name
|
||||
@class_name ||= options[:class_name] || derive_class_name
|
||||
end
|
||||
|
||||
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
|
||||
# and +other_aggregation+ has an options hash assigned to it.
|
||||
def ==(other_aggregation)
|
||||
other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
|
||||
end
|
||||
|
||||
def sanitized_conditions #:nodoc:
|
||||
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
||||
end
|
||||
|
||||
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
||||
def belongs_to?
|
||||
macro == :belongs_to
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
name.to_s.camelize
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Holds all the meta-data about an aggregation as it was specified in the Active Record class.
|
||||
class AggregateReflection < MacroReflection #:nodoc:
|
||||
end
|
||||
|
||||
# Holds all the meta-data about an association as it was specified in the Active Record class.
|
||||
class AssociationReflection < MacroReflection #:nodoc:
|
||||
# Returns the target association's class:
|
||||
#
|
||||
# class Author < ActiveRecord::Base
|
||||
# has_many :books
|
||||
# end
|
||||
#
|
||||
# Author.reflect_on_association(:books).klass
|
||||
# # => Book
|
||||
#
|
||||
# <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
|
||||
# a new association object. Use +build_association+ or +create_association+
|
||||
# instead. This allows plugins to hook into association object creation.
|
||||
def klass
|
||||
@klass ||= active_record.send(:compute_type, class_name)
|
||||
end
|
||||
|
||||
# Returns a new, unsaved instance of the associated class. +options+ will
|
||||
# be passed to the class's constructor.
|
||||
def build_association(*options)
|
||||
klass.new(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save. +options+ will be passed to the class's
|
||||
# creation method. Returns the newly created object.
|
||||
def create_association(*options)
|
||||
klass.create(*options)
|
||||
end
|
||||
|
||||
# Creates a new instance of the associated class, and immediates saves it
|
||||
# with ActiveRecord::Base#save!. +options+ will be passed to the class's
|
||||
# creation method. If the created record doesn't pass validations, then an
|
||||
# exception will be raised.
|
||||
#
|
||||
# Returns the newly created object.
|
||||
def create_association!(*options)
|
||||
klass.create!(*options)
|
||||
end
|
||||
|
||||
def table_name
|
||||
@table_name ||= klass.table_name
|
||||
end
|
||||
|
||||
def quoted_table_name
|
||||
@quoted_table_name ||= klass.quoted_table_name
|
||||
end
|
||||
|
||||
def primary_key_name
|
||||
@primary_key_name ||= options[:foreign_key] || derive_primary_key_name
|
||||
end
|
||||
|
||||
def association_foreign_key
|
||||
@association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
|
||||
end
|
||||
|
||||
def counter_cache_column
|
||||
if options[:counter_cache] == true
|
||||
"#{active_record.name.demodulize.underscore.pluralize}_count"
|
||||
elsif options[:counter_cache]
|
||||
options[:counter_cache]
|
||||
end
|
||||
end
|
||||
|
||||
def columns(tbl_name, log_msg)
|
||||
@columns ||= klass.connection.columns(tbl_name, log_msg)
|
||||
end
|
||||
|
||||
def reset_column_information
|
||||
@columns = nil
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
end
|
||||
|
||||
def through_reflection
|
||||
false
|
||||
end
|
||||
|
||||
def through_reflection_primary_key_name
|
||||
end
|
||||
|
||||
def source_reflection
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
class_name = name.to_s.camelize
|
||||
class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
|
||||
class_name
|
||||
end
|
||||
|
||||
def derive_primary_key_name
|
||||
if belongs_to?
|
||||
"#{name}_id"
|
||||
elsif options[:as]
|
||||
"#{options[:as]}_id"
|
||||
else
|
||||
active_record.name.foreign_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Holds all the meta-data about a :through association as it was specified in the Active Record class.
|
||||
class ThroughReflection < AssociationReflection #:nodoc:
|
||||
# Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
||||
# (The <tt>:tags</tt> association on Tagging below.)
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :taggings
|
||||
# has_many :tags, :through => :taggings
|
||||
# end
|
||||
#
|
||||
def source_reflection
|
||||
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
||||
end
|
||||
|
||||
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
||||
# of a HasManyThrough or HasOneThrough association. Example:
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# has_many :taggings
|
||||
# has_many :tags, :through => :taggings
|
||||
# end
|
||||
#
|
||||
# tags_reflection = Post.reflect_on_association(:tags)
|
||||
# taggings_reflection = tags_reflection.through_reflection
|
||||
#
|
||||
def through_reflection
|
||||
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
||||
end
|
||||
|
||||
# Gets an array of possible <tt>:through</tt> source reflection names:
|
||||
#
|
||||
# [:singularized, :pluralized]
|
||||
#
|
||||
def source_reflection_names
|
||||
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
if through_reflection.nil?
|
||||
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
||||
end
|
||||
|
||||
if source_reflection.nil?
|
||||
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
||||
end
|
||||
|
||||
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
||||
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
||||
raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
|
||||
end
|
||||
|
||||
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
|
||||
raise HasManyThroughSourceAssociationMacroError.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
def through_reflection_primary_key
|
||||
through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
|
||||
end
|
||||
|
||||
def through_reflection_primary_key_name
|
||||
through_reflection.primary_key_name if through_reflection.belongs_to?
|
||||
end
|
||||
|
||||
private
|
||||
def derive_class_name
|
||||
# get the class_name of the belongs_to association of the through reflection
|
||||
options[:source_type] || source_reflection.class_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
module ActiveRecord
|
||||
# Allows programmers to programmatically define a schema in a portable
|
||||
# DSL. This means you can define tables, indexes, etc. without using SQL
|
||||
# directly, so your applications can more easily support multiple
|
||||
# databases.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# ActiveRecord::Schema.define do
|
||||
# create_table :authors do |t|
|
||||
# t.string :name, :null => false
|
||||
# end
|
||||
#
|
||||
# add_index :authors, :name, :unique
|
||||
#
|
||||
# create_table :posts do |t|
|
||||
# t.integer :author_id, :null => false
|
||||
# t.string :subject
|
||||
# t.text :body
|
||||
# t.boolean :private, :default => false
|
||||
# end
|
||||
#
|
||||
# add_index :posts, :author_id
|
||||
# end
|
||||
#
|
||||
# ActiveRecord::Schema is only supported by database adapters that also
|
||||
# support migrations, the two features being very similar.
|
||||
class Schema < Migration
|
||||
private_class_method :new
|
||||
|
||||
# Eval the given block. All methods available to the current connection
|
||||
# adapter are available within the block, so you can easily use the
|
||||
# database definition DSL to build up your schema (+create_table+,
|
||||
# +add_index+, etc.).
|
||||
#
|
||||
# The +info+ hash is optional, and if given is used to define metadata
|
||||
# about the current schema (currently, only the schema's version):
|
||||
#
|
||||
# ActiveRecord::Schema.define(:version => 20380119000001) do
|
||||
# ...
|
||||
# end
|
||||
def self.define(info={}, &block)
|
||||
instance_eval(&block)
|
||||
|
||||
unless info[:version].blank?
|
||||
initialize_schema_migrations_table
|
||||
assume_migrated_upto_version info[:version]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,182 +0,0 @@
|
||||
require 'stringio'
|
||||
require 'bigdecimal'
|
||||
|
||||
module ActiveRecord
|
||||
# This class is used to dump the database schema for some connection to some
|
||||
# output format (i.e., ActiveRecord::Schema).
|
||||
class SchemaDumper #:nodoc:
|
||||
private_class_method :new
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# A list of tables which should not be dumped to the schema.
|
||||
# Acceptable values are strings as well as regexp.
|
||||
# This setting is only used if ActiveRecord::Base.schema_format == :ruby
|
||||
cattr_accessor :ignore_tables
|
||||
@@ignore_tables = []
|
||||
|
||||
def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
|
||||
new(connection).dump(stream)
|
||||
stream
|
||||
end
|
||||
|
||||
def dump(stream)
|
||||
header(stream)
|
||||
tables(stream)
|
||||
trailer(stream)
|
||||
stream
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(connection)
|
||||
@connection = connection
|
||||
@types = @connection.native_database_types
|
||||
@version = Migrator::current_version rescue nil
|
||||
end
|
||||
|
||||
def header(stream)
|
||||
define_params = @version ? ":version => #{@version}" : ""
|
||||
|
||||
stream.puts <<HEADER
|
||||
# This file is auto-generated from the current state of the database. Instead of editing this file,
|
||||
# please use the migrations feature of Active Record to incrementally modify your database, and
|
||||
# then regenerate this schema definition.
|
||||
#
|
||||
# Note that this schema.rb definition is the authoritative source for your database schema. If you need
|
||||
# to create the application database on another system, you should be using db:schema:load, not running
|
||||
# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
||||
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(#{define_params}) do
|
||||
|
||||
HEADER
|
||||
end
|
||||
|
||||
def trailer(stream)
|
||||
stream.puts "end"
|
||||
end
|
||||
|
||||
def tables(stream)
|
||||
@connection.tables.sort.each do |tbl|
|
||||
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
|
||||
case ignored
|
||||
when String; tbl == ignored
|
||||
when Regexp; tbl =~ ignored
|
||||
else
|
||||
raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
|
||||
end
|
||||
end
|
||||
table(tbl, stream)
|
||||
end
|
||||
end
|
||||
|
||||
def table(table, stream)
|
||||
columns = @connection.columns(table)
|
||||
begin
|
||||
tbl = StringIO.new
|
||||
|
||||
# first dump primary key column
|
||||
if @connection.respond_to?(:pk_and_sequence_for)
|
||||
pk, pk_seq = @connection.pk_and_sequence_for(table)
|
||||
elsif @connection.respond_to?(:primary_key)
|
||||
pk = @connection.primary_key(table)
|
||||
end
|
||||
|
||||
tbl.print " create_table #{table.inspect}"
|
||||
if columns.detect { |c| c.name == pk }
|
||||
if pk != 'id'
|
||||
tbl.print %Q(, :primary_key => "#{pk}")
|
||||
end
|
||||
else
|
||||
tbl.print ", :id => false"
|
||||
end
|
||||
tbl.print ", :force => true"
|
||||
tbl.puts " do |t|"
|
||||
|
||||
# then dump all non-primary key columns
|
||||
column_specs = columns.map do |column|
|
||||
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
|
||||
next if column.name == pk
|
||||
spec = {}
|
||||
spec[:name] = column.name.inspect
|
||||
spec[:type] = column.type.to_s
|
||||
spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal
|
||||
spec[:precision] = column.precision.inspect if !column.precision.nil?
|
||||
spec[:scale] = column.scale.inspect if !column.scale.nil?
|
||||
spec[:null] = 'false' if !column.null
|
||||
spec[:default] = default_string(column.default) if column.has_default?
|
||||
(spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
|
||||
spec
|
||||
end.compact
|
||||
|
||||
# find all migration keys used in this table
|
||||
keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten
|
||||
|
||||
# figure out the lengths for each column based on above keys
|
||||
lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max }
|
||||
|
||||
# the string we're going to sprintf our values against, with standardized column widths
|
||||
format_string = lengths.map{ |len| "%-#{len}s" }
|
||||
|
||||
# find the max length for the 'type' column, which is special
|
||||
type_length = column_specs.map{ |column| column[:type].length }.max
|
||||
|
||||
# add column type definition to our format string
|
||||
format_string.unshift " t.%-#{type_length}s "
|
||||
|
||||
format_string *= ''
|
||||
|
||||
column_specs.each do |colspec|
|
||||
values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len }
|
||||
values.unshift colspec[:type]
|
||||
tbl.print((format_string % values).gsub(/,\s*$/, ''))
|
||||
tbl.puts
|
||||
end
|
||||
|
||||
tbl.puts " end"
|
||||
tbl.puts
|
||||
|
||||
indexes(table, tbl)
|
||||
|
||||
tbl.rewind
|
||||
stream.print tbl.read
|
||||
rescue => e
|
||||
stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}"
|
||||
stream.puts "# #{e.message}"
|
||||
stream.puts
|
||||
end
|
||||
|
||||
stream
|
||||
end
|
||||
|
||||
def default_string(value)
|
||||
case value
|
||||
when BigDecimal
|
||||
value.to_s
|
||||
when Date, DateTime, Time
|
||||
"'" + value.to_s(:db) + "'"
|
||||
else
|
||||
value.inspect
|
||||
end
|
||||
end
|
||||
|
||||
def indexes(table, stream)
|
||||
if (indexes = @connection.indexes(table)).any?
|
||||
add_index_statements = indexes.map do |index|
|
||||
statment_parts = [ ('add_index ' + index.table.inspect) ]
|
||||
statment_parts << index.columns.inspect
|
||||
statment_parts << (':name => ' + index.name.inspect)
|
||||
statment_parts << ':unique => true' if index.unique
|
||||
|
||||
' ' + statment_parts.join(', ')
|
||||
end
|
||||
|
||||
stream.puts add_index_statements.sort.join("\n")
|
||||
stream.puts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,101 +0,0 @@
|
||||
require 'active_support/json'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
class Serializer #:nodoc:
|
||||
attr_reader :options
|
||||
|
||||
def initialize(record, options = nil)
|
||||
@record = record
|
||||
@options = options ? options.dup : {}
|
||||
end
|
||||
|
||||
# To replicate the behavior in ActiveRecord#attributes,
|
||||
# <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
|
||||
# for a N level model but is set for the N+1 level models,
|
||||
# then because <tt>:except</tt> is set to a default value, the second
|
||||
# level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
|
||||
# <tt>:only</tt> is set, always delete <tt>:except</tt>.
|
||||
def serializable_attribute_names
|
||||
attribute_names = @record.attribute_names
|
||||
|
||||
if options[:only]
|
||||
options.delete(:except)
|
||||
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
|
||||
else
|
||||
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
|
||||
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
|
||||
end
|
||||
|
||||
attribute_names
|
||||
end
|
||||
|
||||
def serializable_method_names
|
||||
Array(options[:methods]).inject([]) do |method_attributes, name|
|
||||
method_attributes << name if @record.respond_to?(name.to_s)
|
||||
method_attributes
|
||||
end
|
||||
end
|
||||
|
||||
def serializable_names
|
||||
serializable_attribute_names + serializable_method_names
|
||||
end
|
||||
|
||||
# Add associations specified via the <tt>:includes</tt> option.
|
||||
# Expects a block that takes as arguments:
|
||||
# +association+ - name of the association
|
||||
# +records+ - the association record(s) to be serialized
|
||||
# +opts+ - options for the association records
|
||||
def add_includes(&block)
|
||||
if include_associations = options.delete(:include)
|
||||
base_only_or_except = { :except => options[:except],
|
||||
:only => options[:only] }
|
||||
|
||||
include_has_options = include_associations.is_a?(Hash)
|
||||
associations = include_has_options ? include_associations.keys : Array(include_associations)
|
||||
|
||||
for association in associations
|
||||
records = case @record.class.reflect_on_association(association).macro
|
||||
when :has_many, :has_and_belongs_to_many
|
||||
@record.send(association).to_a
|
||||
when :has_one, :belongs_to
|
||||
@record.send(association)
|
||||
end
|
||||
|
||||
unless records.nil?
|
||||
association_options = include_has_options ? include_associations[association] : base_only_or_except
|
||||
opts = options.merge(association_options)
|
||||
yield(association, records, opts)
|
||||
end
|
||||
end
|
||||
|
||||
options[:include] = include_associations
|
||||
end
|
||||
end
|
||||
|
||||
def serializable_record
|
||||
returning(serializable_record = {}) do
|
||||
serializable_names.each { |name| serializable_record[name] = @record.send(name) }
|
||||
add_includes do |association, records, opts|
|
||||
if records.is_a?(Enumerable)
|
||||
serializable_record[association] = records.collect { |r| self.class.new(r, opts).serializable_record }
|
||||
else
|
||||
serializable_record[association] = self.class.new(records, opts).serializable_record
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def serialize
|
||||
# overwrite to implement
|
||||
end
|
||||
|
||||
def to_s(&block)
|
||||
serialize(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_record/serializers/xml_serializer'
|
||||
require 'active_record/serializers/json_serializer'
|
||||
@@ -1,91 +0,0 @@
|
||||
require 'active_support/json'
|
||||
require 'active_support/core_ext/module/model_naming'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
def self.included(base)
|
||||
base.cattr_accessor :include_root_in_json, :instance_writer => false
|
||||
end
|
||||
|
||||
# Returns a JSON string representing the model. Some configuration is
|
||||
# available through +options+.
|
||||
#
|
||||
# The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
|
||||
# top-level behavior of to_json. In a new Rails application, it is set to
|
||||
# <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
|
||||
# to_json will emit a single root node named after the object's type. For example:
|
||||
#
|
||||
# konata = User.find(1)
|
||||
# ActiveRecord::Base.include_root_in_json = true
|
||||
# konata.to_json
|
||||
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true} }
|
||||
#
|
||||
# ActiveRecord::Base.include_root_in_json = false
|
||||
# konata.to_json
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true}
|
||||
#
|
||||
# The remainder of the examples in this section assume include_root_in_json is set to
|
||||
# <tt>false</tt>.
|
||||
#
|
||||
# Without any +options+, the returned JSON string will include all
|
||||
# the model's attributes. For example:
|
||||
#
|
||||
# konata = User.find(1)
|
||||
# konata.to_json
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true}
|
||||
#
|
||||
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
|
||||
# included, and work similar to the +attributes+ method. For example:
|
||||
#
|
||||
# konata.to_json(:only => [ :id, :name ])
|
||||
# # => {"id": 1, "name": "Konata Izumi"}
|
||||
#
|
||||
# konata.to_json(:except => [ :id, :created_at, :age ])
|
||||
# # => {"name": "Konata Izumi", "awesome": true}
|
||||
#
|
||||
# To include any methods on the model, use <tt>:methods</tt>.
|
||||
#
|
||||
# konata.to_json(:methods => :permalink)
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true,
|
||||
# "permalink": "1-konata-izumi"}
|
||||
#
|
||||
# To include associations, use <tt>:include</tt>.
|
||||
#
|
||||
# konata.to_json(:include => :posts)
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true,
|
||||
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
|
||||
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
|
||||
#
|
||||
# 2nd level and higher order associations work as well:
|
||||
#
|
||||
# konata.to_json(:include => { :posts => {
|
||||
# :include => { :comments => {
|
||||
# :only => :body } },
|
||||
# :only => :title } })
|
||||
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
|
||||
# "created_at": "2006/08/01", "awesome": true,
|
||||
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
|
||||
# "title": "Welcome to the weblog"},
|
||||
# {"comments": [{"body": "Don't think too hard"}],
|
||||
# "title": "So I was thinking"}]}
|
||||
def to_json(options = {})
|
||||
super
|
||||
end
|
||||
|
||||
def as_json(options = nil) #:nodoc:
|
||||
hash = Serializer.new(self, options).serializable_record
|
||||
hash = { self.class.model_name.element => hash } if include_root_in_json
|
||||
hash
|
||||
end
|
||||
|
||||
def from_json(json)
|
||||
self.attributes = ActiveSupport::JSON.decode(json)
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,376 +0,0 @@
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
# Builds an XML document to represent the model. Some configuration is
|
||||
# available through +options+. However more complicated cases should
|
||||
# override ActiveRecord::Base#to_xml.
|
||||
#
|
||||
# By default the generated XML document will include the processing
|
||||
# instruction and all the object's attributes. For example:
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <topic>
|
||||
# <title>The First Topic</title>
|
||||
# <author-name>David</author-name>
|
||||
# <id type="integer">1</id>
|
||||
# <approved type="boolean">false</approved>
|
||||
# <replies-count type="integer">0</replies-count>
|
||||
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
|
||||
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
|
||||
# <content>Have a nice day</content>
|
||||
# <author-email-address>david@loudthinking.com</author-email-address>
|
||||
# <parent-id></parent-id>
|
||||
# <last-read type="date">2004-04-15</last-read>
|
||||
# </topic>
|
||||
#
|
||||
# This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>,
|
||||
# <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> .
|
||||
# The <tt>:only</tt> and <tt>:except</tt> options are the same as for the
|
||||
# +attributes+ method. The default is to dasherize all column names, but you
|
||||
# can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt>
|
||||
# to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>.
|
||||
# To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+.
|
||||
#
|
||||
# For instance:
|
||||
#
|
||||
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
|
||||
#
|
||||
# <topic>
|
||||
# <title>The First Topic</title>
|
||||
# <author-name>David</author-name>
|
||||
# <approved type="boolean">false</approved>
|
||||
# <content>Have a nice day</content>
|
||||
# <author-email-address>david@loudthinking.com</author-email-address>
|
||||
# <parent-id></parent-id>
|
||||
# <last-read type="date">2004-04-15</last-read>
|
||||
# </topic>
|
||||
#
|
||||
# To include first level associations use <tt>:include</tt>:
|
||||
#
|
||||
# firm.to_xml :include => [ :account, :clients ]
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <firm>
|
||||
# <id type="integer">1</id>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>37signals</name>
|
||||
# <clients type="array">
|
||||
# <client>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>Summit</name>
|
||||
# </client>
|
||||
# <client>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>Microsoft</name>
|
||||
# </client>
|
||||
# </clients>
|
||||
# <account>
|
||||
# <id type="integer">1</id>
|
||||
# <credit-limit type="integer">50</credit-limit>
|
||||
# </account>
|
||||
# </firm>
|
||||
#
|
||||
# To include deeper levels of associations pass a hash like this:
|
||||
#
|
||||
# firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <firm>
|
||||
# <id type="integer">1</id>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>37signals</name>
|
||||
# <clients type="array">
|
||||
# <client>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>Summit</name>
|
||||
# <address>
|
||||
# ...
|
||||
# </address>
|
||||
# </client>
|
||||
# <client>
|
||||
# <rating type="integer">1</rating>
|
||||
# <name>Microsoft</name>
|
||||
# <address>
|
||||
# ...
|
||||
# </address>
|
||||
# </client>
|
||||
# </clients>
|
||||
# <account>
|
||||
# <id type="integer">1</id>
|
||||
# <credit-limit type="integer">50</credit-limit>
|
||||
# </account>
|
||||
# </firm>
|
||||
#
|
||||
# To include any methods on the model being called use <tt>:methods</tt>:
|
||||
#
|
||||
# firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
|
||||
#
|
||||
# <firm>
|
||||
# # ... normal attributes as shown above ...
|
||||
# <calculated-earnings>100000000000000000</calculated-earnings>
|
||||
# <real-earnings>5</real-earnings>
|
||||
# </firm>
|
||||
#
|
||||
# To call any additional Procs use <tt>:procs</tt>. The Procs are passed a
|
||||
# modified version of the options hash that was given to +to_xml+:
|
||||
#
|
||||
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
|
||||
# firm.to_xml :procs => [ proc ]
|
||||
#
|
||||
# <firm>
|
||||
# # ... normal attributes as shown above ...
|
||||
# <abc>def</abc>
|
||||
# </firm>
|
||||
#
|
||||
# Alternatively, you can yield the builder object as part of the +to_xml+ call:
|
||||
#
|
||||
# firm.to_xml do |xml|
|
||||
# xml.creator do
|
||||
# xml.first_name "David"
|
||||
# xml.last_name "Heinemeier Hansson"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# <firm>
|
||||
# # ... normal attributes as shown above ...
|
||||
# <creator>
|
||||
# <first_name>David</first_name>
|
||||
# <last_name>Heinemeier Hansson</last_name>
|
||||
# </creator>
|
||||
# </firm>
|
||||
#
|
||||
# As noted above, you may override +to_xml+ in your ActiveRecord::Base
|
||||
# subclasses to have complete control about what's generated. The general
|
||||
# form of doing this is:
|
||||
#
|
||||
# class IHaveMyOwnXML < ActiveRecord::Base
|
||||
# def to_xml(options = {})
|
||||
# options[:indent] ||= 2
|
||||
# xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
# xml.instruct! unless options[:skip_instruct]
|
||||
# xml.level_one do
|
||||
# xml.tag!(:second_level, 'content')
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
def to_xml(options = {}, &block)
|
||||
serializer = XmlSerializer.new(self, options)
|
||||
block_given? ? serializer.to_s(&block) : serializer.to_s
|
||||
end
|
||||
|
||||
def from_xml(xml)
|
||||
self.attributes = Hash.from_xml(xml).values.first
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
|
||||
def builder
|
||||
@builder ||= begin
|
||||
options[:indent] ||= 2
|
||||
builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
unless options[:skip_instruct]
|
||||
builder.instruct!
|
||||
options[:skip_instruct] = true
|
||||
end
|
||||
|
||||
builder
|
||||
end
|
||||
end
|
||||
|
||||
def root
|
||||
root = (options[:root] || @record.class.model_name.singular).to_s
|
||||
reformat_name(root)
|
||||
end
|
||||
|
||||
def dasherize?
|
||||
!options.has_key?(:dasherize) || options[:dasherize]
|
||||
end
|
||||
|
||||
def camelize?
|
||||
options.has_key?(:camelize) && options[:camelize]
|
||||
end
|
||||
|
||||
def reformat_name(name)
|
||||
name = name.camelize if camelize?
|
||||
dasherize? ? name.dasherize : name
|
||||
end
|
||||
|
||||
def serializable_attributes
|
||||
serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
|
||||
end
|
||||
|
||||
def serializable_method_attributes
|
||||
Array(options[:methods]).inject([]) do |method_attributes, name|
|
||||
method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s)
|
||||
method_attributes
|
||||
end
|
||||
end
|
||||
|
||||
def add_attributes
|
||||
(serializable_attributes + serializable_method_attributes).each do |attribute|
|
||||
add_tag(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
def add_procs
|
||||
if procs = options.delete(:procs)
|
||||
[ *procs ].each do |proc|
|
||||
proc.call(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_tag(attribute)
|
||||
builder.tag!(
|
||||
reformat_name(attribute.name),
|
||||
attribute.value.to_s,
|
||||
attribute.decorations(!options[:skip_types])
|
||||
)
|
||||
end
|
||||
|
||||
def add_associations(association, records, opts)
|
||||
if records.is_a?(Enumerable)
|
||||
tag = reformat_name(association.to_s)
|
||||
type = options[:skip_types] ? {} : {:type => "array"}
|
||||
|
||||
if records.empty?
|
||||
builder.tag!(tag, type)
|
||||
else
|
||||
builder.tag!(tag, type) do
|
||||
association_name = association.to_s.singularize
|
||||
records.each do |record|
|
||||
if options[:skip_types]
|
||||
record_type = {}
|
||||
else
|
||||
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
|
||||
record_type = {:type => record_class}
|
||||
end
|
||||
|
||||
record.to_xml opts.merge(:root => association_name).merge(record_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if record = @record.send(association)
|
||||
record.to_xml(opts.merge(:root => association))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def serialize
|
||||
args = [root]
|
||||
if options[:namespace]
|
||||
args << {:xmlns=>options[:namespace]}
|
||||
end
|
||||
|
||||
if options[:type]
|
||||
args << {:type=>options[:type]}
|
||||
end
|
||||
|
||||
builder.tag!(*args) do
|
||||
add_attributes
|
||||
procs = options.delete(:procs)
|
||||
add_includes { |association, records, opts| add_associations(association, records, opts) }
|
||||
options[:procs] = procs
|
||||
add_procs
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
class Attribute #:nodoc:
|
||||
attr_reader :name, :value, :type
|
||||
|
||||
def initialize(name, record)
|
||||
@name, @record = name, record
|
||||
|
||||
@type = compute_type
|
||||
@value = compute_value
|
||||
end
|
||||
|
||||
# There is a significant speed improvement if the value
|
||||
# does not need to be escaped, as <tt>tag!</tt> escapes all values
|
||||
# to ensure that valid XML is generated. For known binary
|
||||
# values, it is at least an order of magnitude faster to
|
||||
# Base64 encode binary values and directly put them in the
|
||||
# output XML than to pass the original value or the Base64
|
||||
# encoded value to the <tt>tag!</tt> method. It definitely makes
|
||||
# no sense to Base64 encode the value and then give it to
|
||||
# <tt>tag!</tt>, since that just adds additional overhead.
|
||||
def needs_encoding?
|
||||
![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
|
||||
end
|
||||
|
||||
def decorations(include_types = true)
|
||||
decorations = {}
|
||||
|
||||
if type == :binary
|
||||
decorations[:encoding] = 'base64'
|
||||
end
|
||||
|
||||
if include_types && type != :string
|
||||
decorations[:type] = type
|
||||
end
|
||||
|
||||
if value.nil?
|
||||
decorations[:nil] = true
|
||||
end
|
||||
|
||||
decorations
|
||||
end
|
||||
|
||||
protected
|
||||
def force_binary_encoding(obj)
|
||||
return nil if not obj
|
||||
return obj if not "X".respond_to?('encode')
|
||||
|
||||
case obj.class.to_s
|
||||
when 'String'
|
||||
return obj.encode(::Encoding::BINARY, { :invalid => :replace, :undef => :replace, :replace => '?' })
|
||||
when 'Hash'
|
||||
obj.each_pair do |k,v|
|
||||
obj[k] = force_binary_encoding(v)
|
||||
end
|
||||
when 'Array'
|
||||
obj.each_index do |i|
|
||||
obj[i] = force_binary_encoding(obj[i])
|
||||
end
|
||||
end
|
||||
obj
|
||||
end
|
||||
|
||||
def compute_type
|
||||
type = if @record.class.serialized_attributes.has_key?(name)
|
||||
:yaml
|
||||
else
|
||||
@record.class.columns_hash[name].try(:type)
|
||||
end
|
||||
|
||||
case type
|
||||
when :text
|
||||
:string
|
||||
when :time
|
||||
:datetime
|
||||
else
|
||||
type
|
||||
end
|
||||
end
|
||||
|
||||
def compute_value
|
||||
value = force_binary_encoding(@record.send(name))
|
||||
|
||||
if formatter = Hash::XML_FORMATTING[type.to_s]
|
||||
value ? formatter.call(value) : nil
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class MethodAttribute < Attribute #:nodoc:
|
||||
protected
|
||||
def compute_type
|
||||
Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,326 +0,0 @@
|
||||
module ActiveRecord
|
||||
# A session store backed by an Active Record class. A default class is
|
||||
# provided, but any object duck-typing to an Active Record Session class
|
||||
# with text +session_id+ and +data+ attributes is sufficient.
|
||||
#
|
||||
# The default assumes a +sessions+ tables with columns:
|
||||
# +id+ (numeric primary key),
|
||||
# +session_id+ (text, or longtext if your session data exceeds 65K), and
|
||||
# +data+ (text or longtext; careful if your session data exceeds 65KB).
|
||||
# The +session_id+ column should always be indexed for speedy lookups.
|
||||
# Session data is marshaled to the +data+ column in Base64 format.
|
||||
# If the data you write is larger than the column's size limit,
|
||||
# ActionController::SessionOverflowError will be raised.
|
||||
#
|
||||
# You may configure the table name, primary key, and data column.
|
||||
# For example, at the end of <tt>config/environment.rb</tt>:
|
||||
# ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table'
|
||||
# ActiveRecord::SessionStore::Session.primary_key = 'session_id'
|
||||
# ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data'
|
||||
# Note that setting the primary key to the +session_id+ frees you from
|
||||
# having a separate +id+ column if you don't want it. However, you must
|
||||
# set <tt>session.model.id = session.session_id</tt> by hand! A before filter
|
||||
# on ApplicationController is a good place.
|
||||
#
|
||||
# Since the default class is a simple Active Record, you get timestamps
|
||||
# for free if you add +created_at+ and +updated_at+ datetime columns to
|
||||
# the +sessions+ table, making periodic session expiration a snap.
|
||||
#
|
||||
# You may provide your own session class implementation, whether a
|
||||
# feature-packed Active Record or a bare-metal high-performance SQL
|
||||
# store, by setting
|
||||
# ActiveRecord::SessionStore.session_class = MySessionClass
|
||||
# You must implement these methods:
|
||||
# self.find_by_session_id(session_id)
|
||||
# initialize(hash_of_session_id_and_data)
|
||||
# attr_reader :session_id
|
||||
# attr_accessor :data
|
||||
# save
|
||||
# destroy
|
||||
#
|
||||
# The example SqlBypass class is a generic SQL session store. You may
|
||||
# use it as a basis for high-performance database-specific stores.
|
||||
class SessionStore < ActionController::Session::AbstractStore
|
||||
# The default Active Record class.
|
||||
class Session < ActiveRecord::Base
|
||||
##
|
||||
# :singleton-method:
|
||||
# Customizable data column name. Defaults to 'data'.
|
||||
cattr_accessor :data_column_name
|
||||
self.data_column_name = 'data'
|
||||
|
||||
before_save :marshal_data!
|
||||
before_save :raise_on_session_data_overflow!
|
||||
|
||||
class << self
|
||||
def data_column_size_limit
|
||||
@data_column_size_limit ||= columns_hash[@@data_column_name].limit
|
||||
end
|
||||
|
||||
# Hook to set up sessid compatibility.
|
||||
def find_by_session_id(session_id)
|
||||
setup_sessid_compatibility!
|
||||
find_by_session_id(session_id)
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{connection.quote_column_name('session_id')} TEXT UNIQUE,
|
||||
#{connection.quote_column_name(@@data_column_name)} TEXT(255)
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
|
||||
private
|
||||
# Compatibility with tables using sessid instead of session_id.
|
||||
def setup_sessid_compatibility!
|
||||
# Reset column info since it may be stale.
|
||||
reset_column_information
|
||||
if columns_hash['sessid']
|
||||
def self.find_by_session_id(*args)
|
||||
find_by_sessid(*args)
|
||||
end
|
||||
|
||||
define_method(:session_id) { sessid }
|
||||
define_method(:session_id=) { |session_id| self.sessid = session_id }
|
||||
else
|
||||
def self.find_by_session_id(session_id)
|
||||
find :first, :conditions => {:session_id=>session_id}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
@data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
|
||||
end
|
||||
|
||||
attr_writer :data
|
||||
|
||||
# Has the session been loaded yet?
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
private
|
||||
def marshal_data!
|
||||
return false if !loaded?
|
||||
write_attribute(@@data_column_name, self.class.marshal(self.data))
|
||||
end
|
||||
|
||||
# Ensures that the data about to be stored in the database is not
|
||||
# larger than the data storage column. Raises
|
||||
# ActionController::SessionOverflowError.
|
||||
def raise_on_session_data_overflow!
|
||||
return false if !loaded?
|
||||
limit = self.class.data_column_size_limit
|
||||
if loaded? and limit and read_attribute(@@data_column_name).size > limit
|
||||
raise ActionController::SessionOverflowError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A barebones session store which duck-types with the default session
|
||||
# store but bypasses Active Record and issues SQL directly. This is
|
||||
# an example session model class meant as a basis for your own classes.
|
||||
#
|
||||
# The database connection, table name, and session id and data columns
|
||||
# are configurable class attributes. Marshaling and unmarshaling
|
||||
# are implemented as class methods that you may override. By default,
|
||||
# marshaling data is
|
||||
#
|
||||
# ActiveSupport::Base64.encode64(Marshal.dump(data))
|
||||
#
|
||||
# and unmarshaling data is
|
||||
#
|
||||
# Marshal.load(ActiveSupport::Base64.decode64(data))
|
||||
#
|
||||
# This marshaling behavior is intended to store the widest range of
|
||||
# binary session data in a +text+ column. For higher performance,
|
||||
# store in a +blob+ column instead and forgo the Base64 encoding.
|
||||
class SqlBypass
|
||||
##
|
||||
# :singleton-method:
|
||||
# Use the ActiveRecord::Base.connection by default.
|
||||
cattr_accessor :connection
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The table name defaults to 'sessions'.
|
||||
cattr_accessor :table_name
|
||||
@@table_name = 'sessions'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The session id field defaults to 'session_id'.
|
||||
cattr_accessor :session_id_column
|
||||
@@session_id_column = 'session_id'
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# The data field defaults to 'data'.
|
||||
cattr_accessor :data_column
|
||||
@@data_column = 'data'
|
||||
|
||||
class << self
|
||||
def connection
|
||||
@@connection ||= ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
# Look up a session by id and unmarshal its data if found.
|
||||
def find_by_session_id(session_id)
|
||||
if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
|
||||
new(:session_id => session_id, :marshaled_data => record['data'])
|
||||
end
|
||||
end
|
||||
|
||||
def marshal(data)
|
||||
ActiveSupport::Base64.encode64(Marshal.dump(data)) if data
|
||||
end
|
||||
|
||||
def unmarshal(data)
|
||||
Marshal.load(ActiveSupport::Base64.decode64(data)) if data
|
||||
end
|
||||
|
||||
def create_table!
|
||||
@@connection.execute <<-end_sql
|
||||
CREATE TABLE #{table_name} (
|
||||
id INTEGER PRIMARY KEY,
|
||||
#{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
|
||||
#{@@connection.quote_column_name(data_column)} TEXT
|
||||
)
|
||||
end_sql
|
||||
end
|
||||
|
||||
def drop_table!
|
||||
@@connection.execute "DROP TABLE #{table_name}"
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :session_id
|
||||
attr_writer :data
|
||||
|
||||
# Look for normal and marshaled data, self.find_by_session_id's way of
|
||||
# telling us to postpone unmarshaling until the data is requested.
|
||||
# We need to handle a normal data attribute in case of a new record.
|
||||
def initialize(attributes)
|
||||
@session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
|
||||
@new_record = @marshaled_data.nil?
|
||||
end
|
||||
|
||||
def new_record?
|
||||
@new_record
|
||||
end
|
||||
|
||||
# Lazy-unmarshal session state.
|
||||
def data
|
||||
unless @data
|
||||
if @marshaled_data
|
||||
@data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
|
||||
else
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
@data
|
||||
end
|
||||
|
||||
def loaded?
|
||||
!!@data
|
||||
end
|
||||
|
||||
def save
|
||||
return false if !loaded?
|
||||
marshaled_data = self.class.marshal(data)
|
||||
|
||||
if @new_record
|
||||
@new_record = false
|
||||
@@connection.update <<-end_sql, 'Create session'
|
||||
INSERT INTO #{@@table_name} (
|
||||
#{@@connection.quote_column_name(@@session_id_column)},
|
||||
#{@@connection.quote_column_name(@@data_column)} )
|
||||
VALUES (
|
||||
#{@@connection.quote(session_id)},
|
||||
#{@@connection.quote(marshaled_data)} )
|
||||
end_sql
|
||||
else
|
||||
@@connection.update <<-end_sql, 'Update session'
|
||||
UPDATE #{@@table_name}
|
||||
SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
unless @new_record
|
||||
@@connection.delete <<-end_sql, 'Destroy session'
|
||||
DELETE FROM #{@@table_name}
|
||||
WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
|
||||
end_sql
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The class used for session storage. Defaults to
|
||||
# ActiveRecord::SessionStore::Session
|
||||
cattr_accessor :session_class
|
||||
self.session_class = Session
|
||||
|
||||
SESSION_RECORD_KEY = 'rack.session.record'.freeze
|
||||
|
||||
private
|
||||
def get_session(env, sid)
|
||||
Base.silence do
|
||||
sid ||= generate_sid
|
||||
session = find_session(sid)
|
||||
env[SESSION_RECORD_KEY] = session
|
||||
[sid, session.data]
|
||||
end
|
||||
end
|
||||
|
||||
def set_session(env, sid, session_data)
|
||||
Base.silence do
|
||||
record = get_session_model(env, sid)
|
||||
record.data = session_data
|
||||
return false unless record.save
|
||||
|
||||
session_data = record.data
|
||||
if session_data && session_data.respond_to?(:each_value)
|
||||
session_data.each_value do |obj|
|
||||
obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
def get_session_model(env, sid)
|
||||
if env[ENV_SESSION_OPTIONS_KEY][:id].nil?
|
||||
env[SESSION_RECORD_KEY] = find_session(sid)
|
||||
else
|
||||
env[SESSION_RECORD_KEY] ||= find_session(sid)
|
||||
end
|
||||
end
|
||||
|
||||
def find_session(id)
|
||||
@@session_class.find_by_session_id(id) ||
|
||||
@@session_class.new(:session_id => id, :data => {})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
require "active_support/test_case"
|
||||
|
||||
module ActiveRecord
|
||||
class TestCase < ActiveSupport::TestCase #:nodoc:
|
||||
def assert_date_from_db(expected, actual, message = nil)
|
||||
# SybaseAdapter doesn't have a separate column type just for dates,
|
||||
# so the time is in the string and incorrectly formatted
|
||||
if current_adapter?(:SybaseAdapter)
|
||||
assert_equal expected.to_s, actual.to_date.to_s, message
|
||||
else
|
||||
assert_equal expected.to_s, actual.to_s, message
|
||||
end
|
||||
end
|
||||
|
||||
def assert_sql(*patterns_to_match)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
failed_patterns = []
|
||||
patterns_to_match.each do |pattern|
|
||||
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
|
||||
end
|
||||
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found."
|
||||
end
|
||||
|
||||
def assert_queries(num = 1)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
%w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
|
||||
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
|
||||
end
|
||||
|
||||
def assert_no_queries(&block)
|
||||
assert_queries(0, &block)
|
||||
end
|
||||
|
||||
def self.use_concurrent_connections
|
||||
setup :connection_allow_concurrency_setup
|
||||
teardown :connection_allow_concurrency_teardown
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_setup
|
||||
@connection = ActiveRecord::Base.remove_connection
|
||||
ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
|
||||
end
|
||||
|
||||
def connection_allow_concurrency_teardown
|
||||
ActiveRecord::Base.clear_all_connections!
|
||||
ActiveRecord::Base.establish_connection(@connection)
|
||||
end
|
||||
|
||||
def with_kcode(kcode)
|
||||
if RUBY_VERSION < '1.9'
|
||||
orig_kcode, $KCODE = $KCODE, kcode
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
$KCODE = orig_kcode
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,71 +0,0 @@
|
||||
module ActiveRecord
|
||||
# Active Record automatically timestamps create and update operations if the table has fields
|
||||
# named created_at/created_on or updated_at/updated_on.
|
||||
#
|
||||
# Timestamping can be turned off by setting
|
||||
# <tt>ActiveRecord::Base.record_timestamps = false</tt>
|
||||
#
|
||||
# Timestamps are in the local timezone by default but you can use UTC by setting
|
||||
# <tt>ActiveRecord::Base.default_timezone = :utc</tt>
|
||||
module Timestamp
|
||||
def self.included(base) #:nodoc:
|
||||
base.alias_method_chain :create, :timestamps
|
||||
base.alias_method_chain :update, :timestamps
|
||||
|
||||
base.class_inheritable_accessor :record_timestamps, :instance_writer => false
|
||||
base.record_timestamps = true
|
||||
end
|
||||
|
||||
# Saves the record with the updated_at/on attributes set to the current time.
|
||||
# If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised.
|
||||
# If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# product.touch # updates updated_at
|
||||
# product.touch(:designed_at) # updates the designed_at attribute
|
||||
def touch(attribute = nil)
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
if attribute
|
||||
write_attribute(attribute, current_time)
|
||||
else
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
|
||||
end
|
||||
|
||||
save!
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def create_with_timestamps #:nodoc:
|
||||
if record_timestamps
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
|
||||
write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
|
||||
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
|
||||
end
|
||||
|
||||
create_without_timestamps
|
||||
end
|
||||
|
||||
def update_with_timestamps(*args) #:nodoc:
|
||||
if record_timestamps && (!partial_updates? || changed?)
|
||||
current_time = current_time_from_proper_timezone
|
||||
|
||||
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
|
||||
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
|
||||
end
|
||||
|
||||
update_without_timestamps(*args)
|
||||
end
|
||||
|
||||
def current_time_from_proper_timezone
|
||||
self.class.default_timezone == :utc ? Time.now.utc : Time.now
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,235 +0,0 @@
|
||||
require 'thread'
|
||||
|
||||
module ActiveRecord
|
||||
# See ActiveRecord::Transactions::ClassMethods for documentation.
|
||||
module Transactions
|
||||
class TransactionError < ActiveRecordError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.class_eval do
|
||||
[:destroy, :save, :save!].each do |method|
|
||||
alias_method_chain method, :transactions
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Transactions are protective blocks where SQL statements are only permanent
|
||||
# if they can all succeed as one atomic action. The classic example is a
|
||||
# transfer between two accounts where you can only have a deposit if the
|
||||
# withdrawal succeeded and vice versa. Transactions enforce the integrity of
|
||||
# the database and guard the data against program errors or database
|
||||
# break-downs. So basically you should use transaction blocks whenever you
|
||||
# have a number of statements that must be executed together or not at all.
|
||||
# Example:
|
||||
#
|
||||
# ActiveRecord::Base.transaction do
|
||||
# david.withdrawal(100)
|
||||
# mary.deposit(100)
|
||||
# end
|
||||
#
|
||||
# This example will only take money from David and give to Mary if neither
|
||||
# +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
|
||||
# ROLLBACK that returns the database to the state before the transaction was
|
||||
# begun. Be aware, though, that the objects will _not_ have their instance
|
||||
# data returned to their pre-transactional state.
|
||||
#
|
||||
# == Different Active Record classes in a single transaction
|
||||
#
|
||||
# Though the transaction class method is called on some Active Record class,
|
||||
# the objects within the transaction block need not all be instances of
|
||||
# that class. This is because transactions are per-database connection, not
|
||||
# per-model.
|
||||
#
|
||||
# In this example a <tt>Balance</tt> record is transactionally saved even
|
||||
# though <tt>transaction</tt> is called on the <tt>Account</tt> class:
|
||||
#
|
||||
# Account.transaction do
|
||||
# balance.save!
|
||||
# account.save!
|
||||
# end
|
||||
#
|
||||
# Note that the +transaction+ method is also available as a model instance
|
||||
# method. For example, you can also do this:
|
||||
#
|
||||
# balance.transaction do
|
||||
# balance.save!
|
||||
# account.save!
|
||||
# end
|
||||
#
|
||||
# == Transactions are not distributed across database connections
|
||||
#
|
||||
# A transaction acts on a single database connection. If you have
|
||||
# multiple class-specific databases, the transaction will not protect
|
||||
# interaction among them. One workaround is to begin a transaction
|
||||
# on each class whose models you alter:
|
||||
#
|
||||
# Student.transaction do
|
||||
# Course.transaction do
|
||||
# course.enroll(student)
|
||||
# student.units += course.units
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This is a poor solution, but full distributed transactions are beyond
|
||||
# the scope of Active Record.
|
||||
#
|
||||
# == Save and destroy are automatically wrapped in a transaction
|
||||
#
|
||||
# Both Base#save and Base#destroy come wrapped in a transaction that ensures
|
||||
# that whatever you do in validations or callbacks will happen under the
|
||||
# protected cover of a transaction. So you can use validations to check for
|
||||
# values that the transaction depends on or you can raise exceptions in the
|
||||
# callbacks to rollback, including <tt>after_*</tt> callbacks.
|
||||
#
|
||||
# == Exception handling and rolling back
|
||||
#
|
||||
# Also have in mind that exceptions thrown within a transaction block will
|
||||
# be propagated (after triggering the ROLLBACK), so you should be ready to
|
||||
# catch those in your application code.
|
||||
#
|
||||
# One exception is the ActiveRecord::Rollback exception, which will trigger
|
||||
# a ROLLBACK when raised, but not be re-raised by the transaction block.
|
||||
#
|
||||
# *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
|
||||
# inside a transaction block. StatementInvalid exceptions indicate that an
|
||||
# error occurred at the database level, for example when a unique constraint
|
||||
# is violated. On some database systems, such as PostgreSQL, database errors
|
||||
# inside a transaction causes the entire transaction to become unusable
|
||||
# until it's restarted from the beginning. Here is an example which
|
||||
# demonstrates the problem:
|
||||
#
|
||||
# # Suppose that we have a Number model with a unique column called 'i'.
|
||||
# Number.transaction do
|
||||
# Number.create(:i => 0)
|
||||
# begin
|
||||
# # This will raise a unique constraint error...
|
||||
# Number.create(:i => 0)
|
||||
# rescue ActiveRecord::StatementInvalid
|
||||
# # ...which we ignore.
|
||||
# end
|
||||
#
|
||||
# # On PostgreSQL, the transaction is now unusable. The following
|
||||
# # statement will cause a PostgreSQL error, even though the unique
|
||||
# # constraint is no longer violated:
|
||||
# Number.create(:i => 1)
|
||||
# # => "PGError: ERROR: current transaction is aborted, commands
|
||||
# # ignored until end of transaction block"
|
||||
# end
|
||||
#
|
||||
# One should restart the entire transaction if a StatementError occurred.
|
||||
#
|
||||
# == Nested transactions
|
||||
#
|
||||
# #transaction calls can be nested. By default, this makes all database
|
||||
# statements in the nested transaction block become part of the parent
|
||||
# transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => empty
|
||||
#
|
||||
# It is also possible to requires a sub-transaction by passing
|
||||
# <tt>:requires_new => true</tt>. If anything goes wrong, the
|
||||
# database rolls back to the beginning of the sub-transaction
|
||||
# without rolling back the parent transaction. For example:
|
||||
#
|
||||
# User.transaction do
|
||||
# User.create(:username => 'Kotori')
|
||||
# User.transaction(:requires_new => true) do
|
||||
# User.create(:username => 'Nemu')
|
||||
# raise ActiveRecord::Rollback
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(:all) # => Returns only Kotori
|
||||
#
|
||||
# Most databases don't support true nested transactions. At the time of
|
||||
# writing, the only database that we're aware of that supports true nested
|
||||
# transactions, is MS-SQL. Because of this, Active Record emulates nested
|
||||
# transactions by using savepoints. See
|
||||
# http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
|
||||
# for more information about savepoints.
|
||||
#
|
||||
# === Caveats
|
||||
#
|
||||
# If you're on MySQL, then do not use DDL operations in nested transactions
|
||||
# blocks that are emulated with savepoints. That is, do not execute statements
|
||||
# like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
|
||||
# releases all savepoints upon executing a DDL operation. When #transaction
|
||||
# is finished and tries to release the savepoint it created earlier, a
|
||||
# database error will occur because the savepoint has already been
|
||||
# automatically released. The following example demonstrates the problem:
|
||||
#
|
||||
# Model.connection.transaction do # BEGIN
|
||||
# Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
|
||||
# Model.connection.create_table(...) # active_record_1 now automatically released
|
||||
# end # RELEASE savepoint active_record_1
|
||||
# # ^^^^ BOOM! database error!
|
||||
# end
|
||||
module ClassMethods
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(options = {}, &block)
|
||||
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
|
||||
connection.transaction(options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
|
||||
def transaction(&block)
|
||||
self.class.transaction(&block)
|
||||
end
|
||||
|
||||
def destroy_with_transactions #:nodoc:
|
||||
with_transaction_returning_status(:destroy_without_transactions)
|
||||
end
|
||||
|
||||
def save_with_transactions(perform_validation = true) #:nodoc:
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
|
||||
end
|
||||
|
||||
# Reset id and @new_record if the transaction rolls back.
|
||||
def rollback_active_record_state!
|
||||
id_present = has_attribute?(self.class.primary_key)
|
||||
previous_id = id
|
||||
previous_new_record = new_record?
|
||||
yield
|
||||
rescue Exception
|
||||
@new_record = previous_new_record
|
||||
if id_present
|
||||
self.id = previous_id
|
||||
else
|
||||
@attributes.delete(self.class.primary_key)
|
||||
@attributes_cache.delete(self.class.primary_key)
|
||||
end
|
||||
raise
|
||||
end
|
||||
|
||||
# Executes +method+ within a transaction and captures its return value as a
|
||||
# status flag. If the status is true the transaction is committed, otherwise
|
||||
# a ROLLBACK is issued. In any case the status flag is returned.
|
||||
#
|
||||
# This method is available within the context of an ActiveRecord::Base
|
||||
# instance.
|
||||
def with_transaction_returning_status(method, *args)
|
||||
status = nil
|
||||
self.class.transaction do
|
||||
status = send(method, *args)
|
||||
raise ActiveRecord::Rollback unless status
|
||||
end
|
||||
status
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,1134 +0,0 @@
|
||||
module ActiveRecord
|
||||
# Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
|
||||
# +record+ method to retrieve the record which did not validate.
|
||||
# begin
|
||||
# complex_operation_that_calls_save!_internally
|
||||
# rescue ActiveRecord::RecordInvalid => invalid
|
||||
# puts invalid.record.errors
|
||||
# end
|
||||
class RecordInvalid < ActiveRecordError
|
||||
attr_reader :record
|
||||
def initialize(record)
|
||||
@record = record
|
||||
errors = @record.errors.full_messages.join(I18n.t('support.array.words_connector', :default => ', '))
|
||||
super(I18n.t('activerecord.errors.messages.record_invalid', :errors => errors))
|
||||
end
|
||||
end
|
||||
|
||||
class Error
|
||||
attr_accessor :base, :attribute, :type, :message, :options
|
||||
|
||||
def initialize(base, attribute, type = nil, options = {})
|
||||
self.base = base
|
||||
self.attribute = attribute
|
||||
self.type = type || :invalid
|
||||
self.options = options
|
||||
self.message = options.delete(:message) || self.type
|
||||
end
|
||||
|
||||
def message
|
||||
# When type is a string, it means that we do not have to do a lookup, because
|
||||
# the user already sent the "final" message.
|
||||
type.is_a?(String) ? type : generate_message(default_options)
|
||||
end
|
||||
|
||||
def full_message
|
||||
attribute.to_s == 'base' ? message : generate_full_message(default_options)
|
||||
end
|
||||
|
||||
alias :to_s :message
|
||||
|
||||
def value
|
||||
@base.respond_to?(attribute) ? @base.send(attribute) : nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
|
||||
# Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
|
||||
# it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
|
||||
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
|
||||
# translated attribute name and the value are available for interpolation.
|
||||
#
|
||||
# When using inheritence in your models, it will check all the inherited models too, but only if the model itself
|
||||
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
|
||||
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
|
||||
#
|
||||
# <ol>
|
||||
# <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.admin.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.models.user.blank</tt></li>
|
||||
# <li><tt>activerecord.errors.messages.blank</tt></li>
|
||||
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
|
||||
# </ol>
|
||||
def generate_message(options = {})
|
||||
keys = @base.class.self_and_descendants_from_active_record.map do |klass|
|
||||
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{@message}",
|
||||
:"models.#{klass.name.underscore}.#{@message}" ]
|
||||
end.flatten
|
||||
|
||||
keys << options.delete(:default)
|
||||
keys << :"messages.#{@message}"
|
||||
keys << @message if @message.is_a?(String)
|
||||
keys << @type unless @type == @message
|
||||
keys.compact!
|
||||
|
||||
options.merge!(:default => keys)
|
||||
I18n.translate(keys.shift, options)
|
||||
end
|
||||
|
||||
# Wraps an error message into a full_message format.
|
||||
#
|
||||
# The default full_message format for any locale is <tt>"{{attribute}} {{message}}"</tt>.
|
||||
# One can specify locale specific default full_message format by storing it as a
|
||||
# translation for the key <tt>:"activerecord.errors.full_messages.format"</tt>.
|
||||
#
|
||||
# Additionally one can specify a validation specific error message format by
|
||||
# storing a translation for <tt>:"activerecord.errors.full_messages.[message_key]"</tt>.
|
||||
# E.g. the full_message format for any validation that uses :blank as a message
|
||||
# key (such as validates_presence_of) can be stored to <tt>:"activerecord.errors.full_messages.blank".</tt>
|
||||
#
|
||||
# Because the message key used by a validation can be overwritten on the
|
||||
# <tt>validates_*</tt> class macro level one can customize the full_message format for
|
||||
# any particular validation:
|
||||
#
|
||||
# # app/models/article.rb
|
||||
# class Article < ActiveRecord::Base
|
||||
# validates_presence_of :title, :message => :"title.blank"
|
||||
# end
|
||||
#
|
||||
# # config/locales/en.yml
|
||||
# en:
|
||||
# activerecord:
|
||||
# errors:
|
||||
# full_messages:
|
||||
# title:
|
||||
# blank: This title is screwed!
|
||||
def generate_full_message(options = {})
|
||||
keys = [
|
||||
:"full_messages.#{@message}",
|
||||
:'full_messages.format',
|
||||
'%{attribute} %{message}'
|
||||
]
|
||||
options.merge!(:default => keys, :message => self.message)
|
||||
I18n.translate(keys.shift, options)
|
||||
end
|
||||
|
||||
# Return user options with default options.
|
||||
#
|
||||
def default_options
|
||||
options.reverse_merge :scope => [:activerecord, :errors],
|
||||
:model => @base.class.human_name,
|
||||
:attribute => @base.class.human_attribute_name(attribute.to_s),
|
||||
:value => value
|
||||
end
|
||||
end
|
||||
|
||||
# Active Record validation is reported to and from this object, which is used by Base#save to
|
||||
# determine whether the object is in a valid state to be saved. See usage example in Validations.
|
||||
class Errors
|
||||
include Enumerable
|
||||
|
||||
class << self
|
||||
def default_error_messages
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
|
||||
I18n.translate 'activerecord.errors.messages'
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(base) # :nodoc:
|
||||
@base = base
|
||||
clear
|
||||
end
|
||||
|
||||
# Adds an error to the base object instead of any particular attribute. This is used
|
||||
# to report errors that don't tie to any specific attribute, but rather to the object
|
||||
# as a whole. These error messages don't get prepended with any field name when iterating
|
||||
# with +each_full+, so they should be complete sentences.
|
||||
def add_to_base(msg)
|
||||
add(:base, msg)
|
||||
end
|
||||
|
||||
# Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
|
||||
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
|
||||
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
|
||||
# If no +messsage+ is supplied, :invalid is assumed.
|
||||
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
|
||||
#
|
||||
def add(attribute, message = nil, options = {})
|
||||
options[:message] = options.delete(:default) if options.has_key?(:default)
|
||||
error, message = message, nil if message.is_a?(Error)
|
||||
|
||||
@errors[attribute.to_s] ||= []
|
||||
@errors[attribute.to_s] << (error || Error.new(@base, attribute, message, options))
|
||||
end
|
||||
|
||||
# Will add an error message to each of the attributes in +attributes+ that is empty.
|
||||
def add_on_empty(attributes, custom_message = nil)
|
||||
for attr in [attributes].flatten
|
||||
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
|
||||
is_empty = value.respond_to?(:empty?) ? value.empty? : false
|
||||
add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
|
||||
end
|
||||
end
|
||||
|
||||
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
|
||||
def add_on_blank(attributes, custom_message = nil)
|
||||
for attr in [attributes].flatten
|
||||
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
|
||||
add(attr, :blank, :default => custom_message) if value.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has errors associated with it.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.invalid?(:name) # => true
|
||||
# company.errors.invalid?(:address) # => false
|
||||
def invalid?(attribute)
|
||||
!@errors[attribute.to_s].nil?
|
||||
end
|
||||
|
||||
# Returns +nil+, if no errors are associated with the specified +attribute+.
|
||||
# Returns the error message, if one error is associated with the specified +attribute+.
|
||||
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.on(:name) # => ["is too short (minimum is 5 characters)", "can't be blank"]
|
||||
# company.errors.on(:email) # => "can't be blank"
|
||||
# company.errors.on(:address) # => nil
|
||||
def on(attribute)
|
||||
attribute = attribute.to_s
|
||||
return nil unless @errors.has_key?(attribute)
|
||||
errors = @errors[attribute].map(&:to_s)
|
||||
errors.size == 1 ? errors.first : errors
|
||||
end
|
||||
|
||||
alias :[] :on
|
||||
|
||||
# Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
|
||||
def on_base
|
||||
on(:base)
|
||||
end
|
||||
|
||||
# Yields each attribute and associated message per error added.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each{|attr,msg| puts "#{attr} - #{msg}" }
|
||||
# # => name - is too short (minimum is 5 characters)
|
||||
# # name - can't be blank
|
||||
# # address - can't be blank
|
||||
def each
|
||||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error.message } }
|
||||
end
|
||||
|
||||
def each_error
|
||||
@errors.each_key { |attr| @errors[attr].each { |error| yield attr, error } }
|
||||
end
|
||||
|
||||
# Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
|
||||
# through iteration as "First name can't be empty".
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.each_full{|msg| puts msg }
|
||||
# # => Name is too short (minimum is 5 characters)
|
||||
# # Name can't be blank
|
||||
# # Address can't be blank
|
||||
def each_full
|
||||
full_messages.each { |msg| yield msg }
|
||||
end
|
||||
|
||||
# Returns all the full error messages in an array.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.full_messages # =>
|
||||
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
|
||||
def full_messages(options = {})
|
||||
@errors.values.inject([]) do |full_messages, errors|
|
||||
full_messages + errors.map { |error| error.full_message }
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if no errors have been added.
|
||||
def empty?
|
||||
@errors.empty?
|
||||
end
|
||||
|
||||
# Removes all errors that have been added.
|
||||
def clear
|
||||
@errors = ActiveSupport::OrderedHash.new
|
||||
end
|
||||
|
||||
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
|
||||
def size
|
||||
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
||||
end
|
||||
|
||||
alias_method :count, :size
|
||||
alias_method :length, :size
|
||||
|
||||
# Returns an XML representation of this error object.
|
||||
#
|
||||
# class Company < ActiveRecord::Base
|
||||
# validates_presence_of :name, :address, :email
|
||||
# validates_length_of :name, :in => 5..30
|
||||
# end
|
||||
#
|
||||
# company = Company.create(:address => '123 First St.')
|
||||
# company.errors.to_xml
|
||||
# # => <?xml version="1.0" encoding="UTF-8"?>
|
||||
# # <errors>
|
||||
# # <error>Name is too short (minimum is 5 characters)</error>
|
||||
# # <error>Name can't be blank</error>
|
||||
# # <error>Address can't be blank</error>
|
||||
# # </errors>
|
||||
def to_xml(options={})
|
||||
options[:root] ||= "errors"
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
options[:builder].instruct! unless options.delete(:skip_instruct)
|
||||
options[:builder].errors do |e|
|
||||
full_messages.each { |msg| e.error(msg) }
|
||||
end
|
||||
end
|
||||
|
||||
def generate_message(attribute, message = :invalid, options = {})
|
||||
ActiveSupport::Deprecation.warn("ActiveRecord::Errors#generate_message has been deprecated. Please use ActiveRecord::Error.new().to_s.")
|
||||
Error.new(@base, attribute, message, options).to_s
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
|
||||
#
|
||||
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
|
||||
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
|
||||
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# protected
|
||||
# def validate
|
||||
# errors.add_on_empty %w( first_name last_name )
|
||||
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
|
||||
# end
|
||||
#
|
||||
# def validate_on_create # is only run the first time a new object is saved
|
||||
# unless valid_discount?(membership_discount)
|
||||
# errors.add("membership_discount", "has expired")
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def validate_on_update
|
||||
# errors.add_to_base("No changes have occurred") if unchanged_attributes?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.new("first_name" => "David", "phone_number" => "what?")
|
||||
# person.save # => false (and doesn't do the save)
|
||||
# person.errors.empty? # => false
|
||||
# person.errors.count # => 2
|
||||
# person.errors.on "last_name" # => "can't be empty"
|
||||
# person.errors.on "phone_number" # => "has invalid format"
|
||||
# person.errors.each_full { |msg| puts msg }
|
||||
# # => "Last name can't be empty\n" +
|
||||
# # "Phone number has invalid format"
|
||||
#
|
||||
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
|
||||
# person.save # => true (and person is now saved in the database)
|
||||
#
|
||||
# An Errors object is automatically created for every Active Record.
|
||||
module Validations
|
||||
VALIDATIONS = %w( validate validate_on_create validate_on_update )
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend ClassMethods
|
||||
base.class_eval do
|
||||
alias_method_chain :save, :validation
|
||||
alias_method_chain :save!, :validation
|
||||
end
|
||||
|
||||
base.send :include, ActiveSupport::Callbacks
|
||||
base.define_callbacks *VALIDATIONS
|
||||
end
|
||||
|
||||
# Active Record classes can implement validations in several ways. The highest level, easiest to read,
|
||||
# and recommended approach is to use the declarative <tt>validates_..._of</tt> class methods (and
|
||||
# +validates_associated+) documented below. These are sufficient for most model validations.
|
||||
#
|
||||
# Slightly lower level is +validates_each+. It provides some of the same options as the purely declarative
|
||||
# validation methods, but like all the lower-level approaches it requires manually adding to the errors collection
|
||||
# when the record is invalid.
|
||||
#
|
||||
# At a yet lower level, a model can use the class methods +validate+, +validate_on_create+ and +validate_on_update+
|
||||
# to add validation methods or blocks. These are ActiveSupport::Callbacks and follow the same rules of inheritance
|
||||
# and chaining.
|
||||
#
|
||||
# The lowest level style is to define the instance methods +validate+, +validate_on_create+ and +validate_on_update+
|
||||
# as documented in ActiveRecord::Validations.
|
||||
#
|
||||
# == +validate+, +validate_on_create+ and +validate_on_update+ Class Methods
|
||||
#
|
||||
# Calls to these methods add a validation method or block to the class. Again, this approach is recommended
|
||||
# only when the higher-level methods documented below (<tt>validates_..._of</tt> and +validates_associated+) are
|
||||
# insufficient to handle the required validation.
|
||||
#
|
||||
# This can be done with a symbol pointing to a method:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate :must_be_friends
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Or with a block which is passed the current record to be validated:
|
||||
#
|
||||
# class Comment < ActiveRecord::Base
|
||||
# validate do |comment|
|
||||
# comment.must_be_friends
|
||||
# end
|
||||
#
|
||||
# def must_be_friends
|
||||
# errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# This usage applies to +validate_on_create+ and +validate_on_update+ as well.
|
||||
module ClassMethods
|
||||
DEFAULT_VALIDATION_OPTIONS = {
|
||||
:on => :save,
|
||||
:allow_nil => false,
|
||||
:allow_blank => false,
|
||||
:message => nil
|
||||
}.freeze
|
||||
|
||||
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
|
||||
ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
|
||||
:equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
|
||||
:odd => 'odd?', :even => 'even?' }.freeze
|
||||
|
||||
# Validates each attribute against a block.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_each :first_name, :last_name do |record, attr, value|
|
||||
# record.errors.add attr, 'starts with z.' if value[0] == ?z
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Options:
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
|
||||
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_each(*attrs)
|
||||
options = attrs.extract_options!.symbolize_keys
|
||||
attrs = attrs.flatten
|
||||
|
||||
# Declare the validation.
|
||||
send(validation_method(options[:on] || :save), options) do |record|
|
||||
attrs.each do |attr|
|
||||
value = record.send(attr)
|
||||
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
|
||||
yield record, attr, value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
|
||||
#
|
||||
# Model:
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_confirmation_of :user_name, :password
|
||||
# validates_confirmation_of :email_address, :message => "should match confirmation"
|
||||
# end
|
||||
#
|
||||
# View:
|
||||
# <%= password_field "person", "password" %>
|
||||
# <%= password_field "person", "password_confirmation" %>
|
||||
#
|
||||
# The added +password_confirmation+ attribute is virtual; it exists only as an in-memory attribute for validating the password.
|
||||
# To achieve this, the validation adds accessors to the model for the confirmation attribute. NOTE: This check is performed
|
||||
# only if +password_confirmation+ is not +nil+, and by default only on save. To require confirmation, make sure to add a presence
|
||||
# check for the confirmation attribute:
|
||||
#
|
||||
# validates_presence_of :password_confirmation, :if => :password_changed?
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - A custom error message (default is: "doesn't match confirmation").
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_confirmation_of(*attr_names)
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
|
||||
record.errors.add(attr_name, :confirmation, :default => configuration[:message])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_acceptance_of :terms_of_service
|
||||
# validates_acceptance_of :eula, :message => "must be abided"
|
||||
# end
|
||||
#
|
||||
# If the database column does not exist, the +terms_of_service+ attribute is entirely virtual. This check is
|
||||
# performed only if +terms_of_service+ is not +nil+ and by default on save.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - A custom error message (default is: "must be accepted").
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is true).
|
||||
# * <tt>:accept</tt> - Specifies value that is considered accepted. The default value is a string "1", which
|
||||
# makes it easy to relate to an HTML checkbox. This should be set to +true+ if you are validating a database
|
||||
# column, since the attribute is typecast from "1" to +true+ before validation.
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_acceptance_of(*attr_names)
|
||||
configuration = { :on => :save, :allow_nil => true, :accept => "1" }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
db_cols = begin
|
||||
column_names
|
||||
rescue Exception # To ignore both statement and connection errors
|
||||
[]
|
||||
end
|
||||
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
|
||||
attr_accessor(*names)
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
unless value == configuration[:accept]
|
||||
record.errors.add(attr_name, :accepted, :default => configuration[:message])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_presence_of :first_name
|
||||
# end
|
||||
#
|
||||
# The first_name attribute must be in the object and it cannot be blank.
|
||||
#
|
||||
# If you want to validate the presence of a boolean field (where the real values are true and false),
|
||||
# you will want to use <tt>validates_inclusion_of :field_name, :in => [true, false]</tt>.
|
||||
#
|
||||
# This is due to the way Object#blank? handles boolean values: <tt>false.blank? # => true</tt>.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>message</tt> - A custom error message (default is: "can't be blank").
|
||||
# * <tt>on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>,
|
||||
# <tt>:update</tt>).
|
||||
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
|
||||
# The method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>).
|
||||
# The method, proc or string should return or evaluate to a true or false value.
|
||||
#
|
||||
def validates_presence_of(*attr_names)
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
# can't use validates_each here, because it cannot cope with nonexistent attributes,
|
||||
# while errors.add_on_empty can
|
||||
send(validation_method(configuration[:on]), configuration) do |record|
|
||||
record.errors.add_on_blank(attr_names, configuration[:message])
|
||||
end
|
||||
end
|
||||
|
||||
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_length_of :first_name, :maximum=>30
|
||||
# validates_length_of :last_name, :maximum=>30, :message=>"less than {{count}} if you don't mind"
|
||||
# validates_length_of :fax, :in => 7..32, :allow_nil => true
|
||||
# validates_length_of :phone, :in => 7..32, :allow_blank => true
|
||||
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
|
||||
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least {{count}} character"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with {{count}} characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least {{count}} words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:minimum</tt> - The minimum size of the attribute.
|
||||
# * <tt>:maximum</tt> - The maximum size of the attribute.
|
||||
# * <tt>:is</tt> - The exact size of the attribute.
|
||||
# * <tt>:within</tt> - A range specifying the minimum and maximum size of the attribute.
|
||||
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
|
||||
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
|
||||
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
|
||||
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is {{count}} characters)").
|
||||
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is {{count}} characters)").
|
||||
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be {{count}} characters)").
|
||||
# * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
|
||||
# count words as in above example.)
|
||||
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
|
||||
def validates_length_of(*attrs)
|
||||
# Merge given options with defaults.
|
||||
options = {
|
||||
:tokenizer => lambda {|value| value.split(//)}
|
||||
}.merge(DEFAULT_VALIDATION_OPTIONS)
|
||||
options.update(attrs.extract_options!.symbolize_keys)
|
||||
|
||||
# Ensure that one and only one range option is specified.
|
||||
range_options = ALL_RANGE_OPTIONS & options.keys
|
||||
case range_options.size
|
||||
when 0
|
||||
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
|
||||
when 1
|
||||
# Valid number of options; do nothing.
|
||||
else
|
||||
raise ArgumentError, 'Too many range options specified. Choose only one.'
|
||||
end
|
||||
|
||||
# Get range option and value.
|
||||
option = range_options.first
|
||||
option_value = options[range_options.first]
|
||||
key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
|
||||
custom_message = options[:message] || options[key]
|
||||
|
||||
case option
|
||||
when :within, :in
|
||||
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
|
||||
|
||||
validates_each(attrs, options) do |record, attr, value|
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
if value.nil? or value.size < option_value.begin
|
||||
record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin)
|
||||
elsif value.size > option_value.end
|
||||
record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end)
|
||||
end
|
||||
end
|
||||
when :is, :minimum, :maximum
|
||||
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
|
||||
|
||||
# Declare different validations per option.
|
||||
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
|
||||
|
||||
validates_each(attrs, options) do |record, attr, value|
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
|
||||
record.errors.add(attr, key, :default => custom_message, :count => option_value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :validates_size_of, :validates_length_of
|
||||
|
||||
|
||||
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
|
||||
# can be named "davidhh".
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_uniqueness_of :user_name, :scope => :account_id
|
||||
# end
|
||||
#
|
||||
# It can also validate whether the value of the specified attributes are unique based on multiple scope parameters. For example,
|
||||
# making sure that a teacher can only be on the schedule once per semester for a particular class.
|
||||
#
|
||||
# class TeacherSchedule < ActiveRecord::Base
|
||||
# validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
|
||||
# end
|
||||
#
|
||||
# When the record is created, a check is performed to make sure that no record exists in the database with the given value for the specified
|
||||
# attribute (that maps to a column). When the record is updated, the same check is made but disregarding the record itself.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
|
||||
# * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
|
||||
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
|
||||
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
||||
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
#
|
||||
# === Concurrency and integrity
|
||||
#
|
||||
# Using this validation method in conjunction with ActiveRecord::Base#save
|
||||
# does not guarantee the absence of duplicate record insertions, because
|
||||
# uniqueness checks on the application level are inherently prone to race
|
||||
# conditions. For example, suppose that two users try to post a Comment at
|
||||
# the same time, and a Comment's title must be unique. At the database-level,
|
||||
# the actions performed by these users could be interleaved in the following manner:
|
||||
#
|
||||
# User 1 | User 2
|
||||
# ------------------------------------+--------------------------------------
|
||||
# # User 1 checks whether there's |
|
||||
# # already a comment with the title |
|
||||
# # 'My Post'. This is not the case. |
|
||||
# SELECT * FROM comments |
|
||||
# WHERE title = 'My Post' |
|
||||
# |
|
||||
# | # User 2 does the same thing and also
|
||||
# | # infers that his title is unique.
|
||||
# | SELECT * FROM comments
|
||||
# | WHERE title = 'My Post'
|
||||
# |
|
||||
# # User 1 inserts his comment. |
|
||||
# INSERT INTO comments |
|
||||
# (title, content) VALUES |
|
||||
# ('My Post', 'hi!') |
|
||||
# |
|
||||
# | # User 2 does the same thing.
|
||||
# | INSERT INTO comments
|
||||
# | (title, content) VALUES
|
||||
# | ('My Post', 'hello!')
|
||||
# |
|
||||
# | # ^^^^^^
|
||||
# | # Boom! We now have a duplicate
|
||||
# | # title!
|
||||
#
|
||||
# This could even happen if you use transactions with the 'serializable'
|
||||
# isolation level. There are several ways to get around this problem:
|
||||
# - By locking the database table before validating, and unlocking it after
|
||||
# saving. However, table locking is very expensive, and thus not
|
||||
# recommended.
|
||||
# - By locking a lock file before validating, and unlocking it after saving.
|
||||
# This does not work if you've scaled your Rails application across
|
||||
# multiple web servers (because they cannot share lock files, or cannot
|
||||
# do that efficiently), and thus not recommended.
|
||||
# - Creating a unique index on the field, by using
|
||||
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
|
||||
# rare case that a race condition occurs, the database will guarantee
|
||||
# the field's uniqueness.
|
||||
#
|
||||
# When the database catches such a duplicate insertion,
|
||||
# ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
|
||||
# exception. You can either choose to let this error propagate (which
|
||||
# will result in the default Rails exception page being shown), or you
|
||||
# can catch it and restart the transaction (e.g. by telling the user
|
||||
# that the title already exists, and asking him to re-enter the title).
|
||||
# This technique is also known as optimistic concurrency control:
|
||||
# http://en.wikipedia.org/wiki/Optimistic_concurrency_control
|
||||
#
|
||||
# Active Record currently provides no way to distinguish unique
|
||||
# index constraint errors from other types of database errors, so you
|
||||
# will have to parse the (database-specific) exception message to detect
|
||||
# such a case.
|
||||
def validates_uniqueness_of(*attr_names)
|
||||
configuration = { :case_sensitive => true }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
# The check for an existing value should be run from a class that
|
||||
# isn't abstract. This means working down from the current class
|
||||
# (self), to the first non-abstract class. Since classes don't know
|
||||
# their subclasses, we have to build the hierarchy between self and
|
||||
# the record's class.
|
||||
class_hierarchy = [record.class]
|
||||
while class_hierarchy.first != self
|
||||
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
||||
end
|
||||
|
||||
# Now we can work our way down the tree to the first non-abstract
|
||||
# class (which has a database table to query from).
|
||||
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
|
||||
|
||||
column = finder_class.columns_hash[attr_name.to_s]
|
||||
|
||||
if value.nil?
|
||||
comparison_operator = "IS ?"
|
||||
elsif column.text?
|
||||
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
|
||||
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
|
||||
else
|
||||
comparison_operator = "= ?"
|
||||
end
|
||||
|
||||
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
|
||||
|
||||
if value.nil? || (configuration[:case_sensitive] || !column.text?)
|
||||
condition_sql = "#{sql_attribute} #{comparison_operator}"
|
||||
condition_params = [value]
|
||||
else
|
||||
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
|
||||
condition_params = [value.mb_chars.downcase]
|
||||
end
|
||||
|
||||
if scope = configuration[:scope]
|
||||
Array(scope).map do |scope_item|
|
||||
scope_value = record.send(scope_item)
|
||||
condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
|
||||
condition_params << scope_value
|
||||
end
|
||||
end
|
||||
|
||||
unless record.new_record?
|
||||
condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
|
||||
condition_params << record.send(:id)
|
||||
end
|
||||
|
||||
finder_class.with_exclusive_scope do
|
||||
if finder_class.exists?([condition_sql, *condition_params])
|
||||
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
|
||||
# provided.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
|
||||
# end
|
||||
#
|
||||
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
|
||||
#
|
||||
# A regular expression must be provided or else an exception will be raised.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
|
||||
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
||||
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
||||
# * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_format_of(*attr_names)
|
||||
configuration = { :on => :save, :with => nil }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless value.to_s =~ configuration[:with]
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates whether the value of the specified attribute is available in a particular enumerable object.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_inclusion_of :gender, :in => %w( m f )
|
||||
# validates_inclusion_of :age, :in => 0..99
|
||||
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:in</tt> - An enumerable object of available items.
|
||||
# * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
|
||||
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
||||
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_inclusion_of(*attr_names)
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
enum = configuration[:in] || configuration[:within]
|
||||
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless enum.include?(value)
|
||||
record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates that the value of the specified attribute is not in a particular enumerable object.
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
|
||||
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
|
||||
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension {{value}} is not allowed"
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
|
||||
# * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
|
||||
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
|
||||
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_exclusion_of(*attr_names)
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
enum = configuration[:in] || configuration[:within]
|
||||
|
||||
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?(:include?)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
if enum.include?(value)
|
||||
record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
|
||||
#
|
||||
# class Book < ActiveRecord::Base
|
||||
# has_many :pages
|
||||
# belongs_to :library
|
||||
#
|
||||
# validates_associated :pages, :library
|
||||
# end
|
||||
#
|
||||
# Warning: If, after the above definition, you then wrote:
|
||||
#
|
||||
# class Page < ActiveRecord::Base
|
||||
# belongs_to :book
|
||||
#
|
||||
# validates_associated :book
|
||||
# end
|
||||
#
|
||||
# this would specify a circular dependency and cause infinite recursion.
|
||||
#
|
||||
# NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association
|
||||
# is both present and guaranteed to be valid, you also need to use +validates_presence_of+.
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - A custom error message (default is: "is invalid")
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_associated(*attr_names)
|
||||
configuration = { :on => :save }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates whether the value of the specified attribute is numeric by trying to convert it to
|
||||
# a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
|
||||
# <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_numericality_of :value, :on => :create
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>:message</tt> - A custom error message (default is: "is not a number").
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:only_integer</tt> - Specifies whether the value has to be an integer, e.g. an integral value (default is +false+).
|
||||
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is +false+). Notice that for fixnum and float columns empty strings are converted to +nil+.
|
||||
# * <tt>:greater_than</tt> - Specifies the value must be greater than the supplied value.
|
||||
# * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be greater than or equal the supplied value.
|
||||
# * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied value.
|
||||
# * <tt>:less_than</tt> - Specifies the value must be less than the supplied value.
|
||||
# * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less than or equal the supplied value.
|
||||
# * <tt>:odd</tt> - Specifies the value must be an odd number.
|
||||
# * <tt>:even</tt> - Specifies the value must be an even number.
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_numericality_of(*attr_names)
|
||||
configuration = { :on => :save, :only_integer => false, :allow_nil => false }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
|
||||
numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
|
||||
|
||||
(numericality_options - [ :odd, :even ]).each do |option|
|
||||
raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric)
|
||||
end
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
raw_value = record.send("#{attr_name}_before_type_cast") || value
|
||||
|
||||
next if configuration[:allow_nil] and raw_value.nil?
|
||||
|
||||
if configuration[:only_integer]
|
||||
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
|
||||
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
|
||||
next
|
||||
end
|
||||
raw_value = raw_value.to_i
|
||||
else
|
||||
begin
|
||||
raw_value = Kernel.Float(raw_value)
|
||||
rescue ArgumentError, TypeError
|
||||
record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
numericality_options.each do |option|
|
||||
case option
|
||||
when :odd, :even
|
||||
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
|
||||
record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
|
||||
end
|
||||
else
|
||||
record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Creates an object just like Base.create but calls save! instead of save
|
||||
# so an exception is raised if the record is invalid.
|
||||
def create!(attributes = nil, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| create!(attr, &block) }
|
||||
else
|
||||
object = new(attributes)
|
||||
yield(object) if block_given?
|
||||
object.save!
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def validation_method(on)
|
||||
case on
|
||||
when :save then :validate
|
||||
when :create then :validate_on_create
|
||||
when :update then :validate_on_update
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The validation process on save can be skipped by passing false. The regular Base#save method is
|
||||
# replaced with this when the validations module is mixed in, which it is by default.
|
||||
def save_with_validation(perform_validation = true)
|
||||
if perform_validation && valid? || !perform_validation
|
||||
save_without_validation
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false
|
||||
# if the record is not valid.
|
||||
def save_with_validation!
|
||||
if valid?
|
||||
save_without_validation!
|
||||
else
|
||||
raise RecordInvalid.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
# Runs +validate+ and +validate_on_create+ or +validate_on_update+ and returns true if no errors were added otherwise false.
|
||||
def valid?
|
||||
errors.clear
|
||||
|
||||
run_callbacks(:validate)
|
||||
validate
|
||||
|
||||
if new_record?
|
||||
run_callbacks(:validate_on_create)
|
||||
validate_on_create
|
||||
else
|
||||
run_callbacks(:validate_on_update)
|
||||
validate_on_update
|
||||
end
|
||||
|
||||
errors.empty?
|
||||
end
|
||||
|
||||
# Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
|
||||
def invalid?
|
||||
!valid?
|
||||
end
|
||||
|
||||
# Returns the Errors object that holds all information about attribute error messages.
|
||||
def errors
|
||||
@errors ||= Errors.new(self)
|
||||
end
|
||||
|
||||
protected
|
||||
# Overwrite this method for validation checks on all saves and use <tt>Errors.add(field, msg)</tt> for invalid attributes.
|
||||
def validate
|
||||
end
|
||||
|
||||
# Overwrite this method for validation checks used only on creation.
|
||||
def validate_on_create
|
||||
end
|
||||
|
||||
# Overwrite this method for validation checks used only on updates.
|
||||
def validate_on_update
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
module ActiveRecord
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 5
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 2005 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
module ActiveSupport
|
||||
def self.load_all!
|
||||
[Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte, SecureRandom, TimeWithZone]
|
||||
end
|
||||
|
||||
autoload :BacktraceCleaner, 'active_support/backtrace_cleaner'
|
||||
autoload :Base64, 'active_support/base64'
|
||||
autoload :BasicObject, 'active_support/basic_object'
|
||||
autoload :BufferedLogger, 'active_support/buffered_logger'
|
||||
autoload :Cache, 'active_support/cache'
|
||||
autoload :Callbacks, 'active_support/callbacks'
|
||||
autoload :Deprecation, 'active_support/deprecation'
|
||||
autoload :Duration, 'active_support/duration'
|
||||
autoload :Gzip, 'active_support/gzip'
|
||||
autoload :Inflector, 'active_support/inflector'
|
||||
autoload :Memoizable, 'active_support/memoizable'
|
||||
autoload :MessageEncryptor, 'active_support/message_encryptor'
|
||||
autoload :MessageVerifier, 'active_support/message_verifier'
|
||||
autoload :Multibyte, 'active_support/multibyte'
|
||||
autoload :OptionMerger, 'active_support/option_merger'
|
||||
autoload :OrderedHash, 'active_support/ordered_hash'
|
||||
autoload :OrderedOptions, 'active_support/ordered_options'
|
||||
autoload :Rescuable, 'active_support/rescuable'
|
||||
autoload :SecureRandom, 'active_support/secure_random'
|
||||
autoload :StringInquirer, 'active_support/string_inquirer'
|
||||
autoload :TimeWithZone, 'active_support/time_with_zone'
|
||||
autoload :TimeZone, 'active_support/values/time_zone'
|
||||
autoload :XmlMini, 'active_support/xml_mini'
|
||||
end
|
||||
|
||||
require 'active_support/vendor'
|
||||
require 'active_support/core_ext'
|
||||
require 'active_support/dependencies'
|
||||
require 'active_support/json'
|
||||
|
||||
I18n.load_path << "#{File.dirname(__FILE__)}/active_support/locale/en.yml"
|
||||
@@ -1,8 +0,0 @@
|
||||
# For forward compatibility with Rails 3.
|
||||
#
|
||||
# require 'active_support' loads a very bare minumum in Rails 3.
|
||||
# require 'active_support/all' loads the whole suite like Rails 2 did.
|
||||
#
|
||||
# To prepare for Rails 3, switch to require 'active_support/all' now.
|
||||
|
||||
require 'active_support'
|
||||
@@ -1,72 +0,0 @@
|
||||
module ActiveSupport
|
||||
# Many backtraces include too much information that's not relevant for the context. This makes it hard to find the signal
|
||||
# in the backtrace and adds debugging time. With a BacktraceCleaner, you can setup filters and silencers for your particular
|
||||
# context, so only the relevant lines are included.
|
||||
#
|
||||
# If you need to reconfigure an existing BacktraceCleaner, like the one in Rails, to show as much as possible, you can always
|
||||
# call BacktraceCleaner#remove_silencers!
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# bc = BacktraceCleaner.new
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
|
||||
# bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
|
||||
#
|
||||
# Inspired by the Quiet Backtrace gem by Thoughtbot.
|
||||
class BacktraceCleaner
|
||||
def initialize
|
||||
@filters, @silencers = [], []
|
||||
end
|
||||
|
||||
# Returns the backtrace after all filters and silencers has been run against it. Filters run first, then silencers.
|
||||
def clean(backtrace)
|
||||
silence(filter(backtrace))
|
||||
end
|
||||
|
||||
# Adds a filter from the block provided. Each line in the backtrace will be mapped against this filter.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
|
||||
# backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
def add_filter(&block)
|
||||
@filters << block
|
||||
end
|
||||
|
||||
# Adds a silencer from the block provided. If the silencer returns true for a given line, it'll be excluded from the
|
||||
# clean backtrace.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# # Will reject all lines that include the word "mongrel", like "/gems/mongrel/server.rb" or "/app/my_mongrel_server/rb"
|
||||
# backtrace_cleaner.add_silencer { |line| line =~ /mongrel/ }
|
||||
def add_silencer(&block)
|
||||
@silencers << block
|
||||
end
|
||||
|
||||
# Will remove all silencers, but leave in the filters. This is useful if your context of debugging suddenly expands as
|
||||
# you suspect a bug in the libraries you use.
|
||||
def remove_silencers!
|
||||
@silencers = []
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def filter(backtrace)
|
||||
@filters.each do |f|
|
||||
backtrace = backtrace.map { |line| f.call(line) }
|
||||
end
|
||||
|
||||
backtrace
|
||||
end
|
||||
|
||||
def silence(backtrace)
|
||||
@silencers.each do |s|
|
||||
backtrace = backtrace.reject { |line| s.call(line) }
|
||||
end
|
||||
|
||||
backtrace
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,33 +0,0 @@
|
||||
begin
|
||||
require 'base64'
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
if defined? ::Base64
|
||||
Base64 = ::Base64
|
||||
else
|
||||
# Base64 provides utility methods for encoding and de-coding binary data
|
||||
# using a base 64 representation. A base 64 representation of binary data
|
||||
# consists entirely of printable US-ASCII characters. The Base64 module
|
||||
# is included in Ruby 1.8, but has been removed in Ruby 1.9.
|
||||
module Base64
|
||||
# Encodes a string to its base 64 representation. Each 60 characters of
|
||||
# output is separated by a newline character.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"
|
||||
def self.encode64(data)
|
||||
[data].pack("m")
|
||||
end
|
||||
|
||||
# Decodes a base 64 encoded string to its original representation.
|
||||
#
|
||||
# ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==")
|
||||
# # => "Original unencoded string"
|
||||
def self.decode64(data)
|
||||
data.unpack("m").first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# A base class with no predefined methods that tries to behave like Builder's
|
||||
# BlankSlate in Ruby 1.9. In Ruby pre-1.9, this is actually the
|
||||
# Builder::BlankSlate class.
|
||||
#
|
||||
# Ruby 1.9 introduces BasicObject which differs slightly from Builder's
|
||||
# BlankSlate that has been used so far. ActiveSupport::BasicObject provides a
|
||||
# barebones base class that emulates Builder::BlankSlate while still relying on
|
||||
# Ruby 1.9's BasicObject in Ruby 1.9.
|
||||
module ActiveSupport
|
||||
if defined? ::BasicObject
|
||||
class BasicObject < ::BasicObject
|
||||
undef_method :==
|
||||
undef_method :equal?
|
||||
|
||||
# Let ActiveSupport::BasicObject at least raise exceptions.
|
||||
def raise(*args)
|
||||
::Object.send(:raise, *args)
|
||||
end
|
||||
end
|
||||
else
|
||||
require 'blankslate'
|
||||
BasicObject = BlankSlate
|
||||
end
|
||||
end
|
||||
@@ -1,127 +0,0 @@
|
||||
module ActiveSupport
|
||||
# Inspired by the buffered logger idea by Ezra
|
||||
class BufferedLogger
|
||||
module Severity
|
||||
DEBUG = 0
|
||||
INFO = 1
|
||||
WARN = 2
|
||||
ERROR = 3
|
||||
FATAL = 4
|
||||
UNKNOWN = 5
|
||||
end
|
||||
include Severity
|
||||
|
||||
MAX_BUFFER_SIZE = 1000
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Set to false to disable the silencer
|
||||
cattr_accessor :silencer
|
||||
self.silencer = true
|
||||
|
||||
# Silences the logger for the duration of the block.
|
||||
def silence(temporary_level = ERROR)
|
||||
if silencer
|
||||
begin
|
||||
old_logger_level, self.level = level, temporary_level
|
||||
yield self
|
||||
ensure
|
||||
self.level = old_logger_level
|
||||
end
|
||||
else
|
||||
yield self
|
||||
end
|
||||
end
|
||||
|
||||
attr_accessor :level
|
||||
attr_reader :auto_flushing
|
||||
|
||||
def initialize(log, level = DEBUG)
|
||||
@level = level
|
||||
@buffer = {}
|
||||
@auto_flushing = 1
|
||||
@guard = Mutex.new
|
||||
|
||||
if log.respond_to?(:write)
|
||||
@log = log
|
||||
elsif File.exist?(log)
|
||||
@log = open(log, (File::WRONLY | File::APPEND))
|
||||
@log.sync = true
|
||||
else
|
||||
FileUtils.mkdir_p(File.dirname(log))
|
||||
@log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
|
||||
@log.sync = true
|
||||
@log.write("# Logfile created on %s" % [Time.now.to_s])
|
||||
end
|
||||
end
|
||||
|
||||
def add(severity, message = nil, progname = nil, &block)
|
||||
return if @level > severity
|
||||
message = (message || (block && block.call) || progname).to_s
|
||||
# If a newline is necessary then create a new message ending with a newline.
|
||||
# Ensures that the original message is not mutated.
|
||||
message = "#{message}\n" unless message[-1] == ?\n
|
||||
buffer << message
|
||||
auto_flush
|
||||
message
|
||||
end
|
||||
|
||||
for severity in Severity.constants
|
||||
class_eval <<-EOT, __FILE__, __LINE__
|
||||
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
|
||||
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
|
||||
end # end
|
||||
#
|
||||
def #{severity.downcase}? # def debug?
|
||||
#{severity} >= @level # DEBUG >= @level
|
||||
end # end
|
||||
EOT
|
||||
end
|
||||
|
||||
# Set the auto-flush period. Set to true to flush after every log message,
|
||||
# to an integer to flush every N messages, or to false, nil, or zero to
|
||||
# never auto-flush. If you turn auto-flushing off, be sure to regularly
|
||||
# flush the log yourself -- it will eat up memory until you do.
|
||||
def auto_flushing=(period)
|
||||
@auto_flushing =
|
||||
case period
|
||||
when true; 1
|
||||
when false, nil, 0; MAX_BUFFER_SIZE
|
||||
when Integer; period
|
||||
else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def flush
|
||||
@guard.synchronize do
|
||||
unless buffer.empty?
|
||||
old_buffer = buffer
|
||||
@log.write(old_buffer.join)
|
||||
end
|
||||
|
||||
# Important to do this even if buffer was empty or else @buffer will
|
||||
# accumulate empty arrays for each request where nothing was logged.
|
||||
clear_buffer
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
flush
|
||||
@log.close if @log.respond_to?(:close)
|
||||
@log = nil
|
||||
end
|
||||
|
||||
protected
|
||||
def auto_flush
|
||||
flush if buffer.size >= @auto_flushing
|
||||
end
|
||||
|
||||
def buffer
|
||||
@buffer[Thread.current] ||= []
|
||||
end
|
||||
|
||||
def clear_buffer
|
||||
@buffer.delete(Thread.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,248 +0,0 @@
|
||||
require 'benchmark'
|
||||
|
||||
module ActiveSupport
|
||||
# See ActiveSupport::Cache::Store for documentation.
|
||||
module Cache
|
||||
autoload :FileStore, 'active_support/cache/file_store'
|
||||
autoload :MemoryStore, 'active_support/cache/memory_store'
|
||||
autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store'
|
||||
autoload :DRbStore, 'active_support/cache/drb_store'
|
||||
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
|
||||
autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store'
|
||||
|
||||
module Strategy
|
||||
autoload :LocalCache, 'active_support/cache/strategy/local_cache'
|
||||
end
|
||||
|
||||
# Creates a new CacheStore object according to the given options.
|
||||
#
|
||||
# If no arguments are passed to this method, then a new
|
||||
# ActiveSupport::Cache::MemoryStore object will be returned.
|
||||
#
|
||||
# If you pass a Symbol as the first argument, then a corresponding cache
|
||||
# store class under the ActiveSupport::Cache namespace will be created.
|
||||
# For example:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemoryStore object
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:drb_store)
|
||||
# # => returns a new ActiveSupport::Cache::DRbStore object
|
||||
#
|
||||
# Any additional arguments will be passed to the corresponding cache store
|
||||
# class's constructor:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache")
|
||||
# # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache")
|
||||
#
|
||||
# If the first argument is not a Symbol, then it will simply be returned:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
|
||||
# # => returns MyOwnCacheStore.new
|
||||
def self.lookup_store(*store_option)
|
||||
store, *parameters = *([ store_option ].flatten)
|
||||
|
||||
case store
|
||||
when Symbol
|
||||
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
|
||||
store_class = ActiveSupport::Cache.const_get(store_class_name)
|
||||
store_class.new(*parameters)
|
||||
when nil
|
||||
ActiveSupport::Cache::MemoryStore.new
|
||||
else
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
def self.expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
|
||||
end
|
||||
|
||||
expanded_cache_key << case
|
||||
when key.respond_to?(:cache_key)
|
||||
key.cache_key
|
||||
when key.is_a?(Array)
|
||||
key.collect { |element| expand_cache_key(element) }.to_param
|
||||
when key
|
||||
key.to_param
|
||||
end.to_s
|
||||
|
||||
expanded_cache_key
|
||||
end
|
||||
|
||||
# An abstract cache store class. There are multiple cache store
|
||||
# implementations, each having its own additional features. See the classes
|
||||
# under the ActiveSupport::Cache module, e.g.
|
||||
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
|
||||
# popular cache store for large production websites.
|
||||
#
|
||||
# ActiveSupport::Cache::Store is meant for caching strings. Some cache
|
||||
# store implementations, like MemoryStore, are able to cache arbitrary
|
||||
# Ruby objects, but don't count on every cache store to be able to do that.
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new
|
||||
#
|
||||
# cache.read("city") # => nil
|
||||
# cache.write("city", "Duckburgh")
|
||||
# cache.read("city") # => "Duckburgh"
|
||||
class Store
|
||||
cattr_accessor :logger
|
||||
|
||||
attr_reader :silence, :logger_off
|
||||
|
||||
def silence!
|
||||
@silence = true
|
||||
self
|
||||
end
|
||||
|
||||
alias silence? silence
|
||||
alias logger_off? logger_off
|
||||
|
||||
def mute
|
||||
previous_silence, @silence = defined?(@silence) && @silence, true
|
||||
yield
|
||||
ensure
|
||||
@silence = previous_silence
|
||||
end
|
||||
|
||||
# Fetches data from the cache, using the given key. If there is data in
|
||||
# the cache with the given key, then that data is returned.
|
||||
#
|
||||
# If there is no such data in the cache (a cache miss occurred), then
|
||||
# then nil will be returned. However, if a block has been passed, then
|
||||
# that block will be run in the event of a cache miss. The return value
|
||||
# of the block will be written to the cache under the given cache key,
|
||||
# and that return value will be returned.
|
||||
#
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today") # => "Monday"
|
||||
#
|
||||
# cache.fetch("city") # => nil
|
||||
# cache.fetch("city") do
|
||||
# "Duckburgh"
|
||||
# end
|
||||
# cache.fetch("city") # => "Duckburgh"
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# Setting <tt>:force => true</tt> will force a cache miss:
|
||||
#
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today", :force => true) # => nil
|
||||
#
|
||||
# Other options will be handled by the specific cache store implementation.
|
||||
# Internally, #fetch calls #read, and calls #write on a cache miss.
|
||||
# +options+ will be passed to the #read and #write calls.
|
||||
#
|
||||
# For example, MemCacheStore's #write method supports the +:expires_in+
|
||||
# option, which tells the memcached server to automatically expire the
|
||||
# cache item after a certain period. We can use this option with #fetch
|
||||
# too:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.fetch("foo", :force => true, :expires_in => 5.seconds) do
|
||||
# "bar"
|
||||
# end
|
||||
# cache.fetch("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.fetch("foo") # => nil
|
||||
def fetch(key, options = {})
|
||||
@logger_off = true
|
||||
if !options[:force] && value = read(key, options)
|
||||
@logger_off = false
|
||||
log("hit", key, options)
|
||||
value
|
||||
elsif block_given?
|
||||
@logger_off = false
|
||||
log("miss", key, options)
|
||||
|
||||
value = nil
|
||||
ms = Benchmark.ms { value = yield }
|
||||
|
||||
@logger_off = true
|
||||
write(key, value, options)
|
||||
@logger_off = false
|
||||
|
||||
log('write (will save %.2fms)' % ms, key, nil)
|
||||
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches data from the cache, using the given key. If there is data in
|
||||
# the cache with the given key, then that data is returned. Otherwise,
|
||||
# nil is returned.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
def read(key, options = nil)
|
||||
log("read", key, options)
|
||||
end
|
||||
|
||||
# Writes the given value to the cache, with the given key.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
#
|
||||
# For example, MemCacheStore supports the +:expires_in+ option, which
|
||||
# tells the memcached server to automatically expire the cache item after
|
||||
# a certain period:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.write("foo", "bar", :expires_in => 5.seconds)
|
||||
# cache.read("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.read("foo") # => nil
|
||||
def write(key, value, options = nil)
|
||||
log("write", key, options)
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
log("delete", key, options)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
log("delete matched", matcher.inspect, options)
|
||||
end
|
||||
|
||||
def exist?(key, options = nil)
|
||||
log("exist?", key, options)
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
log("incrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num + amount)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
log("decrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num - amount)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def expires_in(options)
|
||||
expires_in = options && options[:expires_in]
|
||||
|
||||
raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric)
|
||||
|
||||
expires_in || 0
|
||||
end
|
||||
|
||||
def log(operation, key, options)
|
||||
logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !silence? && !logger_off?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class CompressedMemCacheStore < MemCacheStore
|
||||
def read(name, options = nil)
|
||||
if value = super(name, (options || {}).merge(:raw => true))
|
||||
if raw?(options)
|
||||
value
|
||||
else
|
||||
Marshal.load(ActiveSupport::Gzip.decompress(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options)
|
||||
super(name, value, (options || {}).merge(:raw => true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class DRbStore < MemoryStore #:nodoc:
|
||||
attr_reader :address
|
||||
|
||||
def initialize(address = 'druby://localhost:9192')
|
||||
require 'drb' unless defined?(DRbObject)
|
||||
super()
|
||||
@address = address
|
||||
@data = DRbObject.new(nil, address)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything on the filesystem.
|
||||
class FileStore < Store
|
||||
attr_reader :cache_path
|
||||
|
||||
def initialize(cache_path)
|
||||
@cache_path = cache_path
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
ensure_cache_path(File.dirname(real_file_path(name)))
|
||||
File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
|
||||
value
|
||||
rescue => e
|
||||
logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
File.delete(real_file_path(name))
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
search_dir(@cache_path) do |f|
|
||||
if f =~ matcher
|
||||
begin
|
||||
File.delete(f)
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exist?(name, options = nil)
|
||||
super
|
||||
File.exist?(real_file_path(name))
|
||||
end
|
||||
|
||||
private
|
||||
def real_file_path(name)
|
||||
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
|
||||
end
|
||||
|
||||
def ensure_cache_path(path)
|
||||
FileUtils.makedirs(path) unless File.exist?(path)
|
||||
end
|
||||
|
||||
def search_dir(dir, &callback)
|
||||
Dir.foreach(dir) do |d|
|
||||
next if d == "." || d == ".."
|
||||
name = File.join(dir, d)
|
||||
if File.directory?(name)
|
||||
search_dir(name, &callback)
|
||||
else
|
||||
callback.call name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
require 'memcache'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores data in Memcached:
|
||||
# http://www.danga.com/memcached/
|
||||
#
|
||||
# This is currently the most popular cache store for production websites.
|
||||
#
|
||||
# Special features:
|
||||
# - Clustering and load balancing. One can specify multiple memcached servers,
|
||||
# and MemCacheStore will load balance between all available servers. If a
|
||||
# server goes down, then MemCacheStore will ignore it until it goes back
|
||||
# online.
|
||||
# - Time-based expiry support. See #write and the +:expires_in+ option.
|
||||
# - Per-request in memory cache for all communication with the MemCache server(s).
|
||||
class MemCacheStore < Store
|
||||
module Response # :nodoc:
|
||||
STORED = "STORED\r\n"
|
||||
NOT_STORED = "NOT_STORED\r\n"
|
||||
EXISTS = "EXISTS\r\n"
|
||||
NOT_FOUND = "NOT_FOUND\r\n"
|
||||
DELETED = "DELETED\r\n"
|
||||
end
|
||||
|
||||
def self.build_mem_cache(*addresses)
|
||||
addresses = addresses.flatten
|
||||
options = addresses.extract_options!
|
||||
addresses = ["localhost"] if addresses.empty?
|
||||
MemCache.new(addresses, options)
|
||||
end
|
||||
|
||||
# Creates a new MemCacheStore object, with the given memcached server
|
||||
# addresses. Each address is either a host name, or a host-with-port string
|
||||
# in the form of "host_name:port". For example:
|
||||
#
|
||||
# ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
|
||||
#
|
||||
# If no addresses are specified, then MemCacheStore will connect to
|
||||
# localhost port 11211 (the default memcached port).
|
||||
#
|
||||
# Instead of addresses one can pass in a MemCache-like object. For example:
|
||||
#
|
||||
# require 'memcached' # gem install memcached; uses C bindings to libmemcached
|
||||
# ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
|
||||
def initialize(*addresses)
|
||||
if addresses.first.respond_to?(:get)
|
||||
@data = addresses.first
|
||||
else
|
||||
@data = self.class.build_mem_cache(*addresses)
|
||||
end
|
||||
|
||||
extend Strategy::LocalCache
|
||||
end
|
||||
|
||||
# Reads multiple keys from the cache.
|
||||
def read_multi(*keys)
|
||||
@data.get_multi keys
|
||||
end
|
||||
|
||||
def read(key, options = nil) # :nodoc:
|
||||
super
|
||||
@data.get(key, raw?(options))
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
# Writes a value to the cache.
|
||||
#
|
||||
# Possible options:
|
||||
# - +:unless_exist+ - set to true if you don't want to update the cache
|
||||
# if the key is already set.
|
||||
# - +:expires_in+ - the number of seconds that this value may stay in
|
||||
# the cache. See ActiveSupport::Cache::Store#write for an example.
|
||||
def write(key, value, options = nil)
|
||||
super
|
||||
method = options && options[:unless_exist] ? :add : :set
|
||||
# memcache-client will break the connection if you send it an integer
|
||||
# in raw mode, so we convert it to a string to be sure it continues working.
|
||||
value = value.to_s if raw?(options)
|
||||
response = @data.send(method, key, value, expires_in(options), raw?(options))
|
||||
response == Response::STORED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def delete(key, options = nil) # :nodoc:
|
||||
super
|
||||
response = @data.delete(key, expires_in(options))
|
||||
response == Response::DELETED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def exist?(key, options = nil) # :nodoc:
|
||||
# Doesn't call super, cause exist? in memcache is in fact a read
|
||||
# But who cares? Reading is very fast anyway
|
||||
# Local cache is checked first, if it doesn't know then memcache itself is read from
|
||||
!read(key, options).nil?
|
||||
end
|
||||
|
||||
def increment(key, amount = 1) # :nodoc:
|
||||
log("incrementing", key, amount)
|
||||
|
||||
response = @data.incr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1) # :nodoc:
|
||||
log("decrement", key, amount)
|
||||
response = @data.decr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil) # :nodoc:
|
||||
# don't do any local caching at present, just pass
|
||||
# through and let the error happen
|
||||
super
|
||||
raise "Not supported by Memcache"
|
||||
end
|
||||
|
||||
def clear
|
||||
@data.flush_all
|
||||
end
|
||||
|
||||
def stats
|
||||
@data.stats
|
||||
end
|
||||
|
||||
private
|
||||
def raw?(options)
|
||||
options && options[:raw]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything into memory in the
|
||||
# same process. If you're running multiple Ruby on Rails server processes
|
||||
# (which is the case if you're using mongrel_cluster or Phusion Passenger),
|
||||
# then this means that your Rails server process instances won't be able
|
||||
# to share cache data with each other. If your application never performs
|
||||
# manual cache item expiry (e.g. when you're using generational cache keys),
|
||||
# then using MemoryStore is ok. Otherwise, consider carefully whether you
|
||||
# should be using this cache store.
|
||||
#
|
||||
# MemoryStore is not only able to store strings, but also arbitrary Ruby
|
||||
# objects.
|
||||
#
|
||||
# MemoryStore is not thread-safe. Use SynchronizedMemoryStore instead
|
||||
# if you need thread-safety.
|
||||
class MemoryStore < Store
|
||||
def initialize
|
||||
@data = {}
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
@data[name]
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
@data[name] = value.freeze
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
@data.delete(name)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
@data.delete_if { |k,v| k =~ matcher }
|
||||
end
|
||||
|
||||
def exist?(name,options = nil)
|
||||
super
|
||||
@data.has_key?(name)
|
||||
end
|
||||
|
||||
def clear
|
||||
@data.clear
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
module Strategy
|
||||
module LocalCache
|
||||
# this allows caching of the fact that there is nothing in the remote cache
|
||||
NULL = 'remote_cache_store:null'
|
||||
|
||||
def with_local_cache
|
||||
Thread.current[thread_local_key] = MemoryStore.new
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = nil
|
||||
end
|
||||
|
||||
def middleware
|
||||
@middleware ||= begin
|
||||
klass = Class.new
|
||||
klass.class_eval(<<-EOS, __FILE__, __LINE__)
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Thread.current[:#{thread_local_key}] = MemoryStore.new
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[:#{thread_local_key}] = nil
|
||||
end
|
||||
EOS
|
||||
klass
|
||||
end
|
||||
end
|
||||
|
||||
def read(key, options = nil)
|
||||
value = local_cache && local_cache.read(key)
|
||||
if value == NULL
|
||||
nil
|
||||
elsif value.nil?
|
||||
value = super
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
value.duplicable? ? value.dup : value
|
||||
else
|
||||
# forcing the value to be immutable
|
||||
value.duplicable? ? value.dup : value
|
||||
end
|
||||
end
|
||||
|
||||
def write(key, value, options = nil)
|
||||
value = value.to_s if respond_to?(:raw?) && raw?(options)
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
local_cache.mute { local_cache.write(key, NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def exist(key, options = nil)
|
||||
value = local_cache.read(key) if local_cache
|
||||
if value == NULL
|
||||
false
|
||||
elsif value
|
||||
true
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
local_cache.clear if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def thread_local_key
|
||||
@thread_local_key ||= "#{self.class.name.underscore}_local_cache".gsub("/", "_").to_sym
|
||||
end
|
||||
|
||||
def local_cache
|
||||
Thread.current[thread_local_key]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# Like MemoryStore, but thread-safe.
|
||||
class SynchronizedMemoryStore < MemoryStore
|
||||
def initialize
|
||||
super
|
||||
@guard = Monitor.new
|
||||
end
|
||||
|
||||
def fetch(key, options = {})
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def exist?(name,options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def clear
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,279 +0,0 @@
|
||||
module ActiveSupport
|
||||
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
|
||||
# before or after an alteration of the object state.
|
||||
#
|
||||
# Mixing in this module allows you to define callbacks in your class.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
#
|
||||
# Callbacks from parent classes are inherited.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
#
|
||||
# before_save :prepare
|
||||
# def prepare
|
||||
# puts "preparing save"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# preparing save
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
module Callbacks
|
||||
class CallbackChain < Array
|
||||
def self.build(kind, *methods, &block)
|
||||
methods, options = extract_options(*methods, &block)
|
||||
methods.map! { |method| Callback.new(kind, method, options) }
|
||||
new(methods)
|
||||
end
|
||||
|
||||
def run(object, options = {}, &terminator)
|
||||
enumerator = options[:enumerator] || :each
|
||||
|
||||
unless block_given?
|
||||
send(enumerator) { |callback| callback.call(object) }
|
||||
else
|
||||
send(enumerator) do |callback|
|
||||
result = callback.call(object)
|
||||
break result if terminator.call(result, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Decompose into more Array like behavior
|
||||
def replace_or_append!(chain)
|
||||
if index = index(chain)
|
||||
self[index] = chain
|
||||
else
|
||||
self << chain
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def find(callback, &block)
|
||||
select { |c| c == callback && (!block_given? || yield(c)) }.first
|
||||
end
|
||||
|
||||
def delete(callback)
|
||||
super(callback.is_a?(Callback) ? callback : find(callback))
|
||||
end
|
||||
|
||||
private
|
||||
def self.extract_options(*methods, &block)
|
||||
methods.flatten!
|
||||
options = methods.extract_options!
|
||||
methods << block if block_given?
|
||||
return methods, options
|
||||
end
|
||||
|
||||
def extract_options(*methods, &block)
|
||||
self.class.extract_options(*methods, &block)
|
||||
end
|
||||
end
|
||||
|
||||
class Callback
|
||||
attr_reader :kind, :method, :identifier, :options
|
||||
|
||||
def initialize(kind, method, options = {})
|
||||
@kind = kind
|
||||
@method = method
|
||||
@identifier = options[:identifier]
|
||||
@options = options
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
case other
|
||||
when Callback
|
||||
(self.identifier && self.identifier == other.identifier) || self.method == other.method
|
||||
else
|
||||
(self.identifier && self.identifier == other) || self.method == other
|
||||
end
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
self == other
|
||||
end
|
||||
|
||||
def dup
|
||||
self.class.new(@kind, @method, @options.dup)
|
||||
end
|
||||
|
||||
def hash
|
||||
if @identifier
|
||||
@identifier.hash
|
||||
else
|
||||
@method.hash
|
||||
end
|
||||
end
|
||||
|
||||
def call(*args, &block)
|
||||
evaluate_method(method, *args, &block) if should_run_callback?(*args)
|
||||
rescue LocalJumpError
|
||||
raise ArgumentError,
|
||||
"Cannot yield from a Proc type filter. The Proc must take two " +
|
||||
"arguments and execute #call on the second argument."
|
||||
end
|
||||
|
||||
private
|
||||
def evaluate_method(method, *args, &block)
|
||||
case method
|
||||
when Symbol
|
||||
object = args.shift
|
||||
object.send(method, *args, &block)
|
||||
when String
|
||||
eval(method, args.first.instance_eval { binding })
|
||||
when Proc, Method
|
||||
method.call(*args, &block)
|
||||
else
|
||||
if method.respond_to?(kind)
|
||||
method.send(kind, *args, &block)
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
|
||||
"a block to be invoked, or an object responding to the callback method."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def should_run_callback?(*args)
|
||||
[options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
|
||||
![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def define_callbacks(*callbacks)
|
||||
callbacks.each do |callback|
|
||||
class_eval <<-"end_eval"
|
||||
def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block)
|
||||
callbacks = CallbackChain.build(:#{callback}, *methods, &block) # callbacks = CallbackChain.build(:before_save, *methods, &block)
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
@#{callback}_callbacks.concat callbacks # @before_save_callbacks.concat callbacks
|
||||
end # end
|
||||
#
|
||||
def self.#{callback}_callback_chain # def self.before_save_callback_chain
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
#
|
||||
if superclass.respond_to?(:#{callback}_callback_chain) # if superclass.respond_to?(:before_save_callback_chain)
|
||||
CallbackChain.new( # CallbackChain.new(
|
||||
superclass.#{callback}_callback_chain + # superclass.before_save_callback_chain +
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
) # )
|
||||
else # else
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
end # end
|
||||
end # end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Runs all the callbacks defined for the given options.
|
||||
#
|
||||
# If a block is given it will be called after each callback receiving as arguments:
|
||||
#
|
||||
# * the result from the callback
|
||||
# * the object which has the callback
|
||||
#
|
||||
# If the result from the block evaluates to false, the callback chain is stopped.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :pass
|
||||
# before_save :pass
|
||||
# before_save :stop
|
||||
# before_save :pass
|
||||
#
|
||||
# def pass
|
||||
# puts "pass"
|
||||
# end
|
||||
#
|
||||
# def stop
|
||||
# puts "stop"
|
||||
# return false
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# result = run_callbacks(:before_save) { |result, object| result == false }
|
||||
# puts "- save" if result
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# pass
|
||||
# pass
|
||||
# stop
|
||||
def run_callbacks(kind, options = {}, &block)
|
||||
self.class.send("#{kind}_callback_chain").run(self, options, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
filenames = Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.map do |path|
|
||||
File.basename(path, '.rb')
|
||||
end
|
||||
|
||||
# deprecated
|
||||
filenames -= %w(blank)
|
||||
|
||||
filenames.each { |filename| require "active_support/core_ext/#{filename}" }
|
||||
@@ -1,15 +0,0 @@
|
||||
require 'active_support/core_ext/array/access'
|
||||
require 'active_support/core_ext/array/conversions'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'active_support/core_ext/array/grouping'
|
||||
require 'active_support/core_ext/array/random_access'
|
||||
require 'active_support/core_ext/array/wrapper'
|
||||
|
||||
class Array #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Array::Access
|
||||
include ActiveSupport::CoreExtensions::Array::Conversions
|
||||
include ActiveSupport::CoreExtensions::Array::ExtractOptions
|
||||
include ActiveSupport::CoreExtensions::Array::Grouping
|
||||
include ActiveSupport::CoreExtensions::Array::RandomAccess
|
||||
extend ActiveSupport::CoreExtensions::Array::Wrapper
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
# Makes it easier to access parts of an array.
|
||||
module Access
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => %w( a b c d )
|
||||
# %w( a b c d ).from(2) # => %w( c d )
|
||||
# %w( a b c d ).from(10) # => nil
|
||||
# %w().from(0) # => nil
|
||||
def from(position)
|
||||
self[position..-1]
|
||||
end
|
||||
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => %w( a )
|
||||
# %w( a b c d ).to(2) # => %w( a b c )
|
||||
# %w( a b c d ).to(10) # => %w( a b c d )
|
||||
# %w().to(0) # => %w()
|
||||
def to(position)
|
||||
self[0..position]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,197 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Conversions
|
||||
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
|
||||
# * <tt>:words_connector</tt> - The sign or word used to join the elements in arrays with two or more elements (default: ", ")
|
||||
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ")
|
||||
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ")
|
||||
def to_sentence(options = {})
|
||||
default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
|
||||
default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale])
|
||||
default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
|
||||
|
||||
# Try to emulate to_senteces previous to 2.3
|
||||
if options.has_key?(:connector) || options.has_key?(:skip_last_comma)
|
||||
::ActiveSupport::Deprecation.warn(":connector has been deprecated. Use :words_connector instead", caller) if options.has_key? :connector
|
||||
::ActiveSupport::Deprecation.warn(":skip_last_comma has been deprecated. Use :last_word_connector instead", caller) if options.has_key? :skip_last_comma
|
||||
|
||||
skip_last_comma = options.delete :skip_last_comma
|
||||
if connector = options.delete(:connector)
|
||||
options[:last_word_connector] ||= skip_last_comma ? connector : ", #{connector}"
|
||||
else
|
||||
options[:last_word_connector] ||= skip_last_comma ? default_two_words_connector : default_last_word_connector
|
||||
end
|
||||
end
|
||||
|
||||
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
|
||||
options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector
|
||||
|
||||
case length
|
||||
when 0
|
||||
""
|
||||
when 1
|
||||
self[0].to_s
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Calls <tt>to_param</tt> on all its elements and joins the result with
|
||||
# slashes. This is used by <tt>url_for</tt> in Action Pack.
|
||||
def to_param
|
||||
collect { |e| e.to_param }.join '/'
|
||||
end
|
||||
|
||||
# Converts an array into a string suitable for use as a URL query string,
|
||||
# using the given +key+ as the param name.
|
||||
#
|
||||
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
|
||||
def to_query(key)
|
||||
prefix = "#{key}[]"
|
||||
collect { |value| value.to_query(prefix) }.join '&'
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
end
|
||||
end
|
||||
|
||||
# Converts a collection of elements into a formatted string by calling
|
||||
# <tt>to_s</tt> on all elements and joining them:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
|
||||
#
|
||||
# Adding in the <tt>:db</tt> argument as the format yields a prettier
|
||||
# output:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if respond_to?(:empty?) && self.empty?
|
||||
"null"
|
||||
else
|
||||
collect { |element| element.id }.join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a string that represents this array in XML by sending +to_xml+
|
||||
# to each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "records":
|
||||
#
|
||||
# [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <records type="array">
|
||||
# <record>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </record>
|
||||
# <record>
|
||||
# <baz type="integer">3</baz>
|
||||
# </record>
|
||||
# </records>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(:root => "projects")
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default root children have as node name the one of the root
|
||||
# singularized. You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(:skip_types => true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
|
||||
require 'builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize : "records"
|
||||
options[:children] ||= options[:root].singularize
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
root = options.delete(:root).to_s
|
||||
children = options.delete(:children)
|
||||
|
||||
if !options.has_key?(:dasherize) || options[:dasherize]
|
||||
root = root.dasherize
|
||||
end
|
||||
|
||||
options[:builder].instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
opts = options.merge({ :root => children })
|
||||
|
||||
xml = options[:builder]
|
||||
if empty?
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
|
||||
else
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
|
||||
yield xml if block_given?
|
||||
each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module ExtractOptions
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, :a => :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
last.is_a?(::Hash) ? pop : {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,106 +0,0 @@
|
||||
require 'enumerator'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Grouping
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", nil, nil]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", " "]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat([fill_with] * padding)
|
||||
end
|
||||
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
returning [] do |groups|
|
||||
collection.each_slice(number) { |group| groups << group }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", " "]
|
||||
# ["6", "7", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size / number gives minor group size;
|
||||
# size % number gives how many objects need extra accomodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size / number
|
||||
modulo = size % number
|
||||
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
padding = fill_with != false &&
|
||||
modulo > 0 && length == division ? 1 : 0
|
||||
groups << slice(start, length).concat([fill_with] * padding)
|
||||
start += length
|
||||
end
|
||||
|
||||
if block_given?
|
||||
groups.each{|g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
using_block = block_given?
|
||||
|
||||
inject([[]]) do |results, element|
|
||||
if (using_block && yield(element)) || (value == element)
|
||||
results << []
|
||||
else
|
||||
results.last << element
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module RandomAccess
|
||||
# Returns a random element from the array.
|
||||
def rand
|
||||
self[Kernel.rand(length)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Wrapper
|
||||
# Wraps the object in an Array unless it's an Array. Converts the
|
||||
# object to an Array using #to_ary if it implements that.
|
||||
def wrap(object)
|
||||
case object
|
||||
when nil
|
||||
[]
|
||||
when self
|
||||
object
|
||||
else
|
||||
if object.respond_to?(:to_ary)
|
||||
object.to_ary
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'active_support/base64'
|
||||
require 'active_support/core_ext/base64/encoding'
|
||||
|
||||
ActiveSupport::Base64.extend ActiveSupport::CoreExtensions::Base64::Encoding
|
||||
@@ -1,16 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Base64 #:nodoc:
|
||||
module Encoding
|
||||
# Encodes the value as base64 without the newline breaks. This makes the base64 encoding readily usable as URL parameters
|
||||
# or memcache keys without further processing.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64s("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw=="
|
||||
def encode64s(value)
|
||||
encode64(value).gsub(/\n/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
require 'benchmark'
|
||||
|
||||
class << Benchmark
|
||||
# Earlier Ruby had a slower implementation.
|
||||
if RUBY_VERSION < '1.8.7'
|
||||
remove_method :realtime
|
||||
|
||||
def realtime
|
||||
r0 = Time.now
|
||||
yield
|
||||
r1 = Time.now
|
||||
r1.to_f - r0.to_f
|
||||
end
|
||||
end
|
||||
|
||||
def ms
|
||||
1000 * realtime { yield }
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require 'bigdecimal'
|
||||
require 'active_support/core_ext/bigdecimal/conversions'
|
||||
|
||||
class BigDecimal#:nodoc:
|
||||
include ActiveSupport::CoreExtensions::BigDecimal::Conversions
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
require 'yaml'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module BigDecimal #:nodoc:
|
||||
module Conversions
|
||||
DEFAULT_STRING_FORMAT = 'F'.freeze
|
||||
YAML_TAG = 'tag:yaml.org,2002:float'.freeze
|
||||
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :_original_to_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
yaml_as YAML_TAG
|
||||
end
|
||||
end
|
||||
|
||||
def to_formatted_s(format = DEFAULT_STRING_FORMAT)
|
||||
_original_to_s(format)
|
||||
end
|
||||
|
||||
# This emits the number without any scientific notation.
|
||||
# This is better than self.to_f.to_s since it doesn't lose precision.
|
||||
#
|
||||
# Note that reconstituting YAML floats to native floats may lose precision.
|
||||
def to_yaml(opts = {})
|
||||
YAML.quick_emit(nil, opts) do |out|
|
||||
string = to_s
|
||||
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
ActiveSupport::Deprecation.warn 'require "active_support/core_ext/blank" is deprecated and will be removed in Rails 3. Use require "active_support/core_ext/object/blank" instead.'
|
||||
@@ -1,5 +0,0 @@
|
||||
require 'active_support/core_ext/cgi/escape_skipping_slashes'
|
||||
|
||||
class CGI #:nodoc:
|
||||
extend ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module CGI #:nodoc:
|
||||
module EscapeSkippingSlashes #:nodoc:
|
||||
if RUBY_VERSION >= '1.9'
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2' * $1.bytesize).join('%').upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
else
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2').first.upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/core_ext/class/inheritable_attributes'
|
||||
require 'active_support/core_ext/class/removal'
|
||||
require 'active_support/core_ext/class/delegating_attributes'
|
||||
@@ -1,54 +0,0 @@
|
||||
# Extends the class object with class and instance accessors for class attributes,
|
||||
# just like the native attr* accessors for instance attributes.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors
|
||||
# end
|
||||
#
|
||||
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
||||
class Class
|
||||
def cattr_reader(*syms)
|
||||
syms.flatten.each do |sym|
|
||||
next if sym.is_a?(Hash)
|
||||
class_eval(<<-EOS, __FILE__, __LINE__)
|
||||
unless defined? @@#{sym} # unless defined? @@hair_colors
|
||||
@@#{sym} = nil # @@hair_colors = nil
|
||||
end # end
|
||||
#
|
||||
def self.#{sym} # def self.hair_colors
|
||||
@@#{sym} # @@hair_colors
|
||||
end # end
|
||||
#
|
||||
def #{sym} # def hair_colors
|
||||
@@#{sym} # @@hair_colors
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.flatten.each do |sym|
|
||||
class_eval(<<-EOS, __FILE__, __LINE__)
|
||||
unless defined? @@#{sym} # unless defined? @@hair_colors
|
||||
@@#{sym} = nil # @@hair_colors = nil
|
||||
end # end
|
||||
#
|
||||
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
||||
@@#{sym} = obj # @@hair_colors = obj
|
||||
end # end
|
||||
#
|
||||
#{" #
|
||||
def #{sym}=(obj) # def hair_colors=(obj)
|
||||
@@#{sym} = obj # @@hair_colors = obj
|
||||
end # end
|
||||
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_accessor(*syms)
|
||||
cattr_reader(*syms)
|
||||
cattr_writer(*syms)
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
# These class attributes behave something like the class
|
||||
# inheritable accessors. But instead of copying the hash over at
|
||||
# the time the subclass is first defined, the accessors simply
|
||||
# delegate to their superclass unless they have been given a
|
||||
# specific value. This stops the strange situation where values
|
||||
# set after class definition don't get applied to subclasses.
|
||||
class Class
|
||||
def superclass_delegating_reader(*names)
|
||||
class_name_to_stop_searching_on = self.superclass.name.blank? ? "Object" : self.superclass.name
|
||||
names.each do |name|
|
||||
class_eval <<-EOS
|
||||
def self.#{name} # def self.only_reader
|
||||
if defined?(@#{name}) # if defined?(@only_reader)
|
||||
@#{name} # @only_reader
|
||||
elsif superclass < #{class_name_to_stop_searching_on} && # elsif superclass < Object &&
|
||||
superclass.respond_to?(:#{name}) # superclass.respond_to?(:only_reader)
|
||||
superclass.#{name} # superclass.only_reader
|
||||
end # end
|
||||
end # end
|
||||
def #{name} # def only_reader
|
||||
self.class.#{name} # self.class.only_reader
|
||||
end # end
|
||||
def self.#{name}? # def self.only_reader?
|
||||
!!#{name} # !!only_reader
|
||||
end # end
|
||||
def #{name}? # def only_reader?
|
||||
!!#{name} # !!only_reader
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def superclass_delegating_writer(*names)
|
||||
names.each do |name|
|
||||
class_eval <<-EOS
|
||||
def self.#{name}=(value) # def self.only_writer=(value)
|
||||
@#{name} = value # @only_writer = value
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def superclass_delegating_accessor(*names)
|
||||
superclass_delegating_reader(*names)
|
||||
superclass_delegating_writer(*names)
|
||||
end
|
||||
end
|
||||
@@ -1,140 +0,0 @@
|
||||
# Retain for backward compatibility. Methods are now included in Class.
|
||||
module ClassInheritableAttributes # :nodoc:
|
||||
end
|
||||
|
||||
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
|
||||
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
||||
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
||||
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
|
||||
class Class # :nodoc:
|
||||
def class_inheritable_reader(*syms)
|
||||
syms.each do |sym|
|
||||
next if sym.is_a?(Hash)
|
||||
class_eval <<-EOS
|
||||
def self.#{sym} # def self.before_add_for_comments
|
||||
read_inheritable_attribute(:#{sym}) # read_inheritable_attribute(:before_add_for_comments)
|
||||
end # end
|
||||
#
|
||||
def #{sym} # def before_add_for_comments
|
||||
self.class.#{sym} # self.class.before_add_for_comments
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def class_inheritable_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
class_eval <<-EOS
|
||||
def self.#{sym}=(obj) # def self.color=(obj)
|
||||
write_inheritable_attribute(:#{sym}, obj) # write_inheritable_attribute(:color, obj)
|
||||
end # end
|
||||
#
|
||||
#{" #
|
||||
def #{sym}=(obj) # def color=(obj)
|
||||
self.class.#{sym} = obj # self.class.color = obj
|
||||
end # end
|
||||
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def class_inheritable_array_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
class_eval <<-EOS
|
||||
def self.#{sym}=(obj) # def self.levels=(obj)
|
||||
write_inheritable_array(:#{sym}, obj) # write_inheritable_array(:levels, obj)
|
||||
end # end
|
||||
#
|
||||
#{" #
|
||||
def #{sym}=(obj) # def levels=(obj)
|
||||
self.class.#{sym} = obj # self.class.levels = obj
|
||||
end # end
|
||||
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def class_inheritable_hash_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
class_eval <<-EOS
|
||||
def self.#{sym}=(obj) # def self.nicknames=(obj)
|
||||
write_inheritable_hash(:#{sym}, obj) # write_inheritable_hash(:nicknames, obj)
|
||||
end # end
|
||||
#
|
||||
#{" #
|
||||
def #{sym}=(obj) # def nicknames=(obj)
|
||||
self.class.#{sym} = obj # self.class.nicknames = obj
|
||||
end # end
|
||||
" unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def class_inheritable_accessor(*syms)
|
||||
class_inheritable_reader(*syms)
|
||||
class_inheritable_writer(*syms)
|
||||
end
|
||||
|
||||
def class_inheritable_array(*syms)
|
||||
class_inheritable_reader(*syms)
|
||||
class_inheritable_array_writer(*syms)
|
||||
end
|
||||
|
||||
def class_inheritable_hash(*syms)
|
||||
class_inheritable_reader(*syms)
|
||||
class_inheritable_hash_writer(*syms)
|
||||
end
|
||||
|
||||
def inheritable_attributes
|
||||
@inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
|
||||
end
|
||||
|
||||
def write_inheritable_attribute(key, value)
|
||||
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
||||
@inheritable_attributes = {}
|
||||
end
|
||||
inheritable_attributes[key] = value
|
||||
end
|
||||
|
||||
def write_inheritable_array(key, elements)
|
||||
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
|
||||
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
|
||||
end
|
||||
|
||||
def write_inheritable_hash(key, hash)
|
||||
write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
|
||||
write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
|
||||
end
|
||||
|
||||
def read_inheritable_attribute(key)
|
||||
inheritable_attributes[key]
|
||||
end
|
||||
|
||||
def reset_inheritable_attributes
|
||||
@inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
||||
end
|
||||
|
||||
private
|
||||
# Prevent this constant from being created multiple times
|
||||
EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
|
||||
|
||||
def inherited_with_inheritable_attributes(child)
|
||||
inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
|
||||
|
||||
if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
|
||||
new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
|
||||
else
|
||||
new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
|
||||
memo.update(key => value.duplicable? ? value.dup : value)
|
||||
end
|
||||
end
|
||||
|
||||
child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
|
||||
end
|
||||
|
||||
alias inherited_without_inheritable_attributes inherited
|
||||
alias inherited inherited_with_inheritable_attributes
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
class Class #:nodoc:
|
||||
|
||||
# Unassociates the class with its subclasses and removes the subclasses
|
||||
# themselves.
|
||||
#
|
||||
# Integer.remove_subclasses # => [Bignum, Fixnum]
|
||||
# Fixnum # => NameError: uninitialized constant Fixnum
|
||||
def remove_subclasses
|
||||
Object.remove_subclasses_of(self)
|
||||
end
|
||||
|
||||
# Returns an array with the names of the subclasses of +self+ as strings.
|
||||
#
|
||||
# Integer.subclasses # => ["Bignum", "Fixnum"]
|
||||
def subclasses
|
||||
Object.subclasses_of(self).map { |o| o.to_s }
|
||||
end
|
||||
|
||||
# Removes the classes in +klasses+ from their parent module.
|
||||
#
|
||||
# Ordinary classes belong to some module via a constant. This method computes
|
||||
# that constant name from the class name and removes it from the module it
|
||||
# belongs to.
|
||||
#
|
||||
# Object.remove_class(Integer) # => [Integer]
|
||||
# Integer # => NameError: uninitialized constant Integer
|
||||
#
|
||||
# Take into account that in general the class object could be still stored
|
||||
# somewhere else.
|
||||
#
|
||||
# i = Integer # => Integer
|
||||
# Object.remove_class(Integer) # => [Integer]
|
||||
# Integer # => NameError: uninitialized constant Integer
|
||||
# i.subclasses # => ["Bignum", "Fixnum"]
|
||||
# Fixnum.superclass # => Integer
|
||||
def remove_class(*klasses)
|
||||
klasses.flatten.each do |klass|
|
||||
# Skip this class if there is nothing bound to this name
|
||||
next unless defined?(klass.name)
|
||||
|
||||
basename = klass.to_s.split("::").last
|
||||
parent = klass.parent
|
||||
|
||||
# Skip this class if it does not match the current one bound to this name
|
||||
next unless parent.const_defined?(basename) && klass = parent.const_get(basename)
|
||||
|
||||
parent.instance_eval { remove_const basename } unless parent == klass
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
require 'date'
|
||||
require 'active_support/core_ext/date/behavior'
|
||||
require 'active_support/core_ext/date/calculations'
|
||||
require 'active_support/core_ext/date/conversions'
|
||||
|
||||
class Date#:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Date::Behavior
|
||||
include ActiveSupport::CoreExtensions::Date::Calculations
|
||||
include ActiveSupport::CoreExtensions::Date::Conversions
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user