Merge branch 'rapid7' into bap-refactor
commit
eedf4520be
|
@ -1,3 +0,0 @@
|
|||
[submodule "lib/msf3"]
|
||||
path = lib/msf3
|
||||
url = git@framework.github.com:rapid7/metasploit-framework.git
|
|
@ -0,0 +1,12 @@
|
|||
source 'http://rubygems.org'
|
||||
gem 'rails', '3.2.2'
|
||||
gem 'authlogic'
|
||||
gem 'prototype_legacy_helper', '0.0.0', :git => 'git://github.com/jvennix-r7/prototype_legacy_helper.git'
|
||||
gem 'state_machine', '1.1.2'
|
||||
gem 'liquid', '2.3.0'
|
||||
gem 'ice_cube'
|
||||
gem 'acts_as_list'
|
||||
gem 'mime-types', '1.18', :git => "git://github.com/rapid7/mime-types.git"
|
||||
gem 'metasploit_data_models', '0.0.2', :git => "git://github.com/rapid7/metasploit_data_models.git"
|
||||
gem 'robots', '0.10.1'
|
||||
|
Binary file not shown.
|
@ -1,6 +1,30 @@
|
|||
Armitage Changelog
|
||||
==================
|
||||
|
||||
16 Apr 12
|
||||
---------
|
||||
- The search field in the module browser now updates results in real time.
|
||||
Start typing and Armitage will start filtering the module tree for you.
|
||||
Clear the field to reset it to the default state.
|
||||
- Added keyboard shortcuts to switch dynamic workspaces...
|
||||
Ctrl+1 = first workspace
|
||||
Ctrl+2 = second workspace
|
||||
....
|
||||
Ctrl+0 = show all
|
||||
- Added keyboard shortcuts:
|
||||
Ctrl+N = new console
|
||||
Ctrl+O = open preferences
|
||||
- Armitage's Meterpreter -> Access -> Dump Hashes -> lsass method is now
|
||||
much better about grabbing all of the hashdump output and adding it to
|
||||
the creds table. The hashdump command returns output as an arbitrary
|
||||
number of chunks. I now use a different read strategy for determining when
|
||||
the output is complete.
|
||||
- You may now use Ctrl+Alt to deselect highlighted items in a range in the
|
||||
Jobs and Workspaces table views (most other table views that do multi
|
||||
selection should allow this already).
|
||||
- Added Shell -> Pass Session for *NIX shell sessions. Uses the system_session
|
||||
module to pass a shell session elsewhere (or duplicate the current shell)
|
||||
|
||||
29 Mar 12
|
||||
---------
|
||||
- Fixed a bug that affects first-time users. Armitage was not initializing a
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class RenameWorkspaceMembers < ActiveRecord::Migration
|
||||
def up
|
||||
rename_table :project_members, :workspace_members
|
||||
end
|
||||
|
||||
def down
|
||||
rename_table :workspace_members, :project_members
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
source 'http://rubygems.org'
|
||||
gem 'rails', '3.2.2'
|
||||
gem 'authlogic'
|
||||
gem 'prototype_legacy_helper', '0.0.0', :git => 'git://github.com/jvennix-r7/prototype_legacy_helper.git'
|
||||
gem 'state_machine', '1.1.2'
|
||||
gem 'liquid', '2.3.0'
|
||||
gem 'ice_cube'
|
||||
gem 'acts_as_list'
|
||||
gem 'mime-types', '1.18', :git => "git://github.com/rapid7/mime-types.git"
|
||||
gem 'metasploit_data_models', '0.0.2', :git => "git://github.com/rapid7/metasploit_data_models.git"
|
||||
gem 'robots', '0.10.1'
|
||||
|
|
@ -105,6 +105,9 @@ public class Payload extends ClassLoader {
|
|||
classFile.getParentFile().mkdirs();
|
||||
// load ourselves via the class loader (works both on disk and from Jar)
|
||||
writeEmbeddedFile(clazz, clazzFile, classFile);
|
||||
if(props.getProperty("URL", "").startsWith("https:")) {
|
||||
writeEmbeddedFile(clazz, "metasploit/PayloadTrustManager.class", new File(classFile.getParentFile(), "PayloadTrustManager.class"));
|
||||
}
|
||||
FileOutputStream fos = new FileOutputStream(propFile);
|
||||
props.store(fos, "");
|
||||
fos.close();
|
||||
|
|
|
@ -167,6 +167,15 @@ public class Meterpreter {
|
|||
TLVPacket request = null;
|
||||
try {
|
||||
URLConnection uc = url.openConnection();
|
||||
if (url.getProtocol().equals("https")) {
|
||||
// load the trust manager via reflection, to avoid loading
|
||||
// it when it is not needed (it requires Sun Java 1.4+)
|
||||
try {
|
||||
Class.forName("com.metasploit.meterpreter.PayloadTrustManager").getMethod("useFor", new Class[] {URLConnection.class}).invoke(null, new Object[] {uc});
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace(getErrorStream());
|
||||
}
|
||||
}
|
||||
uc.setDoOutput(true);
|
||||
OutputStream out = uc.getOutputStream();
|
||||
out.write(outPacket == null ? RECV : outPacket);
|
||||
|
@ -267,6 +276,15 @@ public class Meterpreter {
|
|||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of the currently buffered error stream content, or <code>-1</code> if no buffering is active.
|
||||
*/
|
||||
public int getErrorBufferLength() {
|
||||
if (errBuffer == null)
|
||||
return -1;
|
||||
return errBuffer.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the currently buffered error stream content, or <code>null</code> if no buffering is active.
|
||||
*/
|
||||
|
@ -315,4 +333,4 @@ public class Meterpreter {
|
|||
ExtensionLoader loader = (ExtensionLoader) classLoader.loadClass(loaderName).newInstance();
|
||||
loader.load(commandManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package com.metasploit.meterpreter;
|
||||
|
||||
import java.net.URLConnection;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Trust manager used for HTTPS URL connection. This is in its own class because it
|
||||
* depends on classes only present on Sun JRE 1.4+, and incorporating it into
|
||||
* the main {@link Meterpreter} class would have made it impossible for other/older
|
||||
* JREs to load it.
|
||||
*
|
||||
* This class is substantically identical to the metasploit.PayloadTrustManager class,
|
||||
* only that it tries to cache the ssl context and trust manager between calls.
|
||||
*/
|
||||
public class PayloadTrustManager implements X509TrustManager, HostnameVerifier {
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
// no preferred issuers
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
|
||||
// trust everyone
|
||||
}
|
||||
|
||||
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
|
||||
// trust everyone
|
||||
}
|
||||
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
// trust everyone
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PayloadTrustManager instance;
|
||||
private static SSLSocketFactory factory;
|
||||
|
||||
/**
|
||||
* Called by the {@link Payload} class to modify the given
|
||||
* {@link URLConnection} so that it uses this trust manager.
|
||||
*/
|
||||
public static synchronized void useFor(URLConnection uc) throws Exception {
|
||||
if (uc instanceof HttpsURLConnection) {
|
||||
HttpsURLConnection huc = ((HttpsURLConnection) uc);
|
||||
if (instance == null) {
|
||||
instance = new PayloadTrustManager();
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, new TrustManager[] { instance }, new java.security.SecureRandom());
|
||||
factory = sc.getSocketFactory();
|
||||
}
|
||||
huc.setSSLSocketFactory(factory);
|
||||
huc.setHostnameVerifier(instance);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,13 @@ public class stdapi_fs_stat implements Command {
|
|||
|
||||
public int execute(Meterpreter meterpreter, TLVPacket request, TLVPacket response) throws Exception {
|
||||
String path = request.getStringValue(TLVType.TLV_TYPE_FILE_PATH);
|
||||
if (path.equals("...")) {
|
||||
long length = meterpreter.getErrorBufferLength();
|
||||
if (length != -1) {
|
||||
response.add(TLVType.TLV_TYPE_STAT_BUF, stat(0444 | 0100000, length, System.currentTimeMillis()));
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
}
|
||||
File file = new File(path);
|
||||
if (!file.exists())
|
||||
file = Loader.expand(path);
|
||||
|
@ -24,24 +31,28 @@ public class stdapi_fs_stat implements Command {
|
|||
}
|
||||
|
||||
public byte[] stat(File file) throws IOException {
|
||||
int mode = (file.canRead() ? 0444 : 0) | (file.canWrite() ? 0222 : 0) | (canExecute(file) ? 0110 : 0) | (file.isHidden() ? 1 : 0) | (file.isDirectory() ? 040000 : 0) | (file.isFile() ? 0100000 : 0);
|
||||
return stat(mode, file.length(), file.lastModified());
|
||||
}
|
||||
|
||||
private byte[] stat(int mode, long length, long lastModified) throws IOException {
|
||||
ByteArrayOutputStream statbuf = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(statbuf);
|
||||
dos.writeInt(le(0)); // dev
|
||||
dos.writeShort(short_le(0)); // ino
|
||||
int mode = (file.canRead() ? 0444 : 0) | (file.canWrite() ? 0222 : 0) | (canExecute(file) ? 0110 : 0) | (file.isHidden() ? 1 : 0) | (file.isDirectory() ? 040000 : 0) | (file.isFile() ? 0100000 : 0);
|
||||
dos.writeShort(short_le(mode)); // mode
|
||||
dos.writeShort(short_le(1)); // nlink
|
||||
dos.writeShort(short_le(65535)); // uid
|
||||
dos.writeShort(short_le(65535)); // gid
|
||||
dos.writeShort(short_le(0)); // padding
|
||||
dos.writeInt(le(0)); // rdev
|
||||
dos.writeInt(le((int) file.length())); // size
|
||||
int mtime = (int) (file.lastModified() / 1000);
|
||||
dos.writeInt(le((int) length)); // size
|
||||
int mtime = (int) (lastModified / 1000);
|
||||
dos.writeInt(le(mtime)); // atime
|
||||
dos.writeInt(le(mtime)); // mtime
|
||||
dos.writeInt(le(mtime)); // ctime
|
||||
dos.writeInt(le(1024)); // blksize
|
||||
dos.writeInt(le((int) ((file.length() + 1023) / 1024))); // blocks
|
||||
dos.writeInt(le((int) ((length + 1023) / 1024))); // blocks
|
||||
return statbuf.toByteArray();
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,11 @@ set_retry:
|
|||
; InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags) );
|
||||
set_security_options:
|
||||
push 0x00003380
|
||||
;0x00002000 | ; SECURITY_FLAG_IGNORE_CERT_DATE_INVALID
|
||||
;0x00001000 | ; SECURITY_FLAG_IGNORE_CERT_CN_INVALID
|
||||
;0x00000200 | ; SECURITY_FLAG_IGNORE_WRONG_USAGE
|
||||
;0x00000100 | ; SECURITY_FLAG_IGNORE_UNKNOWN_CA
|
||||
;0x00000080 ; SECURITY_FLAG_IGNORE_REVOCATION
|
||||
mov eax, esp
|
||||
push byte 4 ; sizeof(dwFlags)
|
||||
push eax ; &dwFlags
|
||||
|
|
|
@ -80,6 +80,11 @@ set_retry:
|
|||
; InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags) );
|
||||
set_security_options:
|
||||
push 0x00003380
|
||||
;0x00002000 | ; SECURITY_FLAG_IGNORE_CERT_DATE_INVALID
|
||||
;0x00001000 | ; SECURITY_FLAG_IGNORE_CERT_CN_INVALID
|
||||
;0x00000200 | ; SECURITY_FLAG_IGNORE_WRONG_USAGE
|
||||
;0x00000100 | ; SECURITY_FLAG_IGNORE_UNKNOWN_CA
|
||||
;0x00000080 ; SECURITY_FLAG_IGNORE_REVOCATION
|
||||
mov eax, esp
|
||||
push byte 4 ; sizeof(dwFlags)
|
||||
push eax ; &dwFlags
|
||||
|
|
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue