diff --git a/Armor.txt b/Armor.txt new file mode 100644 index 0000000..f2f39bd --- /dev/null +++ b/Armor.txt @@ -0,0 +1,204 @@ +name version compactsave rarity spawnable minac maxac absorbs speed reqstr block durability nodurability level levelreq cost gamble cost code namestr magic lvl auto prefix alternategfx OpenBetaGfx normcode ubercode ultracode spelloffset component invwidth invheight hasinv gemsockets gemapplytype flippyfile invfile uniqueinvfile setinvfile rArm lArm Torso Legs rSPad lSPad useable throwable stackable minstack maxstack type type2 dropsound dropsfxframe usesound unique transparent transtbl quivered lightradius belt quest missiletype durwarning qntwarning mindam maxdam StrBonus DexBonus gemoffset bitfield1 CharsiMin CharsiMax CharsiMagicMin CharsiMagicMax CharsiMagicLvl GheedMin GheedMax GheedMagicMin GheedMagicMax GheedMagicLvl AkaraMin AkaraMax AkaraMagicMin AkaraMagicMax AkaraMagicLvl FaraMin FaraMax FaraMagicMin FaraMagicMax FaraMagicLvl LysanderMin LysanderMax LysanderMagicMin LysanderMagicMax LysanderMagicLvl DrognanMin DrognanMax DrognanMagicMin DrognanMagicMax DrognanMagicLvl HraltiMin HraltiMax HraltiMagicMin HraltiMagicMax HraltiMagicLvl AlkorMin AlkorMax AlkorMagicMin AlkorMagicMax AlkorMagicLvl OrmusMin OrmusMax OrmusMagicMin OrmusMagicMax OrmusMagicLvl ElzixMin ElzixMax ElzixMagicMin ElzixMagicMax ElzixMagicLvl AshearaMin AshearaMax AshearaMagicMin AshearaMagicMax AshearaMagicLvl CainMin CainMax CainMagicMin CainMagicMax CainMagicLvl HalbuMin HalbuMax HalbuMagicMin HalbuMagicMax HalbuMagicLvl JamellaMin JamellaMax JamellaMagicMin JamellaMagicMax JamellaMagicLvl LarzukMin LarzukMax LarzukMagicMin LarzukMagicMax LarzukMagicLvl MalahMin MalahMax MalahMagicMin MalahMagicMax MalahMagicLvl DrehyaMin DrehyaMax DrehyaMagicMin DrehyaMagicMax DrehyaMagicLvl Source Art Game Art Transform InvTrans SkipName NightmareUpgrade HellUpgrade mindam maxdam nameable +Cap/hat 0 0 1 1 3 5 0 0 0 0 10 0 1 0 0 0 cap cap cap cap cap xap uap 0 0 2 2 1 3 1 flpcap invcap invcapu invcapu 0 0 0 0 0 helm item_cap 12 item_cap 0 0 5 0 0 0 0 0 3 0 0 0 0 1 2 8 0 skp ghm 0 0 1 +Skull Cap 0 0 4 1 8 11 0 0 15 0 14 0 5 0 0 0 skp skp skp skp skp xkp ukp 0 0 2 2 1 3 1 flpskp invskp 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 hlm crn 0 0 1 +Helm 0 0 4 1 15 18 0 0 26 0 19 0 11 0 0 0 hlm hlm hlm hlm hlm xlm ulm 0 0 2 2 1 3 1 flphlm invhlm invhlmu invhlmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 fhl ghm 0 0 1 +Full Helm 0 0 4 1 23 26 0 0 41 0 24 0 15 0 0 0 fhl fhl fhl hlm fhl xhl uhl 0 0 2 2 1 3 1 flpfhl invfhl invfhlu invfhlu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 0 8 0 ghm xxx 0 0 1 +Great Helm 0 0 4 1 30 35 0 0 63 0 32 0 23 0 0 0 ghm ghm ghm hlm ghm xhm uhm 0 0 2 2 1 4 1 flpghm invghm 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 7 8 0 crn xxx 0 0 1 +Crown 0 0 4 1 25 45 0 0 55 0 40 0 29 0 0 0 crn crn crn hlm crn xrn urn 0 0 2 2 1 4 1 flpcrn invcrn 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Mask 0 0 4 1 9 27 0 0 23 0 16 0 19 0 0 0 msk msk msk hlm msk xsk usk 0 0 2 2 1 4 1 flpmsk invmsk 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Quilted Armor 0 0 1 1 8 11 0 0 12 0 16 0 1 0 0 0 qui qui qlt qlt qui xui uui 0 1 2 3 1 3 1 flpqlt invqlt 0 0 0 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 7 8 0 lea gth 0 0 1 +Leather Armor 0 0 2 1 14 17 0 0 15 0 19 0 3 0 0 0 lea lea lea lea lea xea uea 0 1 2 3 1 3 1 flplea invlea 0 0 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 7 8 0 hla ful 0 0 1 +Hard Leather Armor 0 0 3 1 21 24 0 0 20 0 22 0 5 0 0 0 hla hla hla hla hla xla ula 0 1 2 3 1 3 1 flphla invhla 1 1 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 7 8 0 stu aar 0 0 1 +Studded Leather 0 0 4 1 32 35 0 0 27 0 26 0 8 0 0 0 stu stu stu stu stu xtu utu 0 1 2 3 1 3 1 flpstu invstu 1 0 0 1 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 7 8 0 rng ltp 0 0 1 +Ring Mail 0 0 4 1 45 48 0 5 36 0 21 0 11 0 0 0 rng rng rng rng rng xng ung 0 1 2 3 1 4 1 flprng invrng 0 0 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 scl chn 0 0 1 +Scale Mail 0 0 4 1 57 60 0 10 44 0 29 0 13 0 0 0 scl scl scl scl scl xcl ucl 0 1 2 3 1 3 1 flpscl invscl 1 1 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 chn brs 0 0 1 +Chain Mail 0 0 4 1 72 75 0 5 48 0 36 0 15 0 0 0 chn chn chn chn chn xhn uhn 0 1 2 3 1 3 1 flpchn invchn 1 1 1 1 2 2 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 brs spl 0 0 1 +Breast Plate 0 0 4 1 65 68 1 0 30 0 40 0 18 0 0 0 brs brs brs brs brs xrs urs 0 1 2 3 1 4 1 flpbrs invbrs 0 0 2 0 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 spl plt 0 0 1 +Splint Mail 0 0 4 1 90 95 0 5 51 0 24 0 20 0 0 0 spl spl spl spl spl xpl upl 0 1 2 3 1 3 1 flpspl invspl 1 1 2 1 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 plt fld 0 0 1 +Plate Mail 0 0 4 1 108 116 0 10 65 0 48 0 24 0 0 0 plt plt plt plt plt xlt ult 0 1 2 3 1 3 1 flpplt invplt 2 2 2 2 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 fld xxx 0 0 1 +Field Plate 0 0 4 1 101 105 2 5 55 0 38 0 28 0 0 0 fld fld fld fld fld xld uld 0 1 2 3 1 3 1 flpfld invfld 1 1 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 gth ful 0 0 1 +Gothic Plate 0 0 4 1 128 135 0 5 70 0 44 0 32 0 0 0 gth gth gth gth gth xth uth 0 1 2 3 1 5 1 flpgth invgth 2 2 1 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 ful xxx 0 0 1 +Full Plate Mail 0 0 4 1 150 161 2 10 80 0 56 0 37 0 0 0 ful ful ful ful ful xul uul 0 1 2 3 1 5 1 flpful invful invfulu invfulu 2 2 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 aar xxx 0 0 1 +Ancient Armor 0 0 4 1 218 233 5 5 100 0 48 0 40 0 0 0 aar aar aar aar aar xar uar 0 1 2 3 1 5 1 flpaar invaar invaaru invaaru 1 2 2 2 2 0 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 ltp xxx 0 0 1 +Light Plate 0 0 4 1 90 107 1 0 41 0 48 0 35 0 0 0 ltp ltp ltp ltp ltp xtp utp 0 1 2 3 1 4 1 flpltp invltp 2 0 1 1 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 buc xxx 0 0 1 +Buckler 0 0 2 1 4 6 0 0 12 0 10 0 1 0 0 0 buc buc buc buc buc xuc uuc 0 7 2 2 1 2 2 flpbuc invbuc invbucu invbucu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 1 3 100 0 3 8 8 0 sml tow 0 0 1 +Small Shield 0 0 3 1 8 10 0 0 22 5 13 0 5 0 0 0 sml sml buc buc sml xml uml 0 7 2 2 1 3 2 flpsml invsml invsmlu invsmlu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 2 3 100 0 3 8 5 0 lrg kit 0 0 1 +Large Shield 0 0 4 1 12 14 0 5 34 12 19 0 11 0 0 0 lrg lrg lrg buc lrg xrg urg 0 7 2 3 1 4 2 flplrg invlrg invlrgu invlrgu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 2 4 100 0 3 8 2 0 kit tow 0 0 1 +Kite Shield 0 0 4 1 16 18 0 0 47 8 24 0 15 0 0 0 kit kit kit buc kit xit uit 0 7 2 3 1 4 2 flpkit invkit invkitu invkitu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 2 5 100 0 3 8 2 0 tow gts 0 0 1 +Tower Shield 0 0 4 1 22 25 1 10 75 24 48 0 22 0 0 0 tow tow tow buc tow xow uow 0 7 2 3 1 4 2 flptow invtow invtowu invtowu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 1 5 100 0 3 8 2 0 gts xxx 0 0 1 +Gothic Shield 0 0 4 1 30 35 0 5 60 16 32 0 30 0 0 0 gts gts kit buc gts xts uts 0 7 2 4 1 4 2 flpgts invgts invgtsu invgtsu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 2 6 100 0 3 8 2 0 xxx xxx 0 0 1 +Gloves(L) 0 0 1 1 2 3 0 0 0 0 10 0 3 0 0 0 lgl lgl lgl lgl lgl xlg ulg 0 16 2 2 1 4 0 flplgl invlgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 vgl hgl 0 0 1 +Heavy Gloves 0 0 1 1 5 6 0 0 0 0 11 0 7 0 0 0 vgl vgl vgl vgl vgl xvg uvg 0 16 2 2 1 4 0 flpvgl invvgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 mgl tgl 0 0 1 +Bracers(M) 0 0 2 1 8 9 0 0 25 0 13 0 12 0 0 0 mgl mgl mgl mgl mgl xmg umg 0 16 2 2 1 4 0 flpmgl invmgl 0 0 0 0 0 glov item_gloveschain 12 item_gloveschain 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 tgl hgl 0 0 1 +Light Gauntlets 0 0 3 1 9 11 0 0 45 0 14 0 20 0 0 0 tgl tgl mgl mgl tgl xtg utg 0 16 2 2 1 4 0 flptgl invtgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Gaunlets(H) 0 0 4 1 12 15 0 0 60 0 19 0 27 0 0 0 hgl hgl hgl hgl hgl xhg uhg 0 16 2 2 1 4 0 flphgl invhgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Leather Boots 0 0 1 1 2 3 0 0 0 0 10 0 3 0 0 0 lbt lbt lbt lbt lbt xlb ulb 0 16 2 2 1 4 1 flplbt invlbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 3 8 120 0 1 0 8 0 vbt tbt 0 0 1 +Heavy Boots 0 0 1 1 5 6 0 0 18 0 11 0 7 0 0 0 vbt vbt vbt vbt vbt xvb uvb 0 16 2 2 1 4 1 flpvbt invvbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 4 10 120 0 1 0 8 0 mbt tbt 0 0 1 +Chain Boots 0 0 2 1 8 9 0 0 30 0 13 0 12 0 0 0 mbt mbt mbt mbt mbt xmb umb 0 16 2 2 1 4 1 flpmbt invmbt 0 0 0 0 0 boot item_bootschain 12 item_bootschain 0 0 5 0 0 0 0 0 6 0 6 12 120 0 3 0 8 0 tbt hbt 0 0 1 +Light Plate Boots 0 0 3 1 9 11 0 0 50 0 14 0 20 0 0 0 tbt tbt mbt mbt tbt xtb utb 0 16 2 2 1 4 1 flptbt invtbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 8 16 120 0 3 0 8 0 xxx xxx 0 0 1 +Plate Boots 0 0 4 1 12 15 0 0 70 0 19 0 27 0 0 0 hbt hbt hbt hbt hbt xhb uhb 0 16 2 2 1 4 1 flphbt invhbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 10 20 120 0 3 0 8 0 xxx xxx 0 0 1 +Sash(L) 0 0 1 1 2 2 0 0 0 0 10 0 3 0 0 0 lbl lbl lbl lbl lbl zlb ulc 0 16 2 1 1 1 0 flplbl invlbl 0 0 0 0 0 belt item_lightarmor 12 item_lightarmor 0 0 5 0 0 1 0 0 4 0 0 0 0 1 0 8 0 vbl tbl 0 0 1 +Light Belt 0 0 1 1 3 3 0 0 0 0 11 0 7 0 0 0 vbl vbl vbl vbl vbl zvb uvc 0 16 2 1 1 1 0 flpvbl invvbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 4 0 0 4 0 0 0 0 1 0 8 0 mbl hbl 0 0 1 +Belt(M) 0 0 2 1 5 5 0 0 25 0 13 0 12 0 0 0 mbl mbl mbl mbl mbl zmb umc 0 16 2 1 1 1 0 flpmbl invmbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 0 0 0 4 0 0 0 0 1 0 8 0 tbl hbl 0 0 1 +Heavy Belt 0 0 2 1 6 6 0 0 45 0 14 0 20 0 0 0 tbl tbl mbl mbl tbl ztb utc 0 16 2 1 1 1 0 flptbl invtbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 5 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Girdle(H) 0 0 3 1 8 11 0 0 60 0 19 0 27 0 0 0 hbl hbl hbl hbl hbl zhb uhc 0 16 2 1 1 1 0 flphbl invhbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 3 0 0 4 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Bone Helm 0 0 2 1 33 36 0 0 25 0 32 0 22 0 0 0 bhm bhm bhm hlm bhm xh9 uh9 0 0 2 2 1 3 1 flpbhm invbhm invbhmu invbhmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 2 0 0 0 3 0 0 0 0 1 7 8 0 xxx xxx 0 0 1 +Bone Shield 0 0 2 1 10 30 0 0 25 20 32 0 19 0 0 0 bsh bsh bsh buc bsh xsh ush 0 7 2 3 1 3 2 flpbsh invbsh invbshu invbshu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 3 6 100 0 1 8 8 0 xxx xxx 0 0 1 +Spiked Shield 0 0 3 1 15 25 0 0 30 10 32 0 11 0 0 0 spk spk spk buc spk xpk upk 0 7 2 3 1 3 2 flpspk invspk invspku invspku 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 5 9 100 0 1 8 8 0 xxx xxx 0 0 1 +War Hat 0 0 1 1 45 53 0 0 20 0 10 0 34 22 0 0 xap xap cap cap cap xap uap 0 0 2 2 1 3 1 flpcap invcap 0 0 0 0 0 helm item_cap 12 item_cap 0 0 5 0 0 0 0 0 3 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Sallet 0 0 4 1 52 62 0 0 43 0 14 0 37 25 0 0 xkp xkp skp skp skp xkp ukp 0 0 2 2 1 3 1 flpskp invskp invxkpu invxkpu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Casque 0 0 4 1 63 72 0 0 59 0 19 0 42 25 0 0 xlm xlm hlm hlm hlm xlm ulm 0 0 2 2 1 3 1 flphlm invhlm invhlmu invhlmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Basinet 0 0 4 1 75 84 0 0 82 0 24 0 45 25 0 0 xhl xhl fhl fhl fhl xhl uhl 0 0 2 2 1 3 1 flpfhl invfhl invfhlu invfhlu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Winged Helm 0 0 4 1 85 98 0 0 115 0 32 0 51 25 0 0 xhm xhm ghm ghm ghm xhm uhm 0 0 2 2 1 4 1 flpghm invghm 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 7 8 0 xxx xxx 0 0 1 +Grand Crown 0 0 4 1 78 113 0 0 103 0 40 0 55 25 0 0 xrn xrn crn crn crn xrn urn 0 0 2 2 1 4 1 flpcrn invcrn invxrnu invxrnu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Death Mask 0 0 4 1 54 86 0 0 55 0 16 0 48 25 0 0 xsk xsk msk msk msk xsk usk 0 0 2 2 1 4 1 flpmsk invmsk 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Ghost Armor 0 0 1 1 102 117 0 0 38 0 16 0 34 22 0 0 xui xui qlt qlt qui xui uui 0 1 2 3 1 3 1 flpqlt invqlt 0 0 0 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Serpentskin Armor 0 0 2 1 111 126 0 0 43 0 19 0 36 24 0 0 xea xea lea lea lea xea uea 0 1 2 3 1 3 1 flplea invlea 0 0 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Demonhide Armor 0 0 3 1 122 136 0 0 50 0 22 0 37 25 0 0 xla xla hla hla hla xla ula 0 1 2 3 1 3 1 flphla invhla 1 1 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Trellised Armor 0 0 4 1 138 153 0 0 61 0 26 0 40 25 0 0 xtu xtu stu stu stu xtu utu 0 1 2 3 1 3 1 flpstu invstu invxtuu invxtuu 1 0 0 1 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Linked Mail 0 0 4 1 158 172 0 5 74 0 21 0 42 25 0 0 xng xng rng rng rng xng ung 0 1 2 3 1 4 1 flprng invrng 0 0 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Tigulated Mail 0 0 4 1 176 190 0 10 86 0 29 0 43 25 0 0 xcl xcl scl scl scl xcl ucl 0 1 2 3 1 4 1 flpscl invscl 1 1 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Mesh Armor 0 0 4 1 198 213 0 5 92 0 36 0 45 25 0 0 xhn xhn chn chn chn xhn uhn 0 1 2 3 1 4 1 flpchn invchn 1 1 1 1 2 2 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Cuirass 0 0 4 1 188 202 1 0 65 0 40 0 47 25 0 0 xrs xrs brs brs brs xrs urs 0 1 2 3 1 4 1 flpbrs invbrs invxrss 0 0 2 0 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Russet Armor 0 0 4 1 225 243 0 5 97 0 24 0 49 25 0 0 xpl xpl spl spl spl xpl upl 0 1 2 3 1 4 1 flpspl invspl 1 1 2 1 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Templar Coat 0 0 4 1 252 274 0 10 118 0 48 0 52 25 0 0 xlt xlt plt plt plt xlt ult 0 1 2 3 1 4 1 flpplt invplt 2 2 2 2 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Sharktooth Armor 0 0 4 1 242 258 2 5 103 0 38 0 55 25 0 0 xld xld fld fld fld xld uld 0 1 2 3 1 4 1 flpfld invfld 1 1 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Embossed Plate 0 0 4 1 282 303 0 5 125 0 44 0 58 25 0 0 xth xth gth gth gth xth uth 0 1 2 3 1 5 1 flpgth invgth 2 2 1 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Chaos Armor 0 0 4 1 315 342 2 10 140 0 56 0 61 25 0 0 xul xul ful ful ful xul uul 0 1 2 3 1 5 1 flpful invful 2 2 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Ornate Armor 0 0 4 1 417 450 5 5 170 0 48 0 64 25 0 0 xar xar aar aar aar xar uar 0 1 2 3 1 5 1 flpaar invaar invxaru invxaru 1 2 2 2 2 0 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Mage Plate 0 0 4 1 225 261 1 0 55 0 48 0 60 25 0 0 xtp xtp ltp ltp ltp xtp utp 0 1 2 3 1 4 1 flpltp invltp 2 0 1 1 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 xxx xxx 0 0 1 +Defender 0 0 2 1 41 49 0 0 38 10 54 0 34 22 0 0 xuc xuc buc buc buc xuc uuc 0 7 2 2 1 2 2 flpbuc invbuc invbucu invbucu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 8 12 100 0 3 8 8 0 xxx xxx 0 0 1 +Round Shield 0 0 3 1 47 55 0 0 53 12 51 0 37 25 0 0 xml xml buc buc sml xml uml 0 7 2 2 1 3 2 flpsml invsml invxmlu invxmlu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 7 14 100 0 3 8 5 0 xxx xxx 0 0 1 +Scutum 0 0 4 1 53 61 0 5 71 14 50 0 42 25 0 0 xrg xrg lrg lrg lrg xrg urg 0 7 2 3 1 4 2 flplrg invlrg invxrgu invxrgu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 11 15 100 0 3 8 2 0 xxx xxx 0 0 1 +Dragon Shield 0 0 4 1 59 67 0 0 91 18 61 0 45 25 0 0 xit xit kit kit kit xit uit 0 7 2 3 1 4 2 flpkit invkit invkitu invkitu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 15 24 100 0 3 8 2 0 xxx xxx 0 0 1 +Pavise 0 0 4 1 68 78 1 10 133 24 58 0 50 25 0 0 xow xow tow tow tow xow uow 0 7 2 3 1 4 2 flptow invtow invtowu invtowu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 10 17 100 0 3 8 2 0 xxx xxx 0 0 1 +Ancient Shield 0 0 4 1 80 93 0 5 110 16 64 0 56 25 0 0 xts xts kit kit gts xts uts 0 7 2 4 1 4 2 flpgts invgts invgtsu invgtsu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 12 16 100 0 3 8 2 0 xxx xxx 0 0 1 +Demonhide Gloves 0 0 1 1 28 35 0 0 20 0 10 0 33 21 0 0 xlg xlg lgl lgl lgl xlg ulg 0 16 2 2 1 4 0 flplgl invlgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Sharkskin Gloves 0 0 1 1 33 39 0 0 20 0 11 0 39 25 0 0 xvg xvg vgl vgl vgl xvg uvg 0 16 2 2 1 4 0 flpvgl invvgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Heavy Bracers 0 0 2 1 37 44 0 0 58 0 13 0 43 25 0 0 xmg xmg mgl mgl mgl xmg umg 0 16 2 2 1 4 0 flpmgl invmgl 0 0 0 0 0 glov item_gloveschain 12 item_gloveschain 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Battle Gauntlets 0 0 3 1 39 47 0 0 88 0 14 0 49 25 0 0 xtg xtg mgl mgl tgl xtg utg 0 16 2 2 1 4 0 flptgl invtgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +War Gauntlets 0 0 4 1 43 53 0 0 110 0 19 0 54 25 0 0 xhg xhg hgl hgl hgl xhg uhg 0 16 2 2 1 4 0 flphgl invhgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Demonhide Boots 0 0 1 1 28 35 0 0 20 0 10 0 36 24 0 0 xlb xlb lbt lbt lbt xlb ulb 0 16 2 2 1 4 1 flplbt invlbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 26 46 120 0 1 0 8 0 xxx xxx 0 0 1 +Sharkskin Boots 0 0 1 1 33 39 0 0 47 0 11 0 39 25 0 0 xvb xvb vbt vbt vbt xvb uvb 0 16 2 2 1 4 1 flpvbt invvbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 28 50 120 0 1 0 8 0 xxx xxx 0 0 1 +Mesh Boots 0 0 2 1 37 44 0 0 65 0 13 0 43 25 0 0 xmb xmb mbt mbt mbt xmb umb 0 16 2 2 1 4 1 flpmbt invmbt 0 0 0 0 0 boot item_bootschain 12 item_bootschain 0 0 5 0 0 0 0 0 6 0 23 52 120 0 3 0 8 0 xxx xxx 0 0 1 +Battle Boots 0 0 3 1 39 47 0 0 95 0 14 0 49 25 0 0 xtb xtb mbt mbt tbt xtb utb 0 16 2 2 1 4 1 flptbt invtbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 37 64 120 0 3 0 8 0 xxx xxx 0 0 1 +War Boots 0 0 4 1 43 53 0 0 125 0 19 0 54 25 0 0 xhb xhb hbt hbt hbt xhb uhb 0 16 2 2 1 4 1 flphbt invhbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 39 80 120 0 3 0 8 0 xxx xxx 0 0 1 +Demonhide Sash 0 0 1 1 29 34 0 0 20 0 10 0 36 24 0 0 zlb zlb lbl lbl lbl zlb ulc 0 16 2 1 1 1 0 flplbl invlbl 0 0 0 0 0 belt item_lightarmor 12 item_lightarmor 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Sharkskin Belt 0 0 1 1 31 36 0 0 20 0 11 0 39 25 0 0 zvb zvb vbl vbl vbl zvb uvc 0 16 2 1 1 1 0 flpvbl invvbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Mesh Belt 0 0 2 1 35 40 0 0 58 0 13 0 43 25 0 0 zmb zmb mbl mbl mbl zmb umc 0 16 2 1 1 1 0 flpmbl invmbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Battle Belt 0 0 2 1 37 42 0 0 88 0 14 0 49 25 0 0 ztb ztb mbl mbl tbl ztb utc 0 16 2 1 1 1 0 flptbl invtbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +War Belt 0 0 3 1 41 52 0 0 110 0 19 0 54 25 0 0 zhb zhb hbl hbl hbl zhb uhc 0 16 2 1 1 1 0 flphbl invhbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Grim Helm 0 0 2 1 60 125 0 0 58 0 32 0 50 25 0 0 xh9 xh9 bhm bhm bhm xh9 uh9 0 0 2 2 1 3 1 flpbhm invbhm invbhmu invbhmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 2 0 0 0 3 0 0 0 0 1 7 8 0 xxx xxx 0 0 1 +Grim Shield 0 0 2 1 50 150 0 0 58 20 56 0 48 25 0 0 xsh xsh bsh bsh bsh xsh ush 0 7 2 3 1 3 2 flpbsh invbsh invxshu invxshu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 14 20 100 0 1 8 8 0 xxx xxx 0 0 1 +Barbed Shield 0 0 3 1 58 78 0 0 65 17 44 0 42 25 0 0 xpk xpk spk spk spk xpk upk 0 7 2 3 1 3 2 flpspk invspk invxpku invxpku 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 18 35 100 0 1 8 8 0 xxx xxx 0 0 1 +Expansion +Wolf Head 100 0 1 1 8 11 0 0 16 0 16 0 4 3 0 0 dr1 dr1 dr1 dr1 dr1 dr6 drb 0 0 2 2 1 4 1 flpdr1 invdr1 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Hawk Helm 100 0 1 1 4 15 0 0 20 0 16 0 8 6 0 0 dr2 dr2 dr4 dr4 dr2 dr7 drc 0 0 2 2 1 4 1 flpdr4 invdr2 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Antlers 100 0 1 1 18 24 0 0 24 0 16 0 16 12 0 0 dr3 dr3 dr3 dr3 dr3 dr8 drd 0 0 2 2 1 4 1 flpdr3 invdr3 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Falcon Mask 100 0 1 1 12 28 0 0 28 0 16 0 20 15 0 0 dr4 dr4 dr4 dr4 dr4 dr9 dre 0 0 2 2 1 4 1 flpdr4 invdr4 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Spirit Mask 100 0 1 1 22 35 0 0 30 0 16 0 24 18 0 0 dr5 dr5 dr1 dr1 dr5 dra drf 0 0 2 2 1 4 1 flpdr1 invdr5 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Jawbone Cap 100 0 2 1 10 15 0 0 25 0 20 0 4 3 0 0 ba1 ba1 ba1 dr1 ba1 ba6 bab 0 0 2 2 1 4 1 flpba1 invba1 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 8 0 xxx xxx 0 0 1 +Fanged Helm 100 0 2 1 15 20 0 0 35 0 28 0 8 6 0 0 ba2 ba2 ba1 bhm ba2 ba7 bac 0 0 2 2 1 4 1 flpba1 invba2 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 8 0 xxx xxx 0 0 1 +Horned Helm 100 0 2 1 25 30 0 0 45 0 36 0 16 12 0 0 ba3 ba3 ba3 bhm ba3 ba8 bad 0 0 2 2 1 4 1 flpba3 invba3 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 8 0 xxx xxx 0 0 1 +Assault Helmet 100 0 2 1 30 35 0 0 55 0 40 0 20 15 0 0 ba4 ba4 ba5 bhm ba4 ba9 bae 0 0 2 2 1 4 1 flpba5 invba4 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 8 0 xxx xxx 0 0 1 +Avenger Guard 100 0 2 1 35 50 0 0 65 0 44 0 24 18 0 0 ba5 ba5 ba5 bhm ba5 baa baf 0 0 2 2 1 4 1 flpba5 invba5 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 8 0 xxx xxx 0 0 1 +Targe 100 0 2 1 8 12 0 0 16 10 16 0 4 3 0 0 pa1 pa1 304 pa1 spk pa1 pa6 pab 0 7 2 2 1 5 2 flppa1 invpa1 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 1 0 0 0 1 0 2 6 100 0 3 0 0 0 xxx xxx 0 0 1 +Rondache 100 0 2 1 10 18 0 0 26 15 24 0 8 6 0 0 pa2 pa2 304 pa1 spk pa2 pa7 pac 0 7 2 2 1 5 2 flppa1 invpa2 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 2 0 0 0 1 0 2 8 100 0 3 0 0 0 xxx xxx 0 0 1 +Heraldic Shield 100 0 2 1 16 26 0 0 40 20 32 0 16 12 0 0 pa3 pa3 304 pa3 spk pa3 pa8 pad 0 7 2 4 1 5 2 flppa3 invpa3 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 3 0 0 0 1 0 3 9 100 0 3 0 0 0 xxx xxx 0 0 1 +Aerin Shield 100 0 2 1 26 36 0 0 50 22 40 0 20 15 0 0 pa4 pa4 304 pa3 spk pa4 pa9 pae 0 7 2 4 1 5 2 flppa3 invpa4 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 4 0 0 0 1 0 4 10 100 0 3 0 0 0 xxx xxx 0 0 1 +Crown Shield 100 0 2 1 30 40 0 0 65 25 48 0 24 18 0 0 pa5 pa5 304 pa5 spk pa5 paa paf 0 7 2 2 1 5 2 flppa5 invpa5 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 5 0 0 0 1 0 4 12 100 0 3 0 0 0 xxx xxx 0 0 1 +Preserved Head 100 0 1 1 2 5 0 0 12 3 16 0 4 3 0 0 ne1 ne1 305 ne1 dr1 ne1 ne6 neb 0 10 2 2 1 3 2 flpne1 invne1 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Zombie Head 100 0 1 1 4 8 0 0 14 5 16 0 8 6 0 0 ne2 ne2 305 ne2 bhm ne2 ne7 neg 0 10 2 2 1 3 2 flpne2 invne2 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Unraveller Head 100 0 1 1 6 10 0 0 18 8 16 0 16 12 0 0 ne3 ne3 305 ne3 bhm ne3 ne8 ned 0 10 2 2 1 3 2 flpne3 invne3 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Gargoyle Head 100 0 1 1 10 16 0 0 20 10 16 0 20 15 0 0 ne4 ne4 305 ne3 bhm ne4 ne9 nee 0 10 2 2 1 3 2 flpne3 invne4 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Demon Head 100 0 1 1 15 20 0 0 25 12 16 0 24 18 0 0 ne5 ne5 305 ne2 bhm ne5 nea nef 0 10 2 2 1 3 2 flpne2 invne5 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Circlet 100 0 1 1 20 30 0 0 0 0 28 0 24 16 0 0 ci0 ci0 3 lit lit ci0 0 0 2 2 1 3 1 flpci0 invci0 0 0 0 0 0 circ item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Coronet 100 0 1 1 30 40 0 0 0 0 24 0 52 39 0 0 ci1 ci1 8 lit lit ci1 ci2 ci3 0 0 2 2 1 3 1 flpci1 invci1 0 0 0 0 0 circ item_helm 12 item_helm 0 0 5 0 1 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Tiara 100 0 1 1 40 50 0 0 0 0 20 0 70 52 0 0 ci2 ci2 13 lit lit ci1 ci2 ci3 0 0 2 2 1 4 1 flpci1 invci2 0 0 0 0 0 circ item_helm 12 item_helm 0 0 5 0 2 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Diadem 100 0 1 1 50 60 0 0 0 0 16 0 85 64 0 0 ci3 ci3 18 lit lit ci1 ci2 ci3 0 0 2 2 1 4 1 flpci2 invci3 0 0 0 0 0 circ item_helm 12 item_helm 0 0 5 0 3 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Shako 100 0 1 1 98 141 0 0 50 0 10 0 58 43 0 0 uap uap cap cap cap xap uap 0 0 2 2 1 3 1 flpcap invcap 0 0 0 0 0 helm item_cap 12 item_cap 0 0 5 0 0 0 0 0 3 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Hydraskull 100 0 4 1 101 145 0 0 84 0 14 0 63 47 0 0 ukp ukp skp skp skp xkp ukp 0 0 2 2 1 3 1 flpskp invskp 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Armet 100 0 4 1 105 149 0 0 109 0 19 0 68 51 0 0 ulm ulm hlm hlm hlm xlm ulm 0 0 2 2 1 3 1 flphlm invhlm invhlmu invhlmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Giant Conch 100 0 4 1 110 154 0 0 142 0 24 0 54 40 0 0 uhl uhl fhl fhl fhl xhl uhl 0 0 2 2 1 3 1 flpfhl invfhl invfhlu invfhlu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Spired Helm 100 0 4 1 114 159 0 0 192 0 32 0 79 59 0 0 uhm uhm ghm ghm ghm xhm uhm 0 0 2 2 1 4 1 flpghm invghm invuhms 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 7 8 0 xxx xxx 0 0 1 +Corona 100 0 4 1 111 165 0 0 174 0 40 0 85 66 0 0 urn urn crn crn crn xrn urn 0 0 2 2 1 4 1 flpcrn invcrn 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Demonhead 100 0 4 1 101 154 0 0 102 0 16 0 74 55 0 0 usk usk msk msk msk xsk usk 0 0 2 2 1 4 1 flpmsk invmsk 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 1 2 0 xxx xxx 0 0 1 +Dusk Shroud 100 0 1 1 361 467 0 0 77 0 16 0 65 49 0 0 uui uui qlt qlt qui xui uui 0 1 2 3 1 5 1 flpqlt invqlt 0 0 0 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Wyrmhide 100 0 2 1 364 470 0 0 84 0 19 0 67 50 0 0 uea uea lea lea lea xea uea 0 1 2 3 1 5 1 flplea invlea 0 0 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Scarab Husk 100 0 3 1 369 474 0 0 95 0 22 0 68 51 0 0 ula ula hla hla hla xla ula 0 1 2 3 1 5 1 flphla invhla 1 1 1 0 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Wire Fleece 100 0 4 1 375 481 0 0 111 0 26 0 70 53 0 0 utu utu stu stu stu xtu utu 0 1 2 3 1 5 1 flpstu invstu 1 0 0 1 1 1 0 0 0 0 0 tors item_lightarmor 12 item_lightarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 1 2 8 0 xxx xxx 0 0 1 +Diamond Mail 100 0 4 1 383 489 0 5 131 0 21 0 72 54 0 0 ung ung rng rng rng xng ung 0 1 2 3 1 5 1 flprng invrng 0 0 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Loricated Mail 100 0 4 1 390 496 0 10 149 0 29 0 73 55 0 0 ucl ucl scl scl scl xcl ucl 0 1 2 3 1 5 1 flpscl invscl 1 1 1 1 1 1 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Boneweave 100 0 4 1 399 505 0 5 158 0 36 0 62 47 0 0 uhn uhn chn chn chn xhn uhn 0 1 2 3 1 5 1 flpchn invchn 1 1 1 1 2 2 0 0 0 0 0 tors item_chainarmor 12 item_chainarmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Great Hauberk 100 0 4 1 395 501 1 0 118 0 40 0 75 56 0 0 urs urs brs brs brs xrs urs 0 1 2 3 1 5 1 flpbrs invbrs 0 0 2 0 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Balrog Skin 100 0 4 1 410 517 0 5 165 0 24 0 76 57 0 0 upl upl spl spl spl xpl upl 0 1 2 3 1 5 1 flpspl invspl 1 1 2 1 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 2 8 0 xxx xxx 0 0 1 +Hellforged Plate 100 0 4 1 421 530 0 10 196 0 48 0 78 59 0 0 ult ult plt plt plt xlt ult 0 1 2 3 1 5 1 flpplt invplt 2 2 2 2 1 1 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Kraken Shell 100 0 4 1 417 523 2 5 174 0 38 0 81 61 0 0 uld uld fld fld fld xld uld 0 1 2 3 1 5 1 flpfld invfld 1 1 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Lacquered Plate 100 0 4 1 433 541 0 5 208 0 44 0 82 62 0 0 uth uth gth gth gth xth uth 0 1 2 3 1 5 1 flpgth invgth 2 2 1 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Shadow Plate 100 0 4 1 446 557 2 10 230 0 56 0 83 64 0 0 uul uul ful ful ful xul uul 0 1 2 3 1 5 1 flpful invful 2 2 2 2 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Sacred Armor 100 0 4 1 487 600 5 5 232 0 48 0 85 66 0 0 uar uar aar aar aar xar uar 0 1 2 3 1 5 1 flpaar invaar invaaru invaaru 1 2 2 2 2 0 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 8 8 0 xxx xxx 0 0 1 +Archon Plate 100 0 4 1 410 524 1 0 103 0 48 0 84 63 0 0 utp utp ltp ltp ltp xtp utp 0 1 2 3 1 5 1 flpltp invltp 2 0 1 1 2 2 0 0 0 0 0 tors item_platearmor 12 item_platearmor 0 0 5 0 0 0 0 0 2 0 0 0 0 3 7 8 0 xxx xxx 0 0 1 +Heater 100 0 2 1 95 110 0 0 77 22 70 0 58 43 0 0 uuc uuc buc buc buc xuc uuc 0 7 2 2 1 3 2 flpbuc invbuc invbucu invbucu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 16 30 100 0 3 8 8 0 xxx xxx 0 0 1 +Luna 100 0 3 1 108 123 0 0 100 20 67 0 61 45 0 0 uml uml buc buc sml xml uml 0 7 2 2 1 3 2 flpsml invsml invsmlu invsmlu 0 0 0 0 0 shie item_woodshield 12 item_woodshield 0 0 5 0 0 0 0 0 1 0 17 29 100 0 3 8 5 0 xxx xxx 0 0 1 +Hyperion 100 0 4 1 119 135 0 5 127 24 66 0 64 48 0 0 urg urg lrg lrg lrg xrg urg 0 7 2 3 1 4 2 flplrg invlrg invlrgu invlrgu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 14 32 100 0 3 8 2 0 xxx xxx 0 0 1 +Monarch 100 0 4 1 133 148 0 0 156 22 69 0 72 54 0 0 uit uit kit kit kit xit uit 0 7 2 3 1 5 2 flpkit invkit invkitu invkitu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 12 34 100 0 3 8 2 0 xxx xxx 0 0 1 +Aegis 100 0 4 1 145 161 1 10 219 24 74 0 79 59 0 0 uow uow tow tow tow xow uow 0 7 2 3 1 5 2 flptow invtow invtowu invtowu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 18 28 100 0 3 8 2 0 xxx xxx 0 0 1 +Ward 100 0 4 1 153 170 0 5 185 24 80 0 84 63 0 0 uts uts kit kit gts xts uts 0 7 2 4 1 5 2 flpgts invgts invgtsu invutss 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 11 35 100 0 3 8 2 0 xxx xxx 0 0 1 +Bramble Mitts 100 0 1 1 54 62 0 0 50 0 10 0 57 42 0 0 ulg ulg lgl lgl lgl xlg ulg 0 16 2 2 1 4 0 flplgl invlgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Vampirebone Gloves 100 0 1 1 56 65 0 0 50 0 11 0 63 47 0 0 uvg uvg vgl vgl vgl xvg uvg 0 16 2 2 1 4 0 flpvgl invvgl 0 0 0 0 0 glov item_gloves 12 item_gloves 0 0 5 0 0 0 0 0 5 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Vambraces 100 0 2 1 59 67 0 0 106 0 13 0 69 51 0 0 umg umg mgl mgl mgl xmg umg 0 16 2 2 1 4 0 flpmgl invmgl 0 0 0 0 0 glov item_gloveschain 12 item_gloveschain 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Crusader Gauntlets 100 0 3 1 59 68 0 0 151 0 14 0 76 57 0 0 utg utg mgl mgl tgl xtg utg 0 16 2 2 1 4 0 flptgl invtgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Ogre Gauntlets 100 0 4 1 62 71 0 0 185 0 19 0 85 64 0 0 uhg uhg hgl hgl hgl xhg uhg 0 16 2 2 1 4 0 flphgl invhgl 0 0 0 0 0 glov item_glovesmetal 12 item_glovesmetal 0 0 5 0 0 0 0 0 5 0 0 0 0 3 0 8 0 xxx xxx 0 0 1 +Wyrmhide Boots 100 0 1 1 54 62 0 0 50 0 10 0 60 45 0 0 ulb ulb lbt lbt lbt xlb ulb 0 16 2 2 1 4 1 flplbt invlbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 65 100 120 0 1 0 8 0 xxx xxx 0 0 1 +Scarabshell Boots 100 0 1 1 56 65 0 0 91 0 11 0 66 49 0 0 uvb uvb vbt vbt vbt xvb uvb 0 16 2 2 1 4 1 flpvbt invvbt 0 0 0 0 0 boot item_boots 12 item_boots 0 0 5 0 0 0 0 0 6 0 60 110 120 0 1 0 8 0 xxx xxx 0 0 1 +Boneweave Boots 100 0 2 1 59 67 0 0 118 0 13 0 72 54 0 0 umb umb mbt mbt mbt xmb umb 0 16 2 2 1 4 1 flpmbt invmbt 0 0 0 0 0 boot item_bootschain 12 item_bootschain 0 0 5 0 0 0 0 0 6 0 69 118 120 0 3 0 8 0 xxx xxx 0 0 1 +Mirrored Boots 100 0 3 1 59 68 0 0 163 0 14 0 81 60 0 0 utb utb mbt mbt tbt xtb utb 0 16 2 2 1 4 1 flptbt invtbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 50 145 120 0 3 0 8 0 xxx xxx 0 0 1 +Myrmidon Greaves 100 0 4 1 62 71 0 0 208 0 19 0 85 65 0 0 uhb uhb hbt hbt hbt xhb uhb 0 16 2 2 1 4 1 flphbt invhbt 0 0 0 0 0 boot item_bootsmetal 12 item_bootsmetal 0 0 5 0 0 0 0 0 6 0 83 149 120 0 3 0 8 0 xxx xxx 0 0 1 +Spiderweb Sash 100 0 1 1 55 62 0 0 50 0 10 0 61 46 0 0 ulc ulc lbl lbl lbl zlb ulc 0 16 2 1 1 1 0 flplbl invlbl 0 0 0 0 0 belt item_lightarmor 12 item_lightarmor 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Vampirefang Belt 100 0 1 1 56 63 0 0 50 0 11 0 68 51 0 0 uvc uvc vbl vbl vbl zvb uvc 0 16 2 1 1 1 0 flpvbl invvbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Mithril Coil 100 0 2 1 58 65 0 0 106 0 13 0 75 56 0 0 umc umc mbl mbl mbl zmb umc 0 16 2 1 1 1 0 flpmbl invmbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Troll Belt 100 0 2 1 59 66 0 0 151 0 14 0 82 62 0 0 utc utc mbl mbl tbl ztb utc 0 16 2 1 1 1 0 flptbl invtbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Colossus Girdle 100 0 3 1 61 71 0 0 185 0 19 0 85 67 0 0 uhc uhc hbl hbl hbl zhb uhc 0 16 2 1 1 1 0 flphbl invhbl 0 0 0 0 0 belt item_belt 12 item_belt 0 0 5 0 0 6 0 0 4 0 0 0 0 1 0 8 0 xxx xxx 0 0 1 +Bone Visage 100 0 2 1 100 157 0 0 106 0 32 0 84 63 0 0 uh9 uh9 bhm bhm bhm xh9 uh9 0 0 2 2 1 4 1 flpbhm invbhm invbhmu invbhmu 0 0 0 0 0 helm item_helm 12 item_helm 0 0 5 0 2 0 0 0 3 0 0 0 0 1 7 8 0 xxx xxx 0 0 1 +Troll Nest 100 0 2 1 158 173 0 0 106 20 59 0 76 57 0 0 ush ush bsh bsh bsh xsh ush 0 7 2 3 1 4 2 flpbsh invbsh invbshu invbshu 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 24 38 100 0 1 8 8 0 xxx xxx 0 0 1 +Blade Barrier 100 0 3 1 147 163 0 0 118 20 66 0 68 51 0 0 upk upk spk spk spk xpk upk 0 7 2 3 1 4 2 flpspk invspk invspku invspku 0 0 0 0 0 shie item_metalshield 12 item_metalshield 0 0 5 0 0 0 0 0 1 0 26 40 100 0 1 8 8 0 xxx xxx 0 0 1 +Alpha Helm 100 0 1 1 52 62 0 0 44 0 16 0 35 26 0 0 dr6 dr6 dr1 dr1 dr1 dr6 drb 0 0 2 2 1 4 1 flpdr1 invdr1 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Griffon Headress 100 0 1 1 46 68 0 0 50 0 16 0 40 30 0 0 dr7 dr7 dr4 dr4 dr2 dr7 drc 0 0 2 2 1 4 1 flpdr4 invdr2 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Hunter's Guise 100 0 1 1 67 81 0 0 56 0 16 0 46 29 0 0 dr8 dr8 dr3 dr3 dr3 dr8 drd 0 0 2 2 1 4 1 flpdr3 invdr3 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Sacred Feathers 100 0 1 1 58 87 0 0 62 0 16 0 50 32 0 0 dr9 dr9 dr4 dr4 dr4 dr9 dre 0 0 2 2 1 4 1 flpdr4 invdr4 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Totemic Mask 100 0 1 1 73 98 0 0 65 0 16 0 55 41 0 0 dra dra dr1 dr1 dr5 dra drf 0 0 2 2 1 4 1 flpdr1 invdr5 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Jawbone Visor 100 0 2 1 55 68 0 0 58 0 20 0 33 25 0 0 ba6 ba6 ba1 dr1 ba1 ba6 bab 0 0 2 2 1 4 1 flpba1 invba1 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 0 0 xxx xxx 0 0 1 +Lion Helm 100 0 2 1 63 75 0 0 73 0 28 0 38 29 0 0 ba7 ba7 ba1 bhm ba2 ba7 bac 0 0 2 2 1 4 1 flpba1 invba2 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 0 0 xxx xxx 0 0 1 +Rage Mask 100 0 2 1 78 90 0 0 88 0 36 0 44 29 0 0 ba8 ba8 ba3 bhm ba3 ba8 bad 0 0 2 2 1 4 1 flpba3 invba3 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Savage Helmet 100 0 2 1 85 98 0 0 103 0 40 0 49 32 0 0 ba9 ba9 ba5 bhm ba4 ba9 bae 0 0 2 2 1 4 1 flpba5 invba4 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Slayer Guard 100 0 2 1 93 120 0 0 118 0 44 0 54 40 0 0 baa baa ba5 bhm ba5 baa baf 0 0 2 2 1 4 1 flpba5 invba5 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Akaran Targe 100 0 2 1 101 125 0 0 44 10 16 0 35 26 0 0 pa6 pa6 304 pa1 spk pa1 pa6 pab 0 7 2 2 1 5 2 flppa1 invpa1 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 1 0 0 0 1 0 12 16 100 0 3 0 0 0 xxx xxx 0 0 1 +Akaran Rondache 100 0 2 1 113 137 0 0 59 15 24 0 40 30 0 0 pa7 pa7 304 pa1 spk pa2 pa7 pac 0 7 2 2 1 5 2 flppa1 invpa2 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 2 0 0 0 1 0 15 20 100 0 3 0 0 0 xxx xxx 0 0 1 +Protector Shield 100 0 2 1 129 153 0 0 69 20 32 0 46 34 0 0 pa8 pa8 304 pa3 spk pa3 pa8 pad 0 7 2 4 1 5 2 flppa3 invpa3 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 3 0 0 0 1 0 18 24 100 0 3 0 0 0 xxx xxx 0 0 1 +Guilded Shield 100 0 2 1 144 168 0 0 89 22 40 0 51 38 0 0 pa9 pa9 304 pa3 spk pa4 pa9 pae 0 7 2 4 1 5 2 flppa3 invpa4 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 4 0 0 0 1 0 20 28 100 0 3 0 0 0 xxx xxx 0 0 1 +Royal Shield 100 0 2 1 156 181 0 0 114 25 48 0 55 41 0 0 paa paa 304 pa5 spk pa5 paa paf 0 7 2 2 1 5 2 flppa5 invpa5 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 5 0 0 0 1 0 24 32 100 0 3 0 0 0 xxx xxx 0 0 1 +Mummified Trophy 100 0 1 1 38 48 0 0 38 3 16 0 33 24 0 0 ne6 ne6 305 ne1 dr1 ne1 ne6 neb 0 10 2 2 1 3 2 flpne1 invne1 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Fetish Trophy 100 0 1 1 41 52 0 0 41 5 16 0 39 29 0 0 ne7 ne7 305 ne2 bhm ne2 ne7 neg 0 10 2 2 1 3 2 flpne2 invne2 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Sexton Trophy 100 0 1 1 44 55 0 0 47 8 16 0 45 33 0 0 ne8 ne8 305 ne3 bhm ne3 ne8 ned 0 10 2 2 1 3 2 flpne3 invne3 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Cantor Trophy 100 0 1 1 50 64 0 0 50 10 16 0 49 36 0 0 ne9 ne9 305 ne3 bhm ne4 ne9 nee 0 10 2 2 1 3 2 flpne3 invne4 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Heirophant Trophy 100 0 1 1 58 70 0 0 58 12 16 0 54 40 0 0 nea nea 305 ne2 bhm ne5 nea nef 0 10 2 2 1 3 2 flpne2 invne5 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Blood Spirt 100 0 1 1 101 145 0 0 86 0 16 0 62 46 0 0 drb drb dr1 dr1 dr1 dr6 drb 0 0 2 2 1 4 1 flpdr1 invdr1 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Sun Spirit 100 0 1 1 98 147 0 0 95 0 16 0 69 51 0 0 drc drc dr4 dr4 dr2 dr7 drc 0 0 2 2 1 4 1 flpdr4 invdr2 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Earth Spirit 100 0 1 1 107 152 0 0 104 0 16 0 76 57 0 0 drd drd dr3 dr3 dr3 dr8 drd 0 0 2 2 1 4 1 flpdr3 invdr3 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Sky Spirit 100 0 1 1 103 155 0 0 113 0 16 0 83 62 0 0 dre dre dr4 dr4 dr4 dr9 dre 0 0 2 2 1 4 1 flpdr4 invdr4 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Dream Spirit 100 0 1 1 109 159 0 0 118 0 16 0 85 66 0 0 drf drf dr1 dr1 dr5 dra drf 0 0 2 2 1 4 1 flpdr1 invdr5 0 0 0 0 0 pelt item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 8 8 0 xxx xxx 0 0 1 +Carnage Helm 100 0 2 1 102 147 0 0 106 0 20 0 60 45 0 0 bab bab ba1 dr1 ba1 ba6 bab 0 0 2 2 1 4 1 flpba1 invba1 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 0 0 xxx xxx 0 0 1 +Fury Visor 100 0 2 1 105 150 0 0 129 0 28 0 66 49 0 0 bac bac ba1 bhm ba2 ba7 bac 0 0 2 2 1 4 1 flpba1 invba2 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 1 5 0 0 xxx xxx 0 0 1 +Destroyer Helm 100 0 2 1 111 156 0 0 151 0 36 0 73 54 0 0 bad bad ba3 bhm ba3 ba8 bad 0 0 2 2 1 4 1 flpba3 invba3 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Conquerer Crown 100 0 2 1 114 159 0 0 174 0 40 0 80 60 0 0 bae bae ba5 bhm ba4 ba9 bae 0 0 2 2 1 4 1 flpba5 invba4 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Guardian Crown 100 0 2 1 117 168 0 0 196 0 44 0 85 65 0 0 baf baf ba5 bhm ba5 baa baf 0 0 2 2 1 4 1 flpba5 invba5 0 0 0 0 0 phlm item_helm 12 item_helm 0 0 5 0 0 0 0 0 3 0 0 0 0 3 5 0 0 xxx xxx 0 0 1 +Sacred Targe 100 0 2 1 126 158 0 0 86 30 36 0 63 47 0 0 pab pab 304 pa1 spk pa1 pa6 pab 0 7 2 2 1 5 2 flppa1 invpa1 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 1 0 0 0 1 0 22 70 100 0 3 0 0 0 xxx xxx 0 0 1 +Sacred Rondache 100 0 2 1 138 164 0 0 109 28 54 0 70 52 0 0 pac pac 304 pa1 spk pa2 pa7 pac 0 7 2 2 1 5 2 flppa1 invpa2 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 2 0 0 0 1 0 35 58 100 0 3 0 0 0 xxx xxx 0 0 1 +Ancient Shield 100 0 2 1 154 172 0 0 124 25 44 0 74 55 0 0 pad pad 304 pa3 spk pa3 pa8 pad 0 7 2 4 1 5 2 flppa3 invpa3 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 3 0 0 0 1 0 10 82 100 0 3 0 0 0 xxx xxx 0 0 1 +Zakarum Shield 100 0 2 1 169 193 0 0 142 22 52 0 82 61 0 0 pae pae 304 pa3 spk pa4 pa9 pae 0 7 2 4 1 5 2 flppa3 invpa4 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 4 0 0 0 1 0 46 46 100 0 3 0 0 0 xxx xxx 0 0 1 +Vortex Shield 100 0 2 1 182 225 0 0 148 19 72 0 85 66 0 0 paf paf 304 pa5 spk pa5 paa paf 0 7 2 2 1 5 2 flppa5 invpa5 0 0 0 0 0 ashd item_metalshield 12 item_metalshield 0 0 5 0 5 0 0 0 1 0 5 87 100 0 3 0 0 0 xxx xxx 0 0 1 +Minion Skull 100 0 1 1 95 139 0 0 77 3 16 0 59 44 0 0 neb neb 305 ne1 dr1 ne1 ne6 neb 0 10 2 2 1 3 2 flpne1 invne1 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Hellspawn Skull 100 0 1 1 96 141 0 0 82 5 16 0 67 50 0 0 neg neg 305 ne2 bhm ne2 ne7 neg 0 10 2 2 1 3 2 flpne2 invne2 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Overseer Skull 100 0 1 1 98 142 0 0 91 8 16 0 66 49 0 0 ned ned 305 ne3 bhm ne3 ne8 ned 0 10 2 2 1 3 2 flpne3 invne3 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Succubae Skull 100 0 1 1 100 146 0 0 95 10 16 0 81 60 0 0 nee nee 305 ne3 bhm ne4 ne9 nee 0 10 2 2 1 3 2 flpne3 invne4 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 +Bloodlord Skull 100 0 1 1 103 148 0 0 106 12 16 0 85 65 0 0 nef nef 305 ne2 bhm ne5 nea nef 0 10 2 2 1 3 2 flpne2 invne5 0 0 0 0 0 head item_head 12 item_head 0 0 5 0 0 0 0 0 3 0 0 0 0 1 1 0 0 xxx xxx 0 0 1 diff --git a/Properties.txt b/Properties.txt new file mode 100644 index 0000000..ea34088 --- /dev/null +++ b/Properties.txt @@ -0,0 +1,429 @@ +code *done set1 val1 func1 stat1 set2 val2 func2 stat2 set3 val3 func3 stat3 set4 val4 func4 stat4 set5 val5 func5 stat5 set6 val6 func6 stat6 set7 val7 func7 stat7 *desc *param *min *max *notes *eol +ac 1 1 armorclass none 0 +ac-miss 1 1 armorclass_vs_missile none 0 +ac-hth 1 1 armorclass_vs_hth none 0 +red-dmg 1 1 normal_damage_reduction none 0 +red-dmg% 1 1 damageresist none 0 +ac% 1 2 item_armor_percent none 0 +red-mag 1 1 magic_damage_reduction none 0 +str 1 1 strength none 0 +dex 1 1 dexterity none 0 +vit 1 1 vitality none 0 +enr 1 1 energy none 0 +mana 1 1 maxmana none 0 +mana% 1 1 item_maxmana_percent none 0 +hp 1 1 maxhp none 0 +hp% 1 1 item_maxhp_percent none 0 +att 1 1 tohit none 0 +block 1 1 toblock none 0 +cold-min 1 1 coldmindam none 0 +cold-max 1 1 coldmaxdam none 0 +cold-len 1 1 coldlength none 0 +fire-min 1 1 firemindam none 0 +fire-max 1 1 firemaxdam none 0 +ltng-min 1 1 lightmindam none 0 +ltng-max 1 1 lightmaxdam none 0 +pois-min 1 1 poisonmindam none 0 +pois-max 1 1 poisonmaxdam none 0 +pois-len 1 1 poisonlength none 0 +dmg-min 1 5 none 0 +dmg-max 1 6 none 0 +dmg% 1 7 none 0 +dmg-to-mana 1 1 item_damagetomana none 0 +res-fire 1 1 fireresist none 0 +res-fire-max 1 1 maxfireresist none 0 +res-ltng 1 1 lightresist none 0 +res-ltng-max 1 1 maxlightresist none 0 +res-cold 1 1 coldresist none 0 +res-cold-max 1 1 maxcoldresist none 0 +res-mag 1 1 magicresist none 0 +res-mag-max 1 1 maxmagicresist none 0 +res-pois 1 1 poisonresist none 0 +res-pois-max 1 1 maxpoisonresist none 0 +res-all 1 1 fireresist 3 lightresist 3 coldresist 3 poisonresist none 0 +res-all-max 1 1 maxfireresist 3 maxlightresist 3 maxcoldresist 3 maxpoisonresist none 0 +abs-fire% 1 1 item_absorbfire_percent none 0 +abs-fire 1 1 item_absorbfire none 0 +abs-ltng% 1 1 item_absorblight_percent none 0 +abs-ltng 1 1 item_absorblight none 0 +abs-mag% 1 1 item_absorbmagic_percent none 0 +abs-mag 1 1 item_absorbmagic none 0 +abs-cold% 1 1 item_absorbcold_percent none 0 +abs-cold 1 1 item_absorbcold none 0 +dur 1 1 durability none 0 +dur% 1 13 item_maxdurability_percent none 0 +regen 1 1 hpregen none 0 +thorns 1 1 item_attackertakesdamage none 0 +swing1 1 8 item_fasterattackrate none 0 +swing2 1 8 item_fasterattackrate none 0 +swing3 1 8 item_fasterattackrate none 0 +gold% 1 1 item_goldbonus none 0 +mag% 1 1 item_magicbonus none 0 +knock 1 1 item_knockback none 0 +regen-stam 1 1 staminarecoverybonus none 0 +regen-mana 1 1 manarecoverybonus none 0 +stam 1 1 maxstamina none 0 +time 1 1 item_timeduration none 0 +manasteal 1 1 manadrainmindam none 0 +lifesteal 1 1 lifedrainmindam none 0 +ama 1 0 21 item_addclassskills none 0 +pal 1 3 21 item_addclassskills none 0 +nec 1 2 21 item_addclassskills none 0 +sor 1 1 21 item_addclassskills none 0 +bar 1 4 21 item_addclassskills none 0 +herb 1 1 item_doubleherbduration none 0 +light 1 1 item_lightradius none 0 +color 1 1 1 item_lightcolor none 0 +ease 1 1 item_req_percent none 0 +move1 1 8 item_fastermovevelocity none 0 +move2 1 8 item_fastermovevelocity none 0 +move3 1 8 item_fastermovevelocity none 0 +balance1 1 8 item_fastergethitrate none 0 +balance2 1 8 item_fastergethitrate none 0 +balance3 1 8 item_fastergethitrate none 0 +block1 1 8 item_fasterblockrate none 0 +block2 1 8 item_fasterblockrate none 0 +block3 1 8 item_fasterblockrate none 0 +cast1 1 8 item_fastercastrate none 0 +cast2 1 8 item_fastercastrate none 0 +cast3 1 8 item_fastercastrate none 0 +res-pois-len 1 1 item_poisonlengthresist none 0 +dmg 1 1 item_normaldamage none 0 +howl 1 1 item_howl none 0 +stupidity 1 1 item_stupidity none 0 +ignore-ac 1 1 item_ignoretargetac none 0 +reduce-ac 1 1 item_fractionaltargetac none 0 +noheal 1 1 item_preventheal none 0 +half-freeze 1 1 item_halffreezeduration none 0 +att% 1 1 item_tohit_percent none 0 +dmg-ac 1 1 item_damagetargetac none 0 +dmg-demon 1 1 item_demondamage_percent none 0 +dmg-undead 1 1 item_undeaddamage_percent none 0 +att-demon 1 1 item_demon_tohit none 0 +att-undead 1 1 item_undead_tohit none 0 +throw 1 1 item_throwable none 0 +fireskill 1 1 21 item_elemskill 1 item_elemskillfire none 0 +allskills 1 1 item_allskills none 0 +light-thorns 1 1 item_attackertakeslightdamage none 0 +freeze 1 1 item_freeze chance in 128 length in frames none 0 +openwounds 1 1 item_openwounds none 0 +crush 1 1 item_crushingblow none 0 +kick 1 1 item_kickdamage none 0 +mana-kill 1 1 item_manaafterkill none 0 +demon-heal 1 1 item_healafterdemonkill none 0 +bloody 1 1 item_extrablood none 0 +deadly 1 1 item_deadlystrike none 0 +slow 1 1 item_slow none 0 +nofreeze 1 1 item_cannotbefrozen none 0 +stamdrain 1 1 item_staminadrainpct none 0 +reanimate 1 24 item_reanimate none 0 +pierce 1 1 item_pierce none 0 +magicarrow 1 1 item_magicarrow none 0 +explosivearrow 1 1 item_explosivearrow none 0 +Expansion 0 +dru 1 5 21 item_addclassskills All Druid Skills none 0 +ass 1 6 21 item_addclassskills All Assassin Skills none 0 +skill 1 22 item_singleskill none 0 +skilltab 1 10 item_addskill_tab none 0 +aura 1 22 item_aura none 0 +att-skill 1 11 item_skillonattack Proc Skill on Swing Skill # % Chance Level none 0 +hit-skill 1 11 item_skillonhit Proc Skill on Hit Skill # % Chance Level none 0 +gethit-skill 1 11 item_skillongethit Proc Skill on Get Hit Skill # % Chance Level none 0 +gembonus Increase chance of finding Gems none 0 +regen-dur none 0 +fire-fx none 0 +ltng-fx none 0 +sock 1 14 item_numsockets none 0 +dmg-fire 1 15 firemindam 16 firemaxdam Fire Damage Min Max none 0 +dmg-ltng 1 15 lightmindam 16 lightmaxdam Lightning Damage Min Max none 0 +dmg-mag 1 15 magicmindam 16 magicmaxdam Magic Damge Min Max none 0 +dmg-cold 1 15 coldmindam 16 coldmaxdam 17 coldlength Cold Damage Length (Frames) Min Max none 0 +dmg-pois 1 15 poisonmindam 16 poisonmaxdam 17 poisonlength Poison Damage Length (Frames) Min Max none 0 +dmg-throw 1 15 item_throw_mindamage 16 item_throw_maxdamage Throwing Damage Min Max none 0 +dmg-norm 1 15 mindamage 16 maxdamage Normal Damage Modifier Min Max none 0 +ac/lvl 1 17 item_armor_perlevel AC per Player Level ac/lvl (8ths) none 0 +ac%/lvl 1 17 item_armorpercent_perlevel AC% per Player Level ac%/lvl (8ths) none 0 +hp/lvl 1 17 item_hp_perlevel HP per Player Level hp/lvl (8ths) none 0 +mana/lvl 1 17 item_mana_perlevel Mana per Player Level (8ths) none 0 +dmg/lvl 1 17 item_maxdamage_perlevel Max Damage per Player Level (8ths) none 0 +dmg%/lvl 1 17 item_maxdamage_percent_perlevel Max Damage % per Player Level (8ths) none 0 +str/lvl 1 17 item_strength_perlevel Strength per Player Level (8ths) none 0 +dex/lvl 1 17 item_dexterity_perlevel Dexterity per Player Level (8ths) none 0 +enr/lvl 1 17 item_energy_perlevel Energy per Player Level (8ths) none 0 +vit/lvl 1 17 item_vitality_perlevel Vitality per Player Level (8ths) none 0 +att/lvl 1 17 item_tohit_perlevel Attack per Player Level att/lvl (1) none 0 +att%/lvl 1 17 item_tohitpercent_perlevel Attack% per Player Level att%/lvl (8ths) none 0 +dmg-cold/lvl 1 17 item_cold_damagemax_perlevel Max Cold Damage per Player Level (8ths) none 0 +dmg-fire/lvl 1 17 item_fire_damagemax_perlevel Max Fire Damage per Player Level (8ths) none 0 +dmg-ltng/lvl 1 17 item_ltng_damagemax_perlevel Max Lightning Dmg per Player Level (8ths) none 0 +dmg-pois/lvl 1 17 item_pois_damagemax_perlevel Max Poison Dmg per Player Level (8ths) none 0 +res-cold/lvl 1 17 item_resist_cold_perlevel Resist Cold% per Player Level (8ths) none 0 +res-fire/lvl 1 17 item_resist_fire_perlevel Resist Fire% per Player Level (8ths) none 0 +res-ltng/lvl 1 17 item_resist_ltng_perlevel Resist Lightning% per Player Level (8ths) none 0 +res-pois/lvl 1 17 item_resist_pois_perlevel Resist Poison% per Player Level (8ths) none 0 +abs-cold/lvl 1 17 item_absorb_cold_perlevel Absorb Cold Dmg per Player Level (8ths) none 0 +abs-fire/lvl 1 17 item_absorb_fire_perlevel Absorb Fire Dmg per Player Level (8ths) none 0 +abs-ltng/lvl 1 17 item_absorb_ltng_perlevel Absorb Lightning Dmg per Player Lvl (8ths) none 0 +abs-pois/lvl 1 17 item_absorb_pois_perlevel Absorb Poison Dmg per Player Lvl (8ths) none 0 +thorns/lvl 1 17 item_thorns_perlevel Damage to Attacker per Player Lvl (8ths) none 0 +gold%/lvl 1 17 item_find_gold_perlevel +% Gold Dropped per Player Lvl (8ths) none 0 +mag%/lvl 1 17 item_find_magic_perlevel +% Magical per Player Lvl (8ths) none 0 +regen-stam/lvl 1 17 item_regenstamina_perlevel +% Stamina Regeneration per Player Lvl (8ths) none 0 +stam/lvl 1 17 item_stamina_perlevel Stamina per Player Level (8ths) none 0 +dmg-dem/lvl 1 17 item_damage_demon_perlevel Damage to Demons % per Player Level (8ths) none 0 +dmg-und/lvl 1 17 item_damage_undead_perlevel Damage to Undead % per Player Level (8ths) none 0 +att-dem/lvl 1 17 item_tohit_demon_perlevel Attack Demons + per Player Level att/lvl (1) none 0 +att-und/lvl 1 17 item_tohit_undead_perlevel Attack Undead + per Player Level att/lvl (1) none 0 +crush/lvl 1 17 item_crushingblow_perlevel +% Chance of Crushing Blow per Player Level (8ths) none 0 +wounds/lvl 1 17 item_openwounds_perlevel +% Chance of Open Wounds per Player Level (8ths) none 0 +kick/lvl 1 17 item_kick_damage_perlevel Kick Damage per Player Level (8ths) none 0 +deadly/lvl 1 17 item_deadlystrike_perlevel +% Chance of Deadly Strike per Player Level (8ths) none 0 +gems%/lvl +% Chance of finding Gems per Player Level (8ths) none 0 +rep-dur 1 17 item_replenish_durability regenerates durability speed (see note) regenerates 1 durability point per 100/lvl seconds 0 +rep-quant 1 17 item_replenish_quantity regenerates quantity speed (see note) regenerates 1 quantity per 100/lvl seconds 0 +stack 1 1 item_extra_stack Increased stack size min count max count none 0 +item% +% Chance of finding item item type min chance max chance random chance from min to max is assigned to item 0 +dmg-slash Slashing Damage min max random amount from min to max is assigned to item 0 +dmg-slash% Slashing Damage % min % max % random amount from min to max is assigned to item 0 +dmg-crush Crush Damage min max random amount from min to max is assigned to item 0 +dmg-crush% Crush Damage % min % max % random amount from min to max is assigned to item 0 +dmg-thrust Thrust Damage min max random amount from min to max is assigned to item 0 +dmg-thrust% Thrust Damage % min % max % random amount from min to max is assigned to item 0 +abs-slash Absorb Slashing Damage min amt max amt random amount from min to max is assigned to item 0 +abs-crush Absorb Crushing Damage min amt max amt random amount from min to max is assigned to item 0 +abs-thrust Absorb Thrusting Damage min amt max amt random amount from min to max is assigned to item 0 +abs-slash% Absorb Slashing Damage % min % max % random amount from min to max is assigned to item 0 +abs-crush% Absorb Crushing Damage % min % max % random amount from min to max is assigned to item 0 +abs-thrust% Absorb Thrusting Damage % min % max % random amount from min to max is assigned to item 0 +ac/time 1 18 item_armor_bytime "AC / time increment (0=day, 1=dusk, 2=night, 3=dawn)" center period min max "max at center period, min at opposite period, linear progression" 0 +ac%/time 1 18 item_armorpercent_bytime AC% / time increment (8 periods) center period min max "max at center period, min at opposite period, linear progression" 0 +hp/time 1 18 item_hp_bytime HP / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +mana/time 1 18 item_mana_bytime Mana / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +dmg/time 1 18 item_maxdamage_bytime Max Damage / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +dmg%/time 1 18 item_maxdamage_percent_bytime Max Damage % / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +str/time 1 18 item_strength_bytime Strength / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +dex/time 1 18 item_dexterity_bytime Dexterity / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +enr/time 1 18 item_energy_bytime Energy / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +vit/time 1 18 item_vitality_bytime Vitality / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +att/time 1 18 item_tohit_bytime To hit / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +att%/time 1 18 item_tohitpercent_bytime To Hit % / time increment center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-cold/time 1 18 item_cold_damagemax_bytime Cold Damage Max / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-fire/time 1 18 item_fire_damagemax_bytime Fire Damage Max / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-ltng/time 1 18 item_ltng_damagemax_bytime Lightning Damage Max / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-pois/time 1 18 item_pois_damagemax_bytime Poison Damage Max / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +res-cold/time 1 18 item_resist_cold_bytime Resist Cold / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +res-fire/time 1 18 item_resist_fire_bytime Resist Fire / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +res-ltng/time 1 18 item_resist_ltng_bytime Resist Lightning / time inc center period min max "max at center period, min at opposite period, linear progression" 0 +res-pois/time 1 18 item_resist_pois_bytime Resist Poison / time inc center period min max "max at center period, min at opposite period, linear progression" 0 +abs-cold/time 1 18 item_absorb_cold_bytime Absorb Cold / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +abs-fire/time 1 18 item_absorb_fire_bytime Absorb Fire / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +abs-ltng/time 1 18 item_absorb_ltng_bytime Absorb Lightning / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +abs-pois/time 1 18 item_absorb_pois_bytime Absorb Poison / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +gold%/time 1 18 item_find_gold_bytime Find Gold Amt % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +mag%/time 1 18 item_find_magic_bytime Find Magic % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +regen-stam/time 1 18 item_regenstamina_bytime % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +stam/time 1 18 item_stamina_bytime Stamina / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-dem/time 1 18 item_damage_demon_bytime Damage to Demons % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +dmg-und/time 1 18 item_damage_undead_bytime Damage to Undead % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +att-dem/time 1 18 item_tohit_demon_bytime To Hit Demons % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +att-und/time 1 18 item_tohit_undead_bytime To Hit Undead % / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +crush/time 1 18 item_crushingblow_bytime % chance of Crushing Blow / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +wounds/time 1 18 item_openwounds_bytime +% chance of Open Wounds / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +kick/time 1 18 item_kick_damage_bytime Kick Damage / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +deadly/time 1 18 item_deadlystrike_bytime +% chance of Deadly Strike / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +gems%/time +% chance of finding Gems / time inc. center period min max "max at center period, min at opposite period, linear progression" 0 +pierce-fire 1 1 passive_fire_pierce Negates % of Enemy Cold Resistance min max none 0 +pierce-ltng 1 1 passive_ltng_pierce Negates % of Enemy Fire Resistance min max none 0 +pierce-cold 1 1 passive_cold_pierce Negates % of Enemy Lightning Resistance min max none 0 +pierce-pois 1 1 passive_pois_pierce Negates % of Enemy Poison Resistance min max none 0 +dmg-mon Damage vs. specific Monster Type monster min max none 0 +dmg%-mon Damage % vs. specific Monster Type monster min max none 0 +att-mon To Hit vs. specific Monster Type monster min max none 0 +att%-mon To Hit % vs. specific Monster Type monster min max none 0 +ac-mon AC vs. specific Monster Type monster min max none 0 +ac%-mon AC% vs. specific Monster Type monster min max none 0 +indestruct 1 20 Indestructible none 0 +charged 1 19 item_charged_skill Charged Skill skill charges level none 0 +extra-fire 1 1 passive_fire_mastery 0 +extra-ltng 1 1 passive_ltng_mastery 0 +extra-cold 1 1 passive_cold_mastery 0 +extra-pois 1 1 passive_pois_mastery 0 +dmg-elem 1 15 firemindam 16 firemaxdam 15 lightmindam 16 lightmaxdam 15 coldmindam 16 coldmaxdam 17 coldlength 0 +dmg-elem-min 1 1 firemindam 3 lightmindam 3 coldmindam 0 +dmg-elem-max 1 1 firemaxdam 3 lightmaxdam 3 coldmaxdam 17 coldlength 0 +all-stats 1 1 strength 3 energy 3 dexterity 3 vitality 0 +addxp 1 1 item_addexperience additional xp gain 0 +heal-kill 1 1 item_healafterkill life gained after each kill 0 +cheap 1 1 item_reducedprices reduces vendor prices min max none 0 +rip 1 1 item_restinpeace slain monsters' corpses can't be used 1 1 none 0 +att-mon% 1 24 attack_vs_montype attack% vs. monster type mon type min max 0 +dmg-mon% 1 24 damage_vs_montype damage% vs. monster type mon type min max 0 +kill-skill 1 11 item_skillonkill Proc Skill on killing something Skill # % Chance Level 0 +death-skill 1 11 item_skillondeath Proc Skill on getting killed Skill # % Chance Level 0 +levelup-skill 1 11 item_skillonlevelup Proc Skill on level up Skill # % Chance Level 0 +skill-rand 1 12 item_singleskill Bonus to random skill Level min Skill # max Skill # 0 +fade 1 17 fade only use on monprop fade type 0 +levelreq 1 1 item_levelreq 0 +ethereal 1 23 0 +oskill 1 22 item_nonclassskill Skill min level max level none 0 +state 1 24 state state 1 1 0 +str% 1 1 item_strength_percent 0 +dex% 1 1 item_dexterity_percent 0 +vit% 1 1 item_vitality_percent 0 +enr% 1 1 item_energy_percent 0 +str%/lvl 1 17 item_strengthpercent_perlevel Strength per Player Level (8ths) none 0 +dex%/lvl 1 17 item_dexteritypercent_perlevel Dexterity per Player Level (8ths) none 0 +enr%/lvl 1 17 item_energypercent_perlevel Energy per Player Level (8ths) none 0 +vit%/lvl 1 17 item_vitalitypercent_perlevel Vitality per Player Level (8ths) none 0 +thorns-blind 1 11 item_attackergetsblind attacker gets blinded min max armour only 0 +thorns-flee 1 11 item_attackerflees attacker runs away min max armour only 0 +fire-thorns 1 1 item_attackertakesfiredamage 0 +cold-thorns 1 1 item_attackertakescolddamage 0 +dmg-max/str 1 17 item_maxdamage_perstr (8ths) 0 +dmg-max/dex 1 17 item_maxdamage_perdex (8ths) 0 +dmg-min/lvl 1 17 item_mindamage_perlvl (8ths) 0 +dmg-min/str 1 17 item_mindamage_perstr (8ths) 0 +dmg-min/dex 1 17 item_mindamage_perdex (8ths) 0 +dmg%/str 1 17 item_maxdamage_percent_perstr (8ths) 0 +dmg%/dex 1 17 item_maxdamage_percent_perdex (8ths) 0 +wounds/dex 1 17 item_openwounds_perdex (8ths) 0 +deadly/dex 1 17 item_deadlystrike_perdex (8ths) 0 +wounds/str 1 17 item_openwounds_perstr (8ths) 0 +deadly/str 1 17 item_deadlystrike_perstr (8ths) 0 +ac/str 1 17 item_armor_perstr armour per strength (8ths) armour only 0 +ac/dex 1 17 item_armor_perdex armour class per dex (8ths) min max armour only 0 +att/str 1 17 item_tohit_perstr to hit per dex (8ths) min max weap only 0 +att/dex 1 17 item_tohit_perDex to hit per dex (8ths) min max weap only 0 +hp/vit 1 17 item_hp_pervitality (8ths) 0 +mana/enr 1 17 item_mana_perenr (8ths) 0 +cast/enr 1 17 item_fastercastrate_perenr (8ths) 0 +block/dex 1 17 item_fasterblockrate_perdex (8ths) 0 +move/vit 1 17 item_fastermovevelocity_pervitality (8ths) 0 +swing/str 1 17 item_fasterswingvelocity_perstrength (8ths) 0 +dmg-min%/str 1 17 item_mindamage_percent_perstr (8ths) 0 +dmg-min%/dex 1 17 item_mindamage_percent_perdex (8ths) 0 +dmg-min%/lvl 1 17 item_mindamage_percent_perlvl (8ths) 0 +crush/str 1 17 item_crushingblow_perstr +% Chance of Crushing Blow per Player Level (8ths) none 0 +crush/dex 1 17 item_crushingblow_perdex +% Chance of Crushing Blow per Player Level (8ths) none 0 +coldskill 1 4 21 item_elemskill 1 item_elemskillcold none 0 +lightningskill 1 2 21 item_elemskill 1 item_elemskilllight none 0 +poisonskill 1 5 21 item_elemskill 1 item_elemskillpoison none 0 +magicskill 1 3 21 item_elemskill 1 item_elemskillmagic none 0 +ac%/str 1 17 item_armorpercent_perstr AC% per Player Level ac%/lvl (8ths) none 0 +ac%/dex 1 17 item_armorpercent_perdex AC% per Player Level ac%/lvl (8ths) none 0 +att%/str 1 17 item_tohitpercent_perstr Attack% per Player Level att%/lvl (8ths) none 0 +att%/dex 1 17 item_tohitpercent_perdex Attack% per Player Level att%/lvl (8ths) none 0 +swing/dex 1 17 item_fasterswingvelocity_perdex (8ths) 0 +swing/lvl 1 17 item_fasterswingvelocity_perlvl (8ths) 0 +block/str 1 17 item_fasterblockrate_perstr (8ths) 0 +block/lvl 1 17 item_fasterblockrate_perlvl (8ths) 0 +move/dex 1 17 item_fastermovevelocity_perdex (8ths) 0 +move/lvl 1 17 item_fastermovevelocity_perlvl (8ths) 0 +cast/lvl 1 17 item_fastercastrate_perlvl (8ths) 0 +balance/vit 1 17 item_fasterhitrecovery_pervit (8ths) 0 +balance/lvl 1 17 item_fasterhitrecovery_perlvl (8ths) 0 +block%/dex 1 17 item_increasedblock_perdex (8ths) 0 +block%/lvl 1 17 item_increasedblock_perlvl (8ths) 0 +addxp/lvl 1 17 item_addexperience_perlvl additional xp gain 0 +addxp/enr 1 17 item_addexperience_perenr additional xp gain 0 +cheap/lvl 1 17 item_reducedprices_perlvl reduces vendor prices min max none 0 +pierce/lvl 1 17 item_pierce_perlvl none 0 +pierce/str 1 17 item_pierce_perstr none 0 +all-zero-display 1 1 resmax_display_all_zero 0 +res-all-hidden 1 1 magicresist_hidden 3 fireresist_hidden 3 lightresist_hidden 3 coldresist_hidden 3 poisonresist_hidden 3 hiddenresist_dummy 0 +res-all-max-hidden 1 1 maxmagicresist_hidden 3 maxfireresist_hidden 3 maxlightresist_hidden 3 maxcoldresist_hidden 3 maxpoisonresist_hidden 3 hiddenresist_dummy 0 +res-all-max% 1 1 maxmagres_percent 3 maxfireres_percent 3 maxlightres_percent 3 maxcoldres_percent 3 maxpoisres_percent 0 +res-all/lvl 1 17 item_resist_cold_perlevel 3 item_resist_ltng_perlevel 3 item_resist_fire_perlevel 3 item_resist_pois_perlevel none 0 +slow/lvl 1 17 item_slow_perlvl none 0 +kick/str 1 17 item_kick_damage_perstr Kick Damage per Player Level (8ths) none 0 +kick/dex 1 17 item_kick_damage_perdex Kick Damage per Player Level (8ths) none 0 +red-dmg%/lvl 1 17 damageresist_perlvl none 0 +red-dmg%/vit 1 17 damageresist_pervit none 0 +red-mag/lvl 1 17 magic_damage_reduction_perlvl none 0 +red-mag/enr 1 17 magic_damage_reduction_perenr none 0 +res-mag/lvl 1 17 magicresist_perlvl none 0 +res-mag/enr 1 17 magicresist_perenr none 0 +stupidity/lvl 1 17 item_stupidity_perlvl none 0 +freeze/lvl 1 17 item_freeze_perlvl chance in 128 length in frames none 0 +freeze/enr 1 17 item_freeze_perenr chance in 128 length in frames none 0 +res-pois-len/lvl 1 17 item_poisonlengthresist_perlvl none 0 +res-pois-len/vit 1 17 item_poisonlengthresist_pervit none 0 +res-curse/lvl 1 17 curse_resistance_perlvl 0 +res-curse/enr 1 17 curse_resistance_perenr 0 +res-curse-len 1 1 curse_resistance_length 1 curse_resistance 0 +mana-kill/lvl 1 17 item_manaafterkill_perlvl none 0 +mana-kill/enr 1 17 item_manaafterkill_perenr none 0 +dmg-mana/lvl 1 17 item_damagetomana_perlvl none 0 +dmg-mana/enr 1 17 item_damagetomana_perenr none 0 +dmg-mana/mana 1 17 item_damagetomana_permana none 0 +pruby 1 1 gembag_pruby 0 +fruby 1 1 gembag_fruby 0 +nruby 1 1 gembag_nruby 0 +flruby 1 1 gembag_flruby 0 +cruby 1 1 gembag_cruby 0 +pamethyst 1 1 gembag_pamethyst 0 +famethyst 1 1 gembag_famethyst 0 +namethyst 1 1 gembag_namethyst 0 +flamethyst 1 1 gembag_flamethyst 0 +camethyst 1 1 gembag_camethyst 0 +pdiamond 1 1 gembag_pdiamond 0 +fdiamond 1 1 gembag_fdiamond 0 +ndiamond 1 1 gembag_ndiamond 0 +fldiamond 1 1 gembag_fldiamond 0 +cdiamond 1 1 gembag_cdiamond 0 +pemerald 1 1 gembag_pemerald 0 +femerald 1 1 gembag_femerald 0 +nemerald 1 1 gembag_nemerald 0 +flemerald 1 1 gembag_flemerald 0 +cemerald 1 1 gembag_cemerald 0 +psapphire 1 1 gembag_psapphire 0 +fsapphire 1 1 gembag_fsapphire 0 +nsapphire 1 1 gembag_nsapphire 0 +flsapphire 1 1 gembag_flsapphire 0 +csapphire 1 1 gembag_csapphire 0 +ptopaz 1 1 gembag_ptopaz 0 +ftopaz 1 1 gembag_ftopaz 0 +ntopaz 1 1 gembag_ntopaz 0 +fltopaz 1 1 gembag_fltopaz 0 +ctopaz 1 1 gembag_ctopaz 0 +pskull 1 1 gembag_pskull 0 +fskull 1 1 gembag_fskull 0 +nskull 1 1 gembag_nskull 0 +flskull 1 1 gembag_flskull 0 +cskull 1 1 gembag_cskull 0 +rbgr01 1 1 runebag_r01 0 +rbgr02 1 1 runebag_r02 0 +rbgr03 1 1 runebag_r03 0 +rbgr04 1 1 runebag_r04 0 +rbgr05 1 1 runebag_r05 0 +rbgr06 1 1 runebag_r06 0 +rbgr07 1 1 runebag_r07 0 +rbgr08 1 1 runebag_r08 0 +rbgr09 1 1 runebag_r09 0 +rbgr10 1 1 runebag_r10 0 +rbgr11 1 1 runebag_r11 0 +rbgr12 1 1 runebag_r12 0 +rbgr13 1 1 runebag_r13 0 +rbgr14 1 1 runebag_r14 0 +rbgr15 1 1 runebag_r15 0 +rbgr16 1 1 runebag_r16 0 +rbgr17 1 1 runebag_r17 0 +rbgr18 1 1 runebag_r18 0 +rbgr19 1 1 runebag_r19 0 +rbgr20 1 1 runebag_r20 0 +rbgr21 1 1 runebag_r21 0 +rbgr22 1 1 runebag_r22 0 +rbgr23 1 1 runebag_r23 0 +rbgr24 1 1 runebag_r24 0 +rbgr25 1 1 runebag_r25 0 +rbgr26 1 1 runebag_r26 0 +rbgr27 1 1 runebag_r27 0 +rbgr28 1 1 runebag_r28 0 +rbgr29 1 1 runebag_r29 0 +rbgr30 1 1 runebag_r30 0 +rbgr31 1 1 runebag_r31 0 +rbgr32 1 1 runebag_r32 0 +rbgr33 1 1 runebag_r33 0 +magharv 1 1 magharv 0 +showkills 1 17 display_kills 0 +iforge 1 1 iforge 0 diff --git a/UniqueItems.txt b/UniqueItems.txt new file mode 100644 index 0000000..2f2eaa6 --- /dev/null +++ b/UniqueItems.txt @@ -0,0 +1,1433 @@ +index version enabled ladder rarity nolimit lvl lvl req code *type *uber carry1 cost mult cost add chrtransform invtransform flippyfile invfile dropsound dropsfxframe usesound prop1 par1 min1 max1 prop2 par2 min2 max2 prop3 par3 min3 max3 prop4 par4 min4 max4 prop5 par5 min5 max5 prop6 par6 min6 max6 prop7 par7 min7 max7 prop8 par8 min8 max8 prop9 par9 min9 max9 prop10 par10 min10 max10 prop11 par11 min11 max11 prop12 par12 min12 max12 *eol +bl1 0 +Axes , 1-Handed 0 +Auburnfire 100 1 1 18 18 hax hand axe 5 5000 cred cred dmg-min 10 15 dmg-max 35 45 dmg-fire 35 50 dmg-ltng 25 50 res-ltng 15 25 0 +Gangrene Reaper 100 1 3 1 6 hax hand axe 5 5000 cgrn invaxe19 dmg% 100 150 dmg-pois 100 55 55 noheal 1 1 rep-dur 15 str 10 10 0 +The Gnasher 0 1 5 1 4 hax Hand Axe 5 5000 dyel invhaxu str 6 10 openwounds 50 50 crush 15 25 dmg% 60 100 0 +Easebringer 100 1 1 22 22 axe axe 5 5000 invaxe13 dmg-min 15 20 dmg-max 40 50 ease -40 -40 swing2 25 25 lifesteal 6 6 0 +Goreflood 100 1 3 1 10 axe axe 5 5000 cred cred dmg% 120 170 bloody 1 1 wounds/lvl 24 skilltab 13 1 3 lifesteal 3 5 pierce-fire 5 10 0 +Deathspade 0 1 5 1 7 axe Axe 5 5000 dgld invaxeu stupidity 1 1 dmg-min 4 12 att% 10 20 mana-kill 3 5 dmg% 60 100 dmg-max 5 10 0 +Longsuffering 100 1 1 25 25 2ax double axe 5 5000 dgrn invaxe12 dmg-norm 20 50 cold-len 200 dmg-pois 100 200 200 slow 20 20 noheal 1 1 openwounds 44 44 0 +Lungreaver 100 1 3 1 14 2ax double axe 5 5000 invaxe11 dmg% 130 180 slow 10 10 lifesteal 5 7 move1 15 15 sock 1 2 0 +Bladebone 0 1 5 1 11 2ax Double Axe 5 5000 lgry lgry dmg-undead 100 100 att-undead 40 100 swing2 20 20 ac 15 25 fire-min 8 8 fire-max 10 15 dmg% 75 100 0 +Broken Earth 100 1 1 29 30 mpi military pick 5 5000 lgry lgry dmg-min 15 20 dmg-max 30 60 hit-skill 234 6 3 dru 1 1 swing1 15 15 res-cold 33 33 res-pois 33 33 0 +Occam's Razor 100 1 3 1 23 mpi military pick 5 5000 dmg% 140 190 deadly 40 40 allskills 1 1 aura 99 5 7 oskill axe mastery 3 6 res-fire 15 25 0 +Mindrend 0 1 5 1 18 mpi Military Pick 5 5000 lgrn invmpiu stupidity 2 2 regen-mana 20 30 ltng-min 1 1 ltng-max 12 15 att 50 100 dmg% 60 100 openwounds 15 15 0 +Shadazar's Answer 100 1 1 30 35 wax war axe 5 5000 lblu lblu dmg-norm 20 70 allskills 1 1 knock 2 2 lifesteal 5 5 mana-kill 3 5 red-dmg 8 10 dur 40 40 0 +Deathflake 100 1 3 1 30 wax war axe 5 5000 dmg% 175 200 dmg-max 20 30 charged raise skeleton 2 40 heal-kill 5 8 dmg-undead 200 200 aura 119 2 4 0 +Rakescar 0 1 5 1 27 wax War Axe 5 5000 dgry dgry pois-min 128 128 pois-max 128 128 pois-len 75 75 att 50 50 res-pois 50 50 dmg% 75 150 swing2 30 30 0 +Bloody Scalp 100 1 1 55 44 9ha hatchet 5 5000 dmg% 100 135 dmg/lvl 8 swing2 20 20 bloody 1 1 deadly 17 17 openwounds 77 77 att% 20 20 0 +Pure Rancor 100 1 3 1 38 9ha hatchet 5 5000 dgrn dgrn invhax dmg% 175 200 bar 2 2 swing2 20 20 res-pois 25 25 dmg-pois 100 466 466 oskill decrepify 1 3 dur 45 45 0 +Coldkill 100 1 5 1 32 9ha Hatchet 5 5000 cblu cblu dmg-cold 50 40 40 res-cold 15 15 res-cold-max 15 15 swing3 30 30 hit-skill 45 10 10 gethit-skill 44 10 5 dmg% 150 190 0 +Slayer of Trents 100 1 3 1 45 9ax cleaver 5 5000 dgrn invaxe3 dmg% 170 200 dmg-norm 15 30 dmg-demon 100 100 reduce-ac 15 15 rep-dur 10 charged 130 15 10 res-pois 25 35 0 +Edge of Forever 100 1 1 50 49 9ax cleaver 5 5000 invaxe dmg% 180 210 allskills 1 1 gethit-skill 53 7 12 rep-dur 15 dmg-norm 20 50 res-pois-len 35 35 sock 1 2 0 +Butcher's Pupil 100 1 5 1 39 9ax Cleaver 5 5000 cblu invaxe8 deadly 35 35 openwounds 25 25 dmg% 150 200 indestruct 1 1 swing3 30 30 dmg-norm 30 50 0 +Cryohydra 100 1 1 55 56 92a twin axe 5 5000 dblu invaxe7 dmg% 150 200 dmg-max 30 50 dmg-cold 350 10 20 freeze 5 5 res-fire 55 55 res-cold 30 40 gethit-skill 39 75 20 0 +Harvest of Souls 100 1 3 1 50 92a twin axe 5 5000 blac invaxe22 dmg% 190 230 swing3 30 30 oskill bone spirit 4 7 aura 118 8 12 dmg-undead 175 175 lifesteal 5 8 noheal 1 1 0 +Islestrike 100 1 5 1 43 92a Twin Axe 5 5000 dru 2 2 str 10 10 dex 10 10 vit 10 10 enr 10 10 ac-miss 50 50 crush 25 25 dmg% 170 190 skill 233 1 1 skill 248 1 1 0 +Cornugon 100 1 3 1 53 9mp crowbill 5 5000 oran oran dmg% 160 220 dmg-demon 250 250 manasteal 10 10 demon-heal 35 35 ac 35 50 red-dmg 10 15 red-mag 8 12 res-ltng 15 25 0 +Groundshatter 100 1 1 55 60 9mp crowbill 5 5000 invmpi dmg% 200 240 hit-skill 234 12 7 fireskill 2 2 att 250 250 str 20 20 dmg-max 30 50 red-dmg 5 9 res-fire 35 40 res-cold 25 35 0 +Pompe's Wrath 100 1 5 1 45 9mp Crowbill 5 5000 cred cred hit-skill 244 4 8 slow 50 50 dmg-fire 35 150 knock 1 1 dmg% 140 170 0 +Couatl 100 1 1 55 65 9wa naga 5 5000 lgrn invaxe5 dmg% 175 200 dmg-norm 10 50 swing3 50 50 move3 50 50 balance3 50 50 block3 50 50 0 +Antics of the Jester 100 1 3 1 57 9wa naga 5 5000 oran oran invwax dmg% 225 260 howl 1 66 heal-kill 1 10 res-all 1 35 extra-cold 1 10 swing3 55 55 sock 1 2 0 +Guardian Naga 100 1 5 1 48 9wa Naga 5 5000 hit-skill 92 5 8 thorns 15 15 res-pois 30 30 dmg% 150 180 dmg-pois 250 256 256 dmg-max 20 20 0 +Marilith 100 1 1 68 7ha tomahawk 5 5000 dyel dyel dmg% 200 240 allskills 2 2 ignore-ac 1 1 aura 114 1 5 lifesteal 6 6 manasteal 6 6 dmg-to-mana 20 20 0 +Razoredge 100 1 2 1 58 7ha Tomahawk 5 5000 lblu dmg% 175 225 swing2 40 40 reduce-ac 33 33 deadly 50 50 openwounds 50 50 0 +Death Slaad 100 1 1 71 7ax small crescent 5 5000 blac dgry invaxe4 dmg% 350 450 dmg/lvl 8 swing3 60 60 rip 1 1 0 +Moon Blade 100 1 2 1 64 7ax Small Crescent 5 5000 dmg% 190 280 block 15 25 block1 15 15 balance1 20 20 oskill Life Tap 3 5 res-all 20 25 0 +Beholder 100 1 1 75 72a ettin axe 5 5000 invaxe6 dmg% 250 290 swing1 15 15 ac% 25 25 red-dmg% 15 15 res-mag 75 75 res-all 60 80 0 +Runemaster 100 1 2 1 69 72a Ettin Axe 5 5000 lblu lblu dmg% 220 270 sock 3 5 res-cold-max 5 15 nofreeze 1 1 0 +Elder Tojanida 100 1 1 80 7mp war spike 5 5000 invaxe2 dmg%/lvl 33 addxp 3 5 dmg-min 50 75 res-ltng 30 40 res-cold 25 25 extra-fire 15 15 sock 1 1 0 +Cranebeak 100 1 2 1 73 7mp War Spike 3 5000 dgry dmg% 240 300 dmg-ltng 1 305 swing2 40 40 reduce-ac 25 25 mag% 20 50 charged Raven 15 8 0 +Frostwyrm 100 1 1 84 7wa berserker axe 5 5000 lblu lblu invaxe9 dmg% 150 200 sock 3 3 swing2 20 20 hit-skill 44 33 35 gethit-skill 45 8 19 abs-cold% 50 50 res-cold 25 25 0 +Deathcleaver 100 1 2 1 77 7wa Berserker Axe 5 5000 blac blac dmg% 230 280 deadly 66 66 reduce-ac 33 33 swing2 40 40 heal-kill 6 9 0 +bl2 0 +Axes , 2-Handed 0 +Light Phasm 100 1 1 20 21 lax large axe 5 5000 whit whit dmg-min 10 20 dmg-max 35 45 light 3 3 dmg-ltng 1 75 res-ltng 35 40 dmg-undead 150 150 dmg% 70 100 0 +Gracehunter 100 1 3 1 8 lax large axe 5 5000 dmg% 100 150 swing2 20 20 mag% 20 20 gold% 75 75 manasteal 6 6 0 +Fechmars Axe 0 1 5 1 6 lax Large Axe 5 5000 lpur lpur dmg% 70 90 freeze 3 3 res-cold 50 50 light 2 2 0 +Night Hag 100 1 1 24 23 bax broad axe 5 5000 blac blac dmg-min 10 20 dmg-max 40 50 dmg-pois 75 133 133 lifesteal 7 7 stupidity 1 1 swing2 15 15 dmg% 70 100 0 +Mirth Bringer 100 1 3 1 14 bax broad axe 5 5000 dmg% 110 160 hit-skill 8 10 3 res-pois 20 25 res-cold 20 25 addxp 3 5 0 +Goreshovel 0 1 5 1 11 bax Broad Axe 5 5000 dpur dpur swing3 30 30 str 25 25 openwounds 60 60 dmg% 70 110 dmg-max 9 9 0 +Tendriculos 100 1 1 27 27 btx battle axe 5 5000 dmg-min 15 25 dmg-max 50 65 dur 40 40 att% 25 35 crush 15 25 reduce-ac 10 15 dmg% 70 100 0 +Sandking 100 1 3 1 23 btx battle axe 5 5000 lyel lyel invaxe1 dmg% 120 170 stupidity 2 2 dmg-fire 8 14 att 120 120 reduce-ac 10 15 0 +The Chieftan 0 1 5 1 19 btx Battle Axe 5 5000 oran invbtxu dmg% 100 125 res-all 10 20 mana-kill 6 6 swing2 20 20 dmg-ltng 1 40 0 +Winter Wolf 100 1 1 30 33 gax great axe 5 5000 invaxe16 dmg-min 20 30 dmg-max 55 75 dmg% 70 100 swing2 25 25 gethit-skill 50 6 4 charged 227 15 8 sock 1 2 dmg% 70 80 0 +Nova Spine 100 1 3 1 28 gax great axe 5 5000 cblu cblu invgax dmg% 140 200 hit-skill 44 100 9 res-cold 25 30 res-cold-max 5 5 dmg-ltng 1 30 sock 1 2 0 +Brainhew 0 1 5 1 25 gax Great Axe 5 5000 whit invgaxu dmg-min 14 14 mana 25 25 light 4 4 manasteal 10 13 dmg% 50 80 dmg-fire 15 35 dmg-max 20 25 0 +Taskmaster's Curse 100 1 1 31 37 gix giant axe 5 5000 dred dred dmg-min 30 40 dmg-max 60 80 hit-skill 81 4 2 manasteal 6 6 str 45 45 ease 35 35 dex 20 20 hp 30 50 dmg% 70 100 0 +Curseweaver 100 1 3 1 32 gix giant axe 5 5000 invaxe10 dmg% 165 200 hit-skill 87 12 5 swing2 25 25 manasteal 5 7 bar 1 1 oskill axe mastery 1 3 0 +The Humongous 0 1 5 1 29 gix Giant Axe 5 5000 blac blac str 20 30 dmg-min 8 8 dmg-max 15 25 crush 33 66 ease 20 20 dmg% 80 100 0 +Sunblighter 100 1 1 55 45 9la military axe 5 5000 dyel dyel dmg% 180 230 dmg-fire 100 300 res-all 25 25 deadly 44 44 extra-fire 15 15 stupidity 2 2 res-fire 40 50 0 +Threat of Storms 100 1 3 1 40 9la military axe 5 5000 dgry dgry invlax dmg% 175 225 dmg-cold 200 25 40 dmg-ltng 1 150 dmg-fire 40 100 res-all 25 25 aura 118 5 10 swing1 15 15 0 +Warlord's Trust 100 1 5 1 35 9la Military Axe 5 5000 whit whit ac 75 75 regen 20 20 vit/lvl 4 res-all 10 10 rep-dur 25 dmg% 175 175 0 +Dwarven Honor 100 1 1 55 51 9ba bearded axe 5 5000 dgry dgry dmg% 180 220 dmg-norm 25 50 red-dmg% 10 15 red-dmg 8 10 rep-dur 20 swing2 20 20 ac 100 150 0 +Grandiose Dreams 100 1 3 1 44 9ba bearded axe 5 5000 dmg% 180 230 mag% 40 40 dmg-max 40 60 allskills 1 1 crush 40 40 hp 60 60 move2 20 20 0 +Spellsteel 100 1 5 1 39 9ba Bearded Axe 5 5000 whit whit ease -60 -60 mana 100 100 red-mag 12 15 cast1 10 10 dmg% 165 165 regen-mana 25 25 charged 54 20 1 charged 87 30 3 charged 101 100 10 charged 225 60 12 0 +Ravid's Bite 100 1 3 1 47 9bt tabar 5 5000 dmg% 200 250 hit-skill 238 12 2 swing3 30 30 all-stats 20 20 hp% 15 15 slow 10 20 sock 2 2 allskills 1 1 0 +Spirit Naga 100 1 1 55 54 9bt tabar 5 5000 lgrn lgrn invbtx dmg% 190 240 rep-dur 18 ethereal 1 1 dmg-demon 150 150 res-fire 35 35 res-fire-max 10 10 bar 2 2 manasteal 8 8 0 +Stormrider 100 1 5 1 41 9bt Tabar 5 5000 lred lred hit-skill 53 5 10 hit-skill 38 10 0 dmg-ltng 1 200 dmg-norm 35 75 dmg% 100 100 dur 50 50 light-thorns 15 15 gethit-skill 38 15 5 0 +Brittlequick 100 1 1 55 59 9ga gothic axe 5 5000 whit invaxe15 dur -45 -45 rep-dur 5 swing3 50 50 dmg% 200 250 dmg-norm 20 40 reduce-ac 10 15 thorns 20 25 bar 1 1 0 +Tonstrike 100 1 3 1 50 9ga gothic axe 5 5000 oran invaxe18 dmg% 200 250 swing3 30 30 str/lvl 4 deadly 25 25 hit-skill 229 4 9 dru 1 2 sock 2 2 0 +Boneslayer Blade 100 1 5 1 42 9ga Gothic Axe 5 5000 att-und/lvl 10 dmg-und/lvl 20 str 8 8 swing2 20 20 att% 35 35 dmg% 180 220 charged 101 200 20 gethit-skill 101 50 0 0 +Ogre's Breath 100 1 3 1 52 9gi ancient axe 5 5000 lgry lgry dmg% 220 270 swing2 20 20 red-dmg% 10 15 gethit-skill 87 2 30 str 15 25 dex -5 -5 regen 1 4 move1 10 10 0 +Mythslayer 100 1 1 55 58 9gi ancient axe 5 5000 dgld dgld invgix dmg% 150 200 dmg%/lvl 12 all-stats 15 15 swing2 25 25 att% 30 30 allskills 1 1 charged 142 20 35 balance2 20 20 0 +The Minataur 100 1 5 1 45 9gi Ancient Axe 5 5000 stupidity 2 2 half-freeze 1 1 str 15 20 slow 50 50 crush 30 30 dmg-norm 20 30 dmg% 140 200 0 +Woodland Beast 100 1 2 1 53 7la feral axe 5 5000 dmg% 200 250 dru 2 3 skilltab 15 1 3 swing2 20 20 deadly/lvl 10 crush/lvl 4 res-all 15 25 0 +Werewolf Slayer 100 1 1 1 65 7la Feral Axe 5 5000 cblu cblu dmg%/lvl 16 dmg-min 25 35 dru 1 2 swing3 35 35 move3 40 40 lifesteal 5 8 sock 1 2 oskill Wearwolf 8 20 oskill Shape Shifting 8 20 dmg% 100 125 0 +Shapeshifter 100 1 1 1 69 7ba silver-edged axe 5 5000 invaxe21 dmg% 200 250 swing3 40 40 skill 224 10 15 hp% 40 40 ethereal 1 1 indestruct 1 1 0 +Ethereal Edge 100 1 2 1 62 7ba silver-edged axe 5 5000 whit whit dmg% 150 180 swing2 25 25 abs-fire 10 12 dmg-demon 150 200 demon-heal 5 10 att 270 350 ethereal 1 1 indestruct 1 1 0 +Dyer's Eve 100 1 1 1 73 7bt decapitator 5 5000 invaxe20 dmg% 200 250 dmg-max 50 100 rip 1 1 lifesteal 12 12 bar 2 2 mag% 40 60 regen 12 15 sock 1 1 0 +Hellslayer 100 1 2 1 66 7bt Decapitator 5 5000 dred dred str/lvl 4 vit/lvl 4 dmg%/lvl 24 dmg-fire 150 250 hp 25 25 dmg% 100 100 att-skill 47 10 0 mana -25 -25 0 +Hero's Welcome 100 1 1 1 79 7ga champion axe 5 5000 invaxe14 dmg% 225 270 addxp 3 5 cheap 10 15 swing2 20 20 vit/lvl 5 red-dmg% 10 10 res-all 25 25 nofreeze 1 1 0 +Messerschmidt's Reaver 100 1 2 1 70 7ga Champion Axe 5 5000 blac dmg%/lvl 20 dmg% 200 200 str 15 15 dex 15 15 vit 15 15 enr 15 15 dmg-fire 20 240 dur 25 25 att% 100 100 0 +Nameless Horror 100 1 1 1 84 7gi glorious axe 5 5000 dmg% 275 325 howl 155 155 gethit-skill 130 10 20 charged 88 77 15 res-pois 88 88 res-pois-len 50 75 res-pois-max 5 5 dmg-pois 300 1455 1455 allskills 2 2 0 +Executioner's Justice 100 1 2 1 75 7gi glorious axe 5 5000 blac blac dmg% 240 290 crush 25 25 reduce-ac 33 33 kill-skill Decrepify 50 6 swing2 30 30 allskills 2 2 0 +bl3 0 +Swords , 1-Handed 0 +Epee of Speed 100 1 1 23 24 ssd short sword 5 5000 invswrd15 dmg-norm 25 50 swing2 30 30 balance2 30 30 move2 20 20 charged 139 100 8 block 20 25 0 +Deviljack 100 1 3 1 5 ssd short sword 5 5000 lgry lgry dmg% 100 150 dmg-demon 100 100 addxp 3 5 cheap 10 10 0 +Rixots Keen 0 1 5 1 2 ssd Short Sword 5 5000 blac blac dmg-min 2 2 att% 20 25 light 2 4 crush 25 25 ac 25 25 dmg% 50 100 0 +Anadek's Sword 100 1 1 24 26 scm scimitar 5 5000 dmg-norm 25 50 swing1 15 15 dmg% 50 60 dmg-fire 25 40 res-cold 20 30 rep-dur 25 0 +Briarblade 100 1 3 1 8 scm scimitar 5 5000 dgrn dgrn dmg% 100 150 dmg-pois 250 100 100 openwounds 50 50 heal-kill 3 5 deadly 15 15 0 +Blood Crescent 0 1 5 1 5 scm Scimitar 5 5000 lblu res-all 15 15 dmg% 60 100 hp 15 15 light 4 4 openwounds 33 33 swing2 15 15 lifesteal 15 15 0 +Sajorn Jinx 100 1 1 25 28 sbr saber 5 5000 invswrd23 dmg-norm 25 55 hit-skill 76 4 3 hp 35 50 att 150 150 lifesteal 4 6 regen-mana 3 6 0 +Chromablade 100 1 3 1 10 sbr saber 5 5000 whit whit dmg% 100 150 ease -100 -100 light 7 7 dmg-norm 5 5 gethit-skill 36 20 4 0 +Krintizs Skewer 0 1 5 1 8 sbr Saber 5 5000 cblu ignore-ac 1 1 str 10 10 dex 10 10 manasteal 7 7 dmg-norm 5 12 0 +Qikadar's Laughter 100 1 1 26 30 flc falshion 5 5000 invswrd25 dmg-min 20 25 dmg-max 50 65 howl 33 33 res-all 20 20 allskills 1 1 swing2 20 20 0 +Fiendslayer 100 1 3 1 13 flc falshion 5 5000 oran oran dmg% 120 170 dmg-demon 200 200 att-demon 200 200 demon-heal 5 8 swing2 20 20 res-all 20 20 0 +Gleamscythe 0 1 5 1 10 flc Falchion 5 5000 lred light 3 3 mana 30 30 ac 20 20 swing2 20 20 dmg% 75 115 dmg-cold 150 2 5 0 +Quartz Star 100 1 1 27 31 crs crystal sword 5 5000 dmg-norm 30 65 manasteal 6 6 res-fire 25 25 block2 15 15 dex 20 20 mag% 35 35 0 +Irksome Edge 100 1 3 1 17 crs crystal sword 5 5000 oran oran dmg% 130 180 ease 25 25 hit-skill 42 8 7 manasteal 5 5 knock 1 1 howl 88 88 0 +Azurewrath1 0 1 5 1 13 crs Crystal Sword 5 5000 lgry lgry deadly 50 50 mag% 10 25 dmg-cold 100 3 6 dmg% 100 100 dur 25 75 dmg-mag 5 10 0 +Omni-Slash 100 1 1 28 32 bsd broadsword 5 5000 invswrd11 dmg% 100 140 dmg-norm 15 35 swing3 20 20 sock 2 2 deadly 20 20 thorns 20 20 0 +Vanquisher 100 1 3 1 22 bsd broadsword 5 5000 blac invswrd7 dmg% 135 185 crush 25 25 lifesteal 5 7 swing2 15 15 hp 35 50 oskill sacrifice 1 3 0 +Griswolds Edge 0 1 5 1 17 bsd Broad Sword 5 5000 cred fire-min 3 4 fire-max 7 8 att 100 100 swing1 10 10 knock 1 1 dmg% 80 120 str 12 12 0 +Honor Guard 100 1 1 29 34 lsd long sword 5 5000 whit whit dmg-norm 40 70 block3 30 30 balance3 30 30 swing3 30 30 block 10 20 hp% 15 15 knock 1 1 0 +Winterswipe 100 1 3 1 25 lsd long sword 5 5000 dmg% 150 200 dmg-cold 600 10 20 freeze 1 1 swing2 25 25 aura 114 3 5 res-cold 75 75 0 +Hellplague 0 1 5 1 22 lsd Long Sword 5 5000 dred dred manasteal 5 5 lifesteal 5 5 pois-min 48 48 pois-max 96 96 pois-len 150 150 dmg% 70 100 dmg-fire 25 75 fireskill 2 2 0 +Orc Slayer 100 1 3 1 31 wsd war sword 5 5000 dgrn dgrn dmg% 100 150 dmg-norm 10 20 dmg-demon 100 100 demon-heal 20 20 att 100 100 openwounds 35 50 res-pois-max 15 15 0 +Warrior Untamed 100 1 1 30 36 wsd war sword 5 5000 invswrd16 dmg-norm 40 100 oskill shout 1 1 oskill battle orders 1 1 oskill battle command 1 1 ease 50 50 swing2 40 40 0 +Culwens Point 0 1 5 1 29 wsd War Sword 5 5000 whit whit allskills 1 1 res-pois-len 50 50 balance2 20 20 swing2 20 20 att 60 60 dmg% 70 80 dmg-norm 10 18 0 +Bridge of Pain 100 1 1 55 39 9ss gladius 5 5000 blac blac dmg% 150 220 dmg-norm 20 40 deadly 15 15 crush 10 15 noheal 1 1 openwounds 33 33 sock 1 1 0 +Cursebreaker 100 1 3 1 34 9ss gladius 5 5000 invswrd5 dmg% 180 230 swing2 20 20 gethit-skill 66 8 3 aura 109 12 15 str 15 15 res-fire 20 40 hp 50 50 0 +Bloodletter 100 1 5 1 30 9ss Gladius 5 5000 cred cred dmg-norm 12 45 att 90 90 lifesteal 8 8 stamdrain 10 10 swing2 20 20 dmg% 140 140 skill 127 2 4 skill 151 1 3 dur 30 30 0 +Darkkon 100 1 1 55 41 9sm cutlass 5 5000 dgry dgry dmg% 150 190 dmg-norm 20 40 stupidity 3 3 light -3 -3 dur 55 55 str 20 20 res-ltng 25 35 0 +Deathfoe 100 1 3 1 36 9sm cutlass 5 5000 invswrd10 dmg% 185 235 rip 1 1 addxp 5 5 dmg-undead 250 250 dmg-norm 15 30 dmg-cold 200 55 75 res-cold 35 35 half-freeze 1 1 0 +Coldsteel Eye 100 1 5 1 31 9sm Cutlass 5 5000 lblu lblu stupidity 1 1 slow 30 30 deadly 50 50 dur 50 50 dmg% 200 250 swing2 20 20 manasteal 6 6 0 +Locathah 100 1 1 55 43 9sb shamshir 5 5000 dyel invswrd17 dmg% 150 180 dmg/lvl 8 swing3 40 40 block2 20 20 cast2 20 20 mana 50 60 allskills 1 1 0 +Death of a Thousand Cuts 100 1 3 1 38 9sb shamshir 5 5000 cred cred invsbr dmg% 190 240 wounds/lvl 11 crush/lvl 4 swing3 30 30 oskill 277 6 6 lifesteal 6 9 pierce-ltng 8 12 res-ltng 35 35 0 +Hexfire 100 1 5 1 33 9sb Shamshir 5 5000 charged 62 36 6 ignore-ac 1 1 res-fire 25 25 res-fire-max 10 10 dmg-norm 35 50 dmg% 140 160 fireskill 3 3 0 +Fleshbleeder 100 1 3 1 40 9fc tulwar 5 5000 invswrd24 dmg% 160 200 dmg-norm 20 40 openwounds 100 100 lifesteal 6 8 heal-kill 6 8 regen 8 8 res-cold 30 35 0 +Song of the Damned 100 1 1 55 45 9fc tulwar 5 5000 lgry lgry invflc dmg% 190 240 dmg/lvl 4 oskill battle orders 1 1 oskill revive 1 1 oskill skeleton mastery 5 8 dmg-und/lvl 32 dex 25 25 swing2 25 25 0 +Blade of Ali Baba 100 1 5 1 35 9fc Tulwar 5 5000 cred cred sock 3 gold%/lvl 20 mag%/lvl 8 mana 15 15 dmg% 60 120 dex 5 15 0 +Pseudodragon 100 1 1 55 47 9cr dimensional blade 5 5000 cgrn cgrn dmg% 120 150 dmg-norm 30 60 charged 111 250 12 move2 20 20 vit 20 20 enr 20 20 res-all 20 30 red-dmg% 10 15 0 +Serpent�s Sharp 100 1 3 1 42 9cr dimensional blade 5 5000 oran oran invcrs dmg% 195 245 rep-dur 15 heal-kill 12 15 res-all 25 35 hp 75 75 dmg-pois 200 1677 1677 pierce-pois 25 25 0 +Ginther's Rift 100 1 5 1 37 9cr Dimensional Blade 5 5000 cblu cblu red-mag 7 12 swing2 30 30 rep-dur 20 dur 40 40 dmg-mag 50 120 dmg% 100 150 0 +Grimlock 100 1 1 55 49 9bs battle sword 5 5000 dmg% 190 240 dmg-norm 30 50 swing3 40 40 mag% 40 50 hp 30 50 ignore-ac 1 1 sock 1 3 0 +Dread of the Blade 100 1 3 1 44 9bs battle sword 5 5000 dyel dyel invbsd dmg% 200 250 dmg-max 40 60 swing2 30 30 allskills 1 1 howl 144 144 oskill sword mastery 3 3 block3 40 40 manasteal 7 7 0 +Headstriker 100 1 5 1 39 9bs Battle Sword 5 5000 bwht bwht noheal 1 1 str 15 15 deadly/lvl 12 dmg/lvl 8 dmg% 150 150 0 +Troglodyte 100 1 1 55 52 9ls rune sword 5 5000 invswrd3 dmg% 180 220 dmg-norm 20 40 reduce-ac 25 25 att% 25 25 lifesteal 6 8 manasteal 6 8 hit-skill 38 7 6 0 +Bane of All Gods 100 1 3 1 47 9ls rune sword 5 5000 dgrn dgrn invlsd dmg% 215 250 dmg-norm 15 30 allskills 1 1 all-stats 10 20 addxp 3 5 dmg-demon 100 100 att% 25 25 sock 1 3 0 +Plague Bearer 100 1 5 1 41 9ls Rune Sword 5 5000 hit-skill 92 5 4 dmg-pois 200 384 384 dmg-norm 10 45 dmg% 150 150 res-pois 45 45 skill 238 5 5 0 +Sahuagin 100 1 1 55 54 9wd amcient sword 5 5000 invswrd20 dmg% 200 220 dmg-max 40 60 dmg-pois 250 777 777 hit-skill 91 3 2 hit-skill 84 33 1 red-mag 10 15 abs-fire 10 15 abs-cold 7 10 0 +Dark Descent 100 1 3 1 49 9wd ancient sword 5 5000 blac blac invwsd dmg% 220 265 swing2 20 20 res-pois 35 60 res-ltng 20 40 nofreeze 1 1 oskill slow missiles 3 5 enr 15 15 cast2 20 20 regen 6 6 0 +The Atlantian 100 1 5 1 42 9wd Ancient Sword 5 5000 lblu lblu dur 100 100 ac 75 75 str 16 16 dex 12 12 vit 8 8 dmg% 200 250 pal 2 2 att% 50 50 0 +Celestial Lion 100 1 1 1 60 7ss falcata 5 5000 lgld lgld dmg% 200 300 dmg-elem 200 50 200 res-all 40 60 swing2 25 25 red-dmg% 10 10 addxp 2 3 0 +Rabbit Slayer 100 1 2 1 49 7ss Falcata 5 5000 invswrd14 dmg% 175 250 move2 25 25 all-stats 20 20 swing3 50 50 rep-dur 700 rip 1 1 0 +Badger's Bite 100 1 1 1 64 7sm ataghan 5 5000 invswrd22 dmg% 200 300 swing3 30 30 allskills 2 2 dmg-pois 25 800 800 noheal 1 1 skilltab 17 1 3 lifesteal 5 8 0 +Djinnslayer 100 1 2 1 55 7sm ataghan 5 5000 dpur dpur dmg% 190 240 dmg-fire 250 500 dmg-demon 100 150 att-demon 200 300 abs-ltng 3 7 manasteal 3 6 sock 1 2 0 +Wight's Touch 100 1 1 1 69 7sb elegant blade 5 5000 invswrd21 dmg% 100 150 lifesteal 50 50 dmg-mag 300 500 hit-skill 72 25 8 nec 2 2 oskill sword mastery 10 10 sock 1 1 0 +Bloodmoon 100 1 2 1 56 7sb elegant blade 5 5000 cred cred dmg% 210 260 lifesteal 10 15 charged BloodGolem 9 15 heal-kill 7 13 openwounds 50 50 0 +Megladon's Bite 100 1 1 1 72 7fc hydra edge 5 5000 invswrd18 dmg% 200 300 crush 33 33 deadly 33 33 regen-mana 25 25 reduce-ac 10 20 str/lvl 3 dex/lvl 3 0 +Krakken 100 1 2 1 57 7fc Hydra Edge 5 5000 dblu dblu dmg% 175 230 dmg-norm 20 40 dmg-cold 300 75 100 res-cold 45 55 res-fire 45 55 sock 2 2 heal-kill 3 12 0 +Lightsabre 100 1 2 1 58 7cr Phase Blade 5 5000 lblu lblu light 7 7 att-skill 53 5 0 ignore-ac 1 1 1 abs-ltng% 25 25 swing2 20 20 dmg-mag 60 120 dmg-ltng 1 200 manasteal 5 7 dmg% 150 200 dmg-norm 10 30 0 +Azurewrath 100 1 1 1 73 7cr phase blade 5 5000 lgry invcrs dmg-mag 250 500 dmg% 230 270 aura Sanctuary 10 13 dmg-cold 250 250 500 swing2 30 30 all-stats 10 10 light 3 3 allskills 1 1 0 +Deathfriend 100 1 2 1 70 7bs conquest sword 5 5000 blac invswrd12 dmg% 200 300 rip 1 1 mag% 50 50 gold% 150 150 lifesteal 8 8 enr 15 15 vit 15 15 0 +King's Bounty 100 1 1 1 79 7bs Conquest Sword 5 5000 invswrd4 dmg% 200 300 gold% 100 100 mag% 75 75 swing2 25 25 manasteal 8 8 str 20 20 ease -10 -100 0 +Rhinoceros Strength 100 1 1 1 83 7ls cryptic sword 5 5000 dmg% 250 350 dmg-norm 50 100 knock 6 6 move3 40 40 str/lvl 10 swing2 20 20 0 +Frostwind 100 1 2 1 75 7ls cryptic sword 5 5000 cblu invswrd13 dmg% 180 230 freeze 4 4 half-freeze 1 1 dmg-cold 150 237 486 swing2 25 25 abs-cold% 7 15 oskill Arctic Blast 7 14 0 +Owlbear's Attack 100 1 1 1 87 7wd mythical sword 5 5000 oran oran dmg% 200 300 sock 1 3 dmg-to-mana 15 15 thorns 75 150 hit-skill 48 12 8 slow 20 20 allskills 2 3 ease 15 15 0 +Call Of Heroes 100 1 2 1 80 7wd Mythical Sword 5 5000 invswrd19 dmg% 200 300 block 25 25 dmg-elem 400 75 125 addxp 5 15 half-freeze 1 1 abs-mag% 20 20 regen-stam 300 300 cast2 25 25 oskill Find Item 6 6 0 +bl4 0 +Swords , 2-Handed 0 +Cloaker Beast 100 1 1 20 20 2hs 2-handed sword 5 5000 invswrd1 dmg-norm 35 70 swing1 15 15 oskill cloak of shadows 2 4 stupidity 1 1 balance2 20 20 dex 15 15 0 +Deathlust 100 1 3 1 12 2hs 2-handed sword 5 5000 blac blac dmg% 100 150 deadly 25 25 crush 15 15 swing2 20 20 skill 69 1 2 0 +Shadowfang 0 1 5 1 9 2hs 2-Handed Sword 5 5000 cgrn manasteal 9 9 res-cold 20 20 light -2 -2 dmg-cold 150 2 5 dmg% 70 100 lifesteal 9 9 0 +Blink Dog 100 1 1 23 25 clm claymore 5 5000 dmg-min 30 40 dmg-max 60 80 gethit-skill 54 10 1 res-cold 35 45 skilltab 13 1 2 dur 35 35 0 +Coaldark 100 1 3 1 20 clm claymore 5 5000 lgry dgry dmg% 110 160 stupidity 3 3 skilltab 13 1 3 sock 3 3 0 +Soulflay 0 1 5 1 16 clm Claymore 5 5000 dgrn dgrn manasteal 4 10 lifesteal 4 10 dmg% 60 100 res-all 5 5 swing2 10 10 0 +Ettercap 100 1 1 27 29 gis giant sword 5 5000 invswrd6 dmg-min 35 45 dmg-max 70 90 move2 20 20 res-pois 40 50 att 75 75 heal-kill 8 10 0 +Hellmessenger 100 1 3 1 24 gis giant sword 5 5000 oran oran invgis dmg% 125 175 dmg-demon 100 100 dmg-fire 10 25 res-fire 10 20 abs-fire 4 7 swing2 25 25 0 +Kinemils Awl 0 1 5 1 21 gis Giant Sword 5 5000 dblu dblu att 100 150 mana 20 20 fire-min 3 3 fire-max 10 20 skill 102 6 6 dmg% 65 100 0 +Yuan-Ti's Venom 100 1 1 30 34 bsw bastard sword 5 5000 dyel dyel dmg% 100 120 dmg-norm 25 50 slow 50 50 bloody 1 1 noheal 1 1 res-all 15 20 red-dmg% 5 10 0 +Oldfather 100 1 3 1 27 bsw bastard sword 5 5000 invbsw dmg% 125 175 dmg/lvl 9 all-stats 5 5 hp 25 25 rep-dur 2 dur% 20 20 0 +Blacktongue 0 1 5 1 24 bsw Bastard Sword 5 5000 lgrn dmg-pois 150 192 192 noheal 1 1 att 50 50 res-pois 50 50 hp 25 25 dmg% 65 100 0 +Gharbad's Cry 100 1 1 32 36 flb flamberge 5 5000 oran oran dmg% 70 100 dmg/lvl 8 dmg-min 25 35 hit-skill 81 11 6 charged 28 15 15 manasteal 6 8 regen 2 4 0 +Warrior's Challenge 100 1 3 1 29 flb flamberge 5 5000 invswrd2 dmg% 135 190 skill 137 5 5 skill 146 5 5 lifesteal 5 7 manasteal 5 5 swing2 20 20 0 +Ripsaw 0 1 5 1 26 flb Flamberge 5 5000 invswrd9 openwounds 80 80 dmg-max 7 15 manasteal 6 6 dmg% 110 125 sock 2 2 0 +Demolisher 100 1 1 33 38 gsd great sword 5 5000 dmg-norm 60 120 crush 50 50 swing2 30 30 knock 1 1 allskills 1 1 sock 2 2 0 +Arctic Edge 100 1 3 1 32 gsd great sword 5 5000 cblu cblu invgsd dmg% 165 200 dmg-cold 300 6 10 hit-skill 45 19 7 res-cold 65 65 pierce-cold 15 15 0 +The Patriarch 0 1 5 1 29 gsd Great Sword 5 5000 cred red-dmg 3 10 red-mag 3 10 stupidity 1 1 gold% 100 100 dmg% 90 140 str 10 10 0 +Celestial Tiger 100 1 1 55 48 92h espadon 5 5000 dmg% 175 220 dmg-norm 25 50 res-all 20 30 addxp 2 4 block 25 35 block3 40 40 str 20 20 0 +Sweet Agony 100 1 3 1 38 92h espadon 5 5000 cred cred inv2hs dmg% 190 240 regen -2 -2 heal-kill 5 5 regen-mana 50 50 gethit-skill 42 7 7 att% 35 35 sock 2 2 0 +Crainte Vomir 100 1 5 1 33 92h Espadon 5 5000 lgld slow 35 35 dmg-ac -70 -70 move2 20 20 red-dmg% 10 10 dmg% 160 200 swing3 50 50 0 +Shirotachi 100 1 1 55 52 9cm dacian falx 5 5000 lgld lgld dmg% 170 230 dmg-norm 25 50 lifesteal 7 7 openwounds 33 33 slow 15 25 vit 20 20 cast1 15 15 0 +Sudden Epiphany 100 1 3 1 46 9cm dacian falx 5 5000 oran oran invclm dmg% 195 245 swing3 25 25 enr 25 25 lifesteal 6 8 manasteal 6 8 bar 2 2 skilltab 14 1 3 0 +Bing Sz Wang 100 1 5 1 39 9cm Dacian Falx 5 5000 ease -30 -30 dmg-cold 75 50 140 str 20 20 freeze 2 2 hit-skill 64 5 3 dmg% 130 160 0 +Thunderclap 100 1 1 55 56 9gs tusk sword 5 5000 invswrd8 dmg% 160 200 swing2 20 20 dmg-ltng 75 150 pierce-ltng 10 15 oskill thunder storm 3 5 oskill lightning mastery 3 5 res-ltng 45 65 abs-ltng% 10 20 0 +The Mangler 100 1 3 1 49 9gs tusk sword 5 5000 dgld dgld invgis dmg% 200 250 dmg%/lvl 8 deadly 25 40 crush 25 50 rep-dur 14 dur 50 50 openwounds 100 100 0 +The Vile Husk 100 1 5 1 42 9gs Tusk Sword 5 5000 dgry dgry hit-skill 66 6 1 dmg-und/lvl 60 res-pois 50 50 dmg-pois 150 426 426 dmg% 150 200 att-und/lvl 20 0 +Na-Krul's Power 100 1 1 55 59 9b9 gothic sword 5 5000 lgry lgry dmg% 190 225 dmg-max 45 60 str/lvl 3 heal-kill 20 30 mana-kill 3 5 oskill sword mastery 3 5 cast2 25 25 red-dmg% 10 15 0 +No Quarter Given 100 1 3 1 51 9b9 gothic sword 5 5000 cgrn cgrn invbsw dmg% 210 255 swing3 25 25 knock 3 3 oskill stun 3 3 slow 20 20 move3 30 30 block3 30 30 0 +Cloudcrack 100 1 5 1 45 9b9 Gothic Sword 5 5000 hit-skill 121 6 7 dmg-ltng 1 240 ac 30 30 light 2 2 res-ltng-max 10 10 dmg% 150 200 light-thorns 15 15 skilltab 10 2 2 skilltab 11 2 2 0 +Black Razor 100 1 1 55 61 9fb zweihander 5 5000 blac blac dmg% 200 260 dmg-norm 25 50 deadly 44 44 openwounds 66 66 lifesteal 12 12 att% 30 30 bar 2 2 0 +Demand for Justice 100 1 3 1 54 9fb zweihander 5 5000 whit whit invflb dmg% 215 260 allskills 1 1 addxp 6 10 demon-heal 5 12 kill-skill static Field 22 13 ac 75 75 charged 276 15 30 balance2 20 20 0 +Todesfaelle Flamme 100 1 5 1 46 9fb Zweihander 5 5000 dmg-fire 50 200 res-fire 40 40 abs-fire 10 10 att-skill 47 10 6 dmg% 120 160 charged 52 45 10 charged 51 20 10 0 +Thundercall 100 1 1 55 65 9gd executioner sword 5 5000 dyel dyel dmg% 160 220 swing2 25 25 hit-skill 49 37 3 gethit-skill 53 4 11 res-ltng 35 70 res-fire 35 70 mag% 30 40 gold% 100 150 hp 50 50 0 +The Quick and the Dead 100 1 3 1 56 9gd executioner sword 5 5000 cblu cblu invgsd dmg% 220 260 swing3 50 50 sock 3 6 bar 1 1 oskill sword mastery 1 3 0 +Swordguard 100 1 5 1 48 9gd Executioner Sword 5 5000 bwht bwht ease -50 -50 ac/lvl 40 res-all 10 20 ac-miss 100 100 dmg-to-mana 30 30 dmg% 170 180 ac-hth 200 200 balance2 20 20 block 20 20 0 +Dragon's Breach 100 1 1 1 68 72h legend sword 5 5000 dred dred dmg% 220 300 dmg-fire 200 300 dmg-cold 200 200 300 res-all 20 45 move2 20 20 balance2 20 20 allskills 2 2 0 +Blade Of Mythos 100 1 2 1 54 72h Legend Sword 5 5000 dmg% 200 300 red-dmg% 15 20 res-all 25 35 hp 50 50 swing2 25 25 att% 50 100 noheal 1 1 0 +Blade of Conan 100 1 1 1 73 7cm highland blade 5 5000 blac blac dmg% 200 300 dmg-max 100 200 res-mag 50 50 swing2 30 30 deadly 25 25 bar 2 2 gold% 100 100 0 +Vorpal Blade 100 1 2 1 60 7cm Highland Blade 5 5000 cblu dmg% 225 275 crush 25 50 ignore-ac 1 1 deadly/lvl 13 heal-kill 15 25 oskill whirlwind 1 20 0 +Split Skull 100 1 1 1 77 7gs balrog blade 5 5000 invgis dmg% 200 300 dmg-undead 250 250 mana 75 75 att 300 300 addxp 2 2 vit/lvl 6 sock 1 1 0 +Flamebellow 100 1 2 1 67 7gs balrog blade 5 5000 cred cred invgis dmg% 170 240 dmg-fire 233 482 fireskill 3 3 abs-fire% 20 30 hit-skill Firestorm 12 16 str 10 20 vit 5 10 oskill Inferno 12 18 0 +Deadly Hunter 100 1 1 1 80 7b7 champion sword 5 5000 dmg% 200 300 deadly 75 75 swing2 25 25 manasteal 6 8 lifesteal 8 8 regen-mana 50 50 rep-dur 15 0 +Doombringer 100 1 2 1 69 7b7 Champion Sword 5 5000 dred dred hp% 20 20 dmg% 180 250 att% 40 40 indestruct 1 1 dmg-norm 30 100 hit-skill 72 8 3 lifesteal 5 7 0 +Nightscape 100 1 1 1 84 7fb colossus sword 5 5000 blac blac dmg% 200 300 regen 5 7 ethereal 1 1 rep-dur 5 hp 40 50 mana% 10 10 bar 1 1 skilltab 14 3 3 0 +Holy Avenger 100 1 2 1 75 7fb Colossal Sword 5 5000 whit dmg% 165 200 dmg%/lvl 10 aura 103 12 15 ac 200 500 swing2 20 20 rep-dur 500 death-skill corpse explosion 100 60 kill-skill corpse explosion 5 5 0 +Grandmaster's Glory 100 1 1 1 87 7gd colossus blade 5 5000 dmg% 200 300 dmg/lvl 16 oskill sword mastery 15 20 gold% 300 300 mag% 60 100 allskills 1 2 balance3 20 20 swing3 40 40 0 +The Grandfather 100 1 2 1 81 7gd Colossus Blade 5 5000 lyel lyel str 20 20 dex 20 20 vit 20 20 enr 20 20 att% 50 50 hp 80 80 dmg/lvl 20 indestruct 1 1 dmg% 150 250 0 +bl5 0 +Clubs 0 +Bramble Oak 100 1 1 24 24 clb club 5 5000 dgrn dgrn invclb dmg-min 10 15 dmg-max 40 50 block 10 15 swing1 15 15 allskills 1 1 0 +Quickfeint 100 1 3 1 7 clb club 5 5000 invmace21 dmg% 100 150 dmg-norm 3 6 swing2 20 20 ac 25 50 block 15 20 move1 15 15 0 +Felloak 0 1 5 1 3 clb Club 5 5000 lgry res-ltng 60 60 res-fire 20 20 knock 1 1 fire-min 2 2 fire-max 3 8 dmg% 70 80 0 +Troll's Nail 100 1 1 28 30 spc spiked club 5 5000 lgrn invmace16 dmg-min 15 20 dmg-max 55 65 regen 6 9 regen-mana 60 60 dmg-pois 150 344 344 res-all 20 20 red-dmg 10 15 0 +Oakheart 100 1 3 1 9 spc spiked club 5 5000 dmg% 110 160 red-dmg% 8 12 red-dmg 3 6 red-mag 3 5 hp 20 30 regen 3 5 0 +Stoutnail 0 1 5 1 5 spc Spiked Club 5 5000 dgry thorns 3 10 dmg% 70 100 vit 7 12 red-mag 2 5 0 +Dawn of Skeon 100 1 1 55 55 9cl cudgel 5 5000 dyel dmg-norm 40 120 dmg% 70 100 pal 1 1 dmg-fire 55 120 res-fire 65 65 dmg-undead 150 150 att% 35 35 0 +Gladiator's Strike 100 1 3 1 40 9cl cudgel 5 5000 oran oran invclb dmg% 175 230 swing2 25 25 res-ltng 45 55 hit-skill 48 15 9 dmg-ltng 1 225 dru 2 2 noheal 1 1 0 +Dark Clan Crusher 100 1 5 1 34 9cl Cudgel 5 5000 dru 2 2 dmg-demon 200 200 att-demon 200 200 dmg% 195 195 att% 20 25 demon-heal 15 15 0 +Umbral's Bat 100 1 1 55 59 9sp barbed club 5 5000 invmace20 dmg-norm 50 150 swing3 40 40 reduce-ac 15 15 noheal 66 66 pierce-fire 20 20 abs-fire 7 12 sock 2 2 rep-dur 25 0 +Eater of Souls 100 1 3 1 46 9sp barbed club 5 5000 dred invmace23 dmg% 200 265 swing2 20 20 rip 1 1 oskill mace mastery 3 5 lifesteal 5 9 dmg-norm 20 45 sock 1 1 block 15 25 0 +Fleshrender 100 1 5 1 38 9sp Barbed Club 5 5000 blac openwounds 25 25 noheal 1 1 crush 20 20 deadly 20 20 dmg-norm 35 50 dmg% 130 200 dru 1 1 skilltab 16 2 2 dur 20 20 0 +Stirgi's Club 100 1 1 1 70 7cl truncheon 5 5000 cred cred invclb dmg% 290 350 knock 3 3 oskill bash 1 1 allskills 1 1 ignore-ac 1 1 res-cold 40 40 res-ltng 40 40 hp 75 75 0 +Nord's Tenderizer 100 1 2 1 63 7cl truncheon 3 5000 dmg% 270 330 freeze 2 4 swing2 25 25 charged Blizzard 12 16 att% 150 180 abs-cold% 5 15 dmg-cold 125 205 455 0 +Blightsummoner 100 1 1 1 77 7sp tyrant club 5 5000 dblu dblu invspc dmg-min 60 90 dmg-max 180 235 swing2 20 20 lifesteal 12 12 manasteal 9 9 abs-ltng 15 25 res-ltng 35 35 res-pois 90 90 hit-skill 92 8 6 0 +Demonlimb 100 1 2 1 68 7sp tyrant club 5 5000 dgrn dgrn invmace5 dmg% 180 230 dmg-fire 222 333 lifesteal 7 13 charged Enchant 20 23 rep-dur 5 dmg-demon 123 123 res-fire 15 20 0 +bl6 0 +Hammers , 1-Handed 0 +Gnat Sting 100 1 1 35 35 whm war hammer 5 5000 dgrn dmg-min 20 40 dmg-max 75 100 swing2 20 20 stupidity 1 1 sock 2 4 slow 35 35 str 15 15 0 +Stunhummer 100 1 3 1 32 whm war hammer 5 5000 invmace11 dmg% 125 200 dmg/lvl 8 slow 50 50 swing2 20 20 pal 1 1 oskill mace mastery 1 4 balance2 20 20 0 +Ironstone 0 1 5 1 27 whm War Hammer 5 5000 cblu cblu att 100 150 dmg% 100 135 ltng-min 1 1 ltng-max 10 20 str 10 10 enr 5 5 0 +Hammer of Jholm 100 1 1 55 60 9wh battle hammer 5 5000 oran oran invmace24 allskills 1 1 dmg/lvl 20 dmg-min 40 60 swing3 30 30 lifesteal 6 8 mana-kill 3 5 crush 25 25 0 +Blasthammer 100 1 3 1 50 9wh battle hammer 5 5000 invmace12 dmg% 200 255 swing3 30 30 allskills 1 1 skill 243 3 5 gethit-skill 84 12 7 ignore-ac 1 1 sock 1 2 0 +Earthshaker 100 1 5 1 43 9wh Battle Hammer 5 5000 hit-skill 234 5 7 knock 1 1 swing3 30 30 dmg% 180 180 stupidity 1 1 skilltab 17 3 3 dur 50 50 0 +Schaefer's Hammer 100 1 1 1 79 7wh Legendary Mallet 5 5000 lblu lblu hit-skill 42 20 10 hp 50 50 att/lvl 16 res-ltng 75 75 swing2 20 20 dmg/lvl 16 indestruct 1 1 dmg% 100 130 light 1 1 dmg-ltng 50 200 0 +Stone Crusher 100 1 2 1 68 7wh legendary mallet 5 5000 whit dmg% 280 320 str 20 30 crush 40 40 reduce-ac 25 25 dmg-ac -100 -100 dmg 10 30 0 +bl7 0 +Hammers , 2-Handed 0 +Famorian's Club 100 1 1 31 39 mau maul 5 5000 oran oran invmau dmg-min 60 90 dmg-max 200 240 swing1 15 15 crush 50 50 hit-skill 243 5 7 sock 1 1 0 +Spiritcrusher 100 1 3 1 25 mau maul 5 5000 dmg% 150 200 dmg-norm 25 50 dmg-undead 250 250 rip 1 1 oskill mace mastery 2 4 swing1 15 15 0 +Bonesob 0 1 5 1 20 mau Maul 5 5000 lred dmg% 200 300 crush 40 40 res-fire 30 30 res-cold 30 30 dmg-undead 50 50 0 +Konnan's Maul 100 1 1 40 46 gma great maul 5 5000 dmg-norm 75 300 swing2 25 25 sock 5 5 0 +Vilifier 100 1 3 1 33 gma great maul 5 5000 dgry dgry dmg% 175 200 oskill find item 1 1 rep-dur 4 res-fire 25 25 res-ltng 25 25 mag%/lvl 8 sock 1 2 0 +Steeldriver 0 1 5 1 29 gma Great Maul 5 5000 cgrn cgrn ease -50 -50 swing3 40 40 regen-stam 25 25 dmg% 150 250 0 +Hill Giant's Bludgeon 100 1 1 55 59 9m9 war club 5 5000 blac blac dmg% 80 100 dmg-min 120 150 dmg-max 300 350 hit-skill 139 9 5 dru 2 2 addxp 3 5 ease 25 25 str/lvl 4 0 +Readiness for War 100 1 3 1 48 9m9 war club 5 5000 invmace7 dmg% 200 300 swing2 30 30 oskill mace mastery 10 10 allskills 2 2 crush/lvl 6 addxp 3 5 move3 30 30 0 +Bloodtree Stump 100 1 5 1 40 9m9 War Club 5 5000 dred crush 50 50 dmg% 180 220 res-all 20 20 str 25 25 skilltab 13 2 2 skill 129 3 3 dur 40 40 0 +Doom Avatar 100 1 1 55 65 9gm martel de fer 5 5000 blac invmace25 dmg% 200 300 dmg-max 150 200 swing3 30 30 dex 25 25 allskills 1 1 charged 95 1 50 rep-dur 20 0 +Burning Desire 100 1 3 1 55 9gm martel de fer 5 5000 cred cred dmg% 225 300 gethit-skill 62 3 30 extra-fire 15 25 res-fire 35 50 res-fire-max 10 10 dmg-fire 100 200 dmg-to-mana 10 10 regen-mana 75 75 0 +The Gavel of Pain 100 1 5 1 47 9gm Martel de Fer 5 5000 hit-skill 66 5 1 gethit-skill 76 5 1 thorns 26 26 indestruct 1 1 dmg-norm 12 30 dmg% 130 160 charged 66 3 8 0 +Astral Dreadnought 100 1 1 1 75 7m7 ogre maul 5 5000 dmg% 75 100 dmg-max 400 500 ethereal 1 1 indestruct 1 1 swing3 15 15 move1 15 15 red-dmg% 20 25 res-all 30 30 0 +Windhammer 100 1 2 1 68 7m7 ogre maul 5 5000 invmace4 dmg% 180 230 crush 50 50 swing2 60 60 hit-skill Twister 33 22 0 +The Cranium Basher 100 1 2 1 72 7gm Thunder Maul 5 5000 blac blac swing2 20 20 indestruct 1 1 str 25 25 res-all 25 25 crush 75 75 dmg-norm 20 20 dmg% 200 240 hit-skill 66 4 1 *mana -30 -30 0 +Earthshifter 100 1 1 1 87 7gm thunder maul 3 5000 dmg% 250 300 hit-skill Eruption 25 14 crush 33 33 swing2 10 10 charged Volcano 30 14 skilltab 17 7 7 cast2 10 10 0 +bl8 0 +Maces 0 +Hycandra 100 1 1 25 25 mac mace 5 5000 oran oran invmac dmg-norm 10 40 res-all 20 25 dex 15 15 balance3 40 40 block3 40 40 0 +Lesson in Pain 100 1 3 1 13 mac mace 5 5000 dmg% 100 150 aura thorns 3 5 gethit-skill 66 20 3 dmg-norm 3 6 pal 1 1 0 +Crushflange 0 1 5 1 9 mac Mace 5 5000 blac blac str 15 15 knock 1 1 light 2 2 res-fire 50 50 dmg% 100 140 crush 33 33 0 +Dawn's Mist 100 1 1 30 29 mst morning star 5 5000 invmst dmg-min 10 15 dmg-max 50 60 indestruct 1 1 indestruct 1 1 pal 1 1 sock 2 2 0 +Endless Sleep 100 1 3 1 20 mst morning star 5 5000 cblu cblu invmace1 dmg% 125 175 knock 3 3 stupidity 2 2 hit-skill 72 8 2 res-all 15 25 block 10 15 0 +Bloodrise 0 1 5 1 15 mst Morning Star 5 5000 lblu dmg% 90 120 att% 50 50 openwounds 25 25 light 2 2 swing1 25 25 skill 96 3 5 lifesteal 5 5 0 +Lashfire 100 1 1 32 34 fla flail 5 5000 invmace2 dmg-min 35 45 dmg-max 50 80 dmg-fire 50 60 res-fire 35 45 aura 102 5 7 abs-fire 10 15 swing2 30 30 dex 10 10 0 +Quickening 100 1 3 1 29 fla flail 5 5000 whit whit dmg% 150 200 dmg-min 30 40 swing2 25 25 ignore-ac 1 1 move2 20 20 cast2 20 20 balance2 20 20 0 +The Generals Tan Do Li Ga 0 1 5 1 21 fla Flail 5 5000 dblu dblu dmg-min 1 1 dmg-max 20 20 slow 50 50 ac 25 25 manasteal 5 5 dmg% 50 60 swing2 20 20 dmg-min 10 15 0 +Cat Tail 100 1 1 55 55 9ma flanged mace 5 5000 dyel invmace22 dmg-norm 50 125 stupidity 1 1 deadly 33 33 str 15 15 cast2 25 25 mana 60 60 regen-mana 75 75 0 +Demise of the Weak 100 1 3 1 45 9ma flanged mace 5 5000 invmace6 dmg% 180 230 regen 10 15 block 15 25 block2 20 20 balance3 50 50 att 225 225 addxp 2 5 charged 57 35 15 0 +Sureshrill Frost 100 1 5 1 39 9ma Flanged Mace 5 5000 dmg-cold 125 63 112 nofreeze 1 1 dmg% 150 180 dmg-norm 5 10 freeze 3 3 charged 64 50 9 0 +Comet's Tail 100 1 1 55 60 9mt jagged star 5 5000 oran oran invmace8 dmg% 200 250 dmg-elem 150 1 200 res-all 50 50 heal-kill 7 10 addxp 3 5 ignore-ac 1 1 dmg-to-mana 10 15 0 +Genoa's Trust 100 1 3 1 49 9mt jagged star 5 5000 cred cred invmst dmg% 190 240 res-all 25 50 pierce-ltng 25 25 extra-fire 15 15 freeze 3 3 mana 50 50 cheap 10 15 0 +Moonfall 100 1 5 1 42 9mt Jagged Star 5 5000 hit-skill 56 5 6 dmg-fire 55 115 red-mag 9 12 dmg% 120 150 light 2 2 charged 56 60 11 dmg-norm 10 15 0 +Efreeti's Eye 100 1 1 55 67 9fl knout 5 5000 dmg% 200 250 dmg-norm 25 50 lifesteal 8 8 dmg-min 25 25 sock 3 3 allskills 2 2 swing2 20 20 0 +Frantic Distress 100 1 3 1 53 9fl knout 5 5000 invmace3 dmg% 200 250 howl 90 90 swing3 30 30 move3 40 40 dex 25 25 gethit-skill 58 5 10 sock 2 2 dmg-min 50 60 0 +Baezil's Vortex 100 1 5 1 45 9fl Knout 5 5000 dblu dblu hit-skill 48 5 8 dmg-ltng 1 150 mana 100 100 res-ltng 25 25 dmg% 160 200 swing2 20 20 charged 48 80 15 0 +Star Dust 100 1 1 1 70 7ma reinforced mace 5 5000 invmac dmg% 250 300 hit-skill 154 12 7 all-stats 30 30 extra-fire 15 15 extra-ltng 15 15 extra-cold 15 15 cold-len 500 0 +Fist of Lachdonnan 100 1 1 1 55 7ma Reinforced Mace 5 5000 oran invmace19 dmg% 200 275 enr 25 25 oskill shiver armor 2 5 aura 115 3 5 allskills 1 1 dmg-demon 200 200 pierce-fire 10 20 dmg-fire 75 200 0 +Soulgatherer 100 1 1 1 77 7mt devil star 5 5000 blac invmace18 dmg-norm 50 200 oskill revive 1 1 oskill skeleton mastery 1 1 dmg-undead 350 350 heal-kill 15 15 gethit-skill 74 10 10 enr 35 35 manasteal 8 8 0 +Baranar's Star 100 1 2 1 65 7mt Devil Star 5 5000 lred lred att% 200 200 dmg% 200 200 dex 15 15 str 15 15 swing2 50 50 dur 100 100 dmg-ltng 1 200 dmg-fire 1 200 dmg-cold 1 200 0 +Horizon's Tornado 100 1 2 1 70 7fl scourge 5 5000 dpur dpur invfla dmg% 230 280 swing2 50 50 slow 20 20 hit-skill Tornado 20 15 ease -20 -20 allskills 1 1 0 +Stormlash 100 1 1 1 82 7fl scourge 3 5000 dgry dgry invmace10 dmg% 240 300 swing2 30 30 hit-skill Static Field 15 10 hit-skill Tornado 20 18 dmg-ltng 1 473 light-thorns 30 30 crush 33 33 abs-ltng 3 9 0 +bl9 0 +Scepter 0 +Angelic Sympathy 100 1 1 25 24 scp scepter 5 5000 dmg-min 5 10 dmg-max 25 40 regen 7 7 aura 99 3 5 red-dmg% 15 15 res-all 15 25 0 +Ambercall 100 1 3 1 8 scp scepter 5 5000 cred cred dmg% 100 150 fire-min 6 9 fire-max 15 20 oskill shout 1 1 vit 10 10 dex 10 10 0 +Knell Striker 0 1 5 1 5 scp Scepter 5 5000 dred dred crush 45 45 res-fire 20 20 res-pois 20 20 mana 15 15 att 35 35 dmg-norm 8 15 0 +Avenger's Honor 100 1 1 28 35 gsc grand scepter 5 5000 blac blac dmg-min 15 25 dmg-max 40 55 pal 1 1 lifesteal 6 6 skill-rand 3 96 125 addxp 3 5 sock 1 1 0 +Angel's Grace 100 1 3 1 22 gsc grand scepter 5 5000 whit whit dmg% 130 180 pal 1 1 dmg-undead 150 150 dmg-demon 200 200 swing2 20 20 balance2 15 15 red-dmg% 10 15 0 +Rusthandle 0 1 5 1 17 gsc Grand Scepter 5 5000 lgld lgld pal 1 1 dmg% 100 150 red-mag 1 1 lifesteal 8 8 dmg-undead 50 60 skill 111 1 3 skill 103 3 3 0 +Crusader's Wrath 100 1 1 30 42 wsp war scepter 5 5000 invmace13 dmg% 60 100 dmg-norm 30 60 crush 22 22 str 10 10 manasteal 6 6 skill 112 3 5 noheal 1 1 0 +Dawnskein 100 1 3 1 29 wsp war scepter 5 5000 lgld invmace9 dmg% 150 200 dmg-norm 10 20 allskills 1 1 lifesteal 5 8 light 3 7 mana/lvl 8 hp/lvl 8 0 +Stormeye 0 1 5 1 23 wsp War Scepter 5 5000 cred cred dmg-ltng 1 6 dmg-cold 75 3 5 regen 10 25 dmg% 80 120 skill 110 3 5 skill 118 3 3 skill 112 1 1 0 +Runestar 100 1 1 55 51 9sc rune scepter 5 5000 dmg-norm 50 125 skill-rand 3 96 125 skill-rand 4 96 125 res-mag 10 15 abs-mag% 30 30 res-all 20 20 allskills 1 1 0 +Heavenly Wrath 100 1 3 1 39 9sc rune scepter 5 5000 whit whit dmg% 190 240 skill-rand 3 96 125 skill-rand 2 96 125 skill-rand 2 96 125 dmg/lvl 8 res-fire 75 75 res-cold 35 50 0 +Zakarum's Hand 100 1 5 1 32 9sc Rune Scepter 5 5000 lpur lpur manasteal 8 8 ignore-ac 1 1 regen-mana 10 10 regen-stam 15 15 hit-skill 59 6 5 dmg% 180 220 swing2 30 30 skill 114 2 2 skill 118 2 2 0 +Trianthalon's Sprinkler 100 1 1 55 58 9qs holy water sprinkler 5 5000 cblu cblu dmg-norm 50 150 mag% 40 50 regen-mana 75 75 mana-kill 8 12 manasteal 8 8 heal-kill 20 20 vit 20 20 move2 20 20 0 +Dragon Mephit 100 1 3 1 48 9qs holy water sprinkler 5 5000 cgrn cgrn dmg% 200 250 dmg-fire 100 200 pierce-fire 15 15 swing2 25 25 cast1 15 15 mana 50 50 dex 10 10 mag% 35 35 0 +The Fetid Sprinkler 100 1 5 1 38 9qs Holy Water Sprinkler 5 5000 pal 2 2 hit-skill 87 5 1 hit-skill 81 10 1 dmg-pois 100 409 409 dmg% 160 190 att 150 200 dmg-norm 15 25 0 +Celestial Knight 100 1 1 55 62 9ws divine scepter 5 5000 dgld invmace17 dmg% 100 125 dmg-min 40 60 dmg-max 120 160 allskills 2 2 oskill valkyrie 3 3 swing3 30 30 reduce-ac 20 20 0 +Lord of Riddles 100 1 3 1 54 9ws divine scepter 5 5000 invmace14 dmg% 225 260 dmg-max 25 35 heal-kill 5 8 regen 5 5 skill-rand 4 96 125 skill-rand 3 96 125 allskills 2 2 res-all 20 20 0 +Hand of Blessed Light 100 1 5 1 42 9ws Divine Scepter 5 5000 lyel pal 2 2 dmg% 130 160 att% 100 100 ac 50 50 regen-mana 15 15 light 4 4 dmg-norm 20 45 skill 101 4 4 skill 121 2 2 hit-skill 121 5 4 0 +Heaven's Light 100 1 2 1 61 7sc mighty scepter 5 5000 cblu cblu dmg% 250 300 swing2 20 20 reduce-ac 33 33 light 3 3 demon-heal 15 20 crush 33 33 sock 1 3 pal 2 3 0 +The Reedeemer 100 1 1 1 72 7sc mighty scepter 3 5000 dmg% 250 300 dmg-demon 200 250 pal 2 2 ease -60 -60 skill Redemption 2 4 skill Holy Bolt 2 4 light 3 3 reduce-ac 33 33 dmg 60 120 0 +Celestial Judgment 100 1 1 1 81 7qs seraph rod 5 5000 dmg% 250 325 dmg-ltng 10 500 dmg-fire 300 400 dmg-cold 150 100 200 freeze 3 3 hit-skill 55 9 2 gethit-skill 64 3 7 res-cold 80 80 res-fire 50 80 0 +Wrath Of Heaven 100 1 2 1 77 7qs Seraph Rod 5 5000 whit dmg% 200 300 swing2 20 20 extra-fire 20 20 oskill Fire Wall 8 12 oskill fire mastery 8 12 abs-fire 15 30 res-all 35 50 block 15 20 0 +Wrath of the Seraphim 100 1 1 1 86 7ws caduceus 5 5000 invmace15 dmg% 200 240 dmg/lvl 8 lifesteal 8 8 mana-kill 15 15 slow 90 90 stupidity 4 4 pal 3 3 swing3 40 40 0 +Ironward 100 1 2 1 80 7ws caduceus 3 5000 blac blac dmg% 240 290 slow 25 25 att% 150 200 swing2 10 10 dmg-mag 80 240 red-dmg 4 7 dmg 40 85 skilltab 9 2 4 crush 33 33 0 +b10 0 +Knifes 0 +Vampire's Fang 100 1 1 19 20 dgr dagger 5 5000 whit whit dmg-norm 20 40 lifesteal 6 6 regen 6 6 regen-mana 50 50 res-cold 45 45 0 +Darkflayer 100 1 3 1 6 dgr dagger 5 5000 dmg-norm 7 15 swing1 15 15 allskills 1 1 res-all 10 15 noheal 1 1 0 +Gull 0 1 5 1 4 dgr Dagger 5 5000 lgry lgry dmg-min 1 1 dmg-max 5 10 mag% 100 100 mana -5 -5 0 +Gornnagal's Dirk 100 1 1 23 25 dir dirk 5 5000 cgrn cgrn dmg-norm 25 45 ignore-ac 1 1 res-pois 20 30 dex 15 15 pierce-cold 10 10 gold% 30 50 0 +Prisoner�s Anguish 100 1 3 1 14 dir dirk 5 5000 dmg% 150 200 dmg-max 6 10 deadly 50 50 lifesteal 4 7 skill 66 1 3 skill 72 1 3 skill 73 1 3 0 +The Diggler 0 1 5 1 11 dir Dirk 5 5000 dgry dgry dex 10 10 swing3 30 30 res-cold 25 25 res-fire 25 25 ignore-ac 1 1 dmg% 50 75 0 +Dragon Talon 100 1 1 25 29 kri kris 5 5000 lyel lyel dmg-norm 30 55 crush 15 20 manasteal 5 5 cast2 20 20 swing2 25 25 oskill poison dagger 1 5 sock 1 1 0 +Zenkiller 100 1 3 1 22 kri kris 5 5000 dmg% 175 200 slow 20 20 oskill sacrifice 1 3 sock 2 2 res-fire 20 25 extra-pois 15 15 0 +The Jade Tan Do 0 1 5 1 19 kri Kris 5 5000 oran att 100 150 nofreeze 1 1 dmg-pois 100 220 220 res-pois 95 95 res-pois-max 20 20 dmg% 60 90 0 +Moonsea Razor 100 1 1 35 35 bld blade 5 5000 cblu cblu dmg-norm 35 70 dmg-cold 100 25 50 deadly 20 30 str 25 25 res-all 20 30 charged 42 22 7 mana 40 40 0 +Deathprick 100 1 3 1 29 bld blade 5 5000 invdagr3 dmg-norm 15 30 dmg-pois 100 145 145 deadly 15 25 crush 15 25 block 10 15 block1 15 15 move1 15 15 0 +Irices Shard 0 1 5 1 25 bld Blade 5 5000 dblu dblu cast3 35 35 mana 35 35 att 35 35 res-all 10 10 sor 1 1 0 +Cyan Bloodbane 100 1 1 55 47 9dg piognard 5 5000 cred cred dmg-norm 25 50 dmg/lvl 8 heal-kill 15 15 mana-kill 15 15 allskills 1 1 charged 82 17 15 ac% 10 10 0 +Chimera's Claw 100 1 3 1 37 9dg piognard 5 5000 blac blac invdgr dmg% 170 220 block 20 25 res-fire 25 30 deadly 25 25 dex 15 15 hit-skill 49 5 9 lifesteal 4 7 0 +Spineripper 100 1 5 1 32 9dg Poignard 5 5000 ignore-ac 1 1 lifesteal 8 8 noheal 1 1 swing3 15 15 dex 10 10 dmg% 200 240 dmg-norm 15 27 nec 1 1 0 +Rattlesnake Bite 100 1 1 55 49 9di rondel 5 5000 dmg-norm 25 50 dmg/lvl 10 stupidity 2 2 slow 35 35 dmg-pois 100 466 466 swing3 40 40 res-cold -25 -25 sock 2 2 0 +Sweetwhisper 100 1 3 1 42 9di rondel 5 5000 dyel dyel invdir sor 1 1 dmg% 175 225 heal-kill 5 7 addxp 5 5 cheap 15 15 charged 32 10 10 mag% 25 25 0 +Heart Carver 100 1 5 1 36 9di Rondel 5 5000 deadly 35 35 ignore-ac 1 1 dmg-norm 15 35 dmg% 190 240 skill 131 4 4 skill 142 4 4 skill 150 4 4 0 +Halfling's Blade 100 1 1 55 53 9kr cinquedeas 5 5000 dmg-norm 25 50 dmg/lvl 12 mag% 35 50 gold% 100 100 cast2 20 20 manasteal 6 9 red-dmg% 5 5 dex/lvl 3 0 +Basalisk's Touch 100 1 3 1 46 9kr cinquedeas 5 5000 lgld lgld invkrs dmg% 180 230 slow 25 25 dmg-pois 75 657 657 oskill dim vision 5 7 ac 100 100 ignore-ac 1 1 dmg/lvl 6 0 +Blackbog's Sharp 100 1 5 1 38 9kr Cinquedeas 5 5000 slow 50 50 ac 50 50 dmg-norm 15 45 swing3 30 30 dmg-pois 250 500 500 skill 73 5 5 skill 83 4 4 skill 92 4 4 0 +Thieve's Lockpicker 100 1 1 55 56 9bl stilleto 5 5000 invdagr1 cast2 30 30 mana 50 100 allskills 1 1 move2 25 25 block2 20 20 block 15 25 res-pois-len 90 90 rep-dur 5 0 +Falcon Talon 100 1 3 1 48 9bl stilleto 5 5000 lyel lyel invbld dmg% 190 240 swing3 30 30 move2 30 30 charged teleport 250 1 oskill raven 6 6 deadly/lvl 12 hp 40 70 res-cold 25 50 0 +Stormspike 100 1 5 1 41 9bl Stilleto 5 5000 cblu cblu dmg-ltng 1 120 light-thorns 20 20 gethit-skill 38 25 3 dmg% 150 150 res-ltng/lvl 8 0 +Lich Dagger 100 1 1 1 70 7dg bone knife 5 5000 invdagr2 dmg% 200 245 lifesteal 12 12 extra-fire 15 15 extra-cold 15 20 extra-ltng 10 15 dmg-norm 20 40 cast2 25 25 regen-mana 100 100 0 +Wizardspike 100 1 2 1 55 7dg Bone Knife 5 5000 mana/lvl 16 regen-mana 15 15 mana% 15 15 res-all 75 75 indestruct 1 1 cast3 50 50 0 +Elven Mystral 100 1 1 1 74 7di mithral point 5 5000 cgrn cgrn invdir dmg-norm 50 100 dmg/lvl 6 allskills 2 2 mag% 25 25 sock 2 2 0 +Fall Of Myth Drannor 100 1 2 1 62 7di Mithral Point 5 5000 dmg-min 20 40 dmg-max 75 100 regen -15 -15 mag% 35 35 block 50 75 block3 50 50 cast3 35 35 mana% 25 25 0 +Chaos Wail 100 1 1 1 80 7kr fanged knife 5 5000 oran oran invkrs dmg% 200 300 nec 1 1 oskill grim ward 5 5 howl 155 155 swing2 25 25 skilltab 6 1 1 skill-rand 3 66 95 0 +Fleshripper 100 1 2 1 68 7kr fanged knife 5 5000 dred dred dmg% 200 300 reduce-ac 50 50 noheal 1 1 crush 25 25 openwounds 50 50 deadly 33 33 slow 20 20 0 +Dagger of Kara'Tir 100 1 1 1 85 7bl legend spike 5 5000 invbld oskill find item 10 10 mag% 200 300 sock 3 3 dmg-min 40 60 dmg-max 100 150 swing2 25 25 0 +Ghostflame 100 1 2 1 73 7bl legend spike 5 5000 cblu cblu dmg% 190 240 ignore-ac 1 1 dmg-mag 108 108 manasteal 10 15 ethereal 1 1 indestruct 1 1 light 2 2 0 +b11 0 +Spears 0 +Sleepthorn 100 1 1 20 18 spr spear 5 5000 lgrn lgrn dmg-norm 35 70 stupidity 3 3 slow 80 80 res-pois 30 50 swing1 115 15 dur 60 60 0 +Zealot's Branch 100 1 3 1 9 spr spear 5 5000 dmg% 100 150 swing1 20 20 skill 106 3 3 res-pois 15 15 vit 10 10 str 10 10 0 +The Dragon Chang 0 1 5 1 5 spr Spear 5 5000 dpur dpur att 35 35 dmg-min 10 10 light 2 2 dmg-undead 100 100 dmg-fire 3 6 dmg% 70 100 0 +Mako's Pierce 100 1 1 26 23 tri trident 5 5000 dgry dgry dmg-norm 40 80 openwounds 25 50 demon-heal 15 15 lifesteal 15 15 thorns 15 20 att 100 100 str 10 10 dur 60 60 0 +Deceiver�s Device 100 1 3 1 15 tri trident 5 5000 dmg% 110 165 move2 25 25 cheap 15 15 res-ltng 35 35 lifesteal 5 5 att 225 225 0 +Razortine 0 1 5 1 10 tri Trident 5 5000 oran oran slow 25 25 reduce-ac 50 50 str 15 15 dex 8 8 swing2 30 30 dmg% 60 90 0 +Spear of Hydragoon 100 1 1 27 25 brn brandistock 5 5000 dmg-norm 45 90 swing2 20 20 dur 60 60 skill 143 8 8 str 10 10 dex 10 10 0 +Fangtree 100 1 3 1 17 brn brandistock 5 5000 dgry dgry dmg% 130 180 bloody 1 1 deadly/lvl 16 gethit-skill 81 25 3 sock 2 3 0 +Bloodthief 0 1 5 1 14 brn Brandistock 5 5000 whit whit openwounds 35 35 str 10 10 lifesteal 8 12 hp 26 50 dmg% 80 125 0 +Dragon Turtle 100 1 1 29 32 spt spetum 5 5000 blac blac dmg-norm 50 100 dur 60 60 res-all 20 30 red-dmg% 15 15 bar 1 1 mag% 30 50 swing2 30 30 0 +Ruemonger 100 1 3 1 26 spt spetum 5 5000 dmg% 150 200 dmg-norm 10 20 stupidity 1 1 oskill spear mastery 2 2 allskills 1 1 extra-ltng 15 15 pierce-ltng 20 20 0 +Lance of Yaggai 0 1 5 1 22 spt Spetum 5 5000 lred lred thorns 8 8 ltng-min 1 1 ltng-max 60 60 res-all 15 15 swing2 40 40 dmg-norm 15 40 0 +Footman's Picket 100 1 1 30 38 pik pike 5 5000 dgld dgld dmg-norm 60 120 ethereal 1 1 rep-dur 25 lifesteal 7 7 manasteal 5 7 swing1 20 20 allskills 1 1 balance2 20 20 0 +Woodclaw 100 1 3 1 30 pik pike 5 5000 dmg% 180 200 dmg-norm 10 40 addxp 3 8 swing2 30 30 sock 1 6 0 +The Tannr Gorerod 0 1 5 1 27 pik Pike 5 5000 lgry lgry fire-min 23 23 fire-max 54 54 res-fire-max 15 15 hp 30 30 att 60 60 light 3 3 res-fire 15 15 dmg% 80 100 0 +Old Wolf 100 1 1 55 41 9sr war spear 5 5000 invspea1 dmg% 120 160 dmg-norm 50 100 dur 60 60 addxp 3 3 oskill wearwolf 5 5 oskill shape shifting 3 5 swing3 40 40 lifesteal 5 5 oskill fury 3 5 0 +Mandrake 100 1 3 1 35 9sr war spear 5 5000 blac blac invspr dmg% 180 230 heal-kill 8 8 lifesteal 6 8 hp 65 65 swing2 20 20 addxp 3 5 hit-skill 49 9 5 0 +The Impaler 100 1 5 1 31 9sr War Spear 5 5000 lred lred ignore-ac 1 1 att 150 150 swing2 20 20 openwounds 40 40 noheal 1 1 dmg% 140 170 skill 19 5 5 skill 14 3 3 0 +Flametongue 100 1 1 55 46 9tr fuscina 5 5000 cred cred dmg% 120 160 dmg-norm 50 100 oskill inferno 5 5 extra-fire 15 20 pierce-fire 15 20 dmg-fire 200 300 res-fire 65 65 dur 60 60 0 +Frostband 100 1 3 1 39 9tr fuscina 5 5000 cblu cblu invtri dmg% 185 235 hit-skill 44 4 17 dmg-cold 175 200 250 allskills 1 1 str 15 15 enr 25 25 res-cold 25 25 res-fire 30 50 0 +Kelpie Snare 100 1 5 1 34 9tr Fuscina 5 5000 slow 75 75 res-fire 50 50 hp/lvl 10 dmg-norm 30 50 str 10 10 dmg% 140 180 0 +Dragoon's Shank 100 1 1 55 49 9br war fork 5 5000 dmg% 125 175 dmg-norm 60 120 skill 143 10 15 swing2 20 20 sock 3 5 bar 2 2 dur 60 60 0 +Fear and Loathing 100 1 3 1 44 9br war fork 5 5000 dgld dgld invbrn dmg% 190 240 dmg-norm 40 80 gethit-skill 77 6 4 hit-skill 86 10 3 swing3 30 30 manasteal 5 8 move2 20 20 0 +Soulfeast Tine 100 1 5 1 37 9br War Fork 5 5000 lyel lyel ease -20 -20 lifesteal 7 7 manasteal 7 7 stamdrain 20 20 dmg% 150 190 att 150 250 dur 15 15 0 +Dragon Soul 100 1 1 55 54 9st yari 5 5000 lred lred dmg% 125 175 dmg-norm 70 140 lifesteal 15 15 hp% 20 25 move3 40 40 balance2 25 25 res-all 20 40 dur 60 60 0 +Dreams of Empire 100 1 3 1 47 9st yari 5 5000 oran oran invspt dmg% 205 255 crush 25 25 balance3 40 40 addxp 10 10 hp% 5 15 oskill valkyrie 1 3 mag% 40 50 gold%/lvl 24 0 +Hone Sundan 100 1 5 1 40 9st Yari 5 5000 sock 3 dmg-norm 20 40 crush 45 45 rep-dur 10 dmg% 160 200 0 +Imperial Dragonlance 100 1 1 55 57 9p9 lance 5 5000 whit whit dmg% 150 200 dmg-norm 80 160 swing3 60 60 sock 2 2 allskills 3 3 rep-dur 20 all-stats 10 10 0 +Jouster's Boast 100 1 3 1 50 9p9 lance 5 5000 invpik dmg% 215 260 bar 2 2 swing3 25 25 lifesteal 12 12 oskill spear mastery 3 3 indestruct 1 1 dmg/lvl 16 0 +Spire of Honor 100 1 5 1 42 9p9 Lance 5 5000 lgry lgry att% 25 25 light 3 3 regen 20 20 balance2 20 20 dmg-norm 20 40 dmg-dem/lvl 12 dmg% 150 200 skilltab 9 3 3 ac% 25 25 0 +Ice Mephit 100 1 1 1 60 7sr hyperion spear 5 5000 dyel dyel dmg% 200 270 dur 60 60 dmg-cold 300 400 freeze 3 3 oskill whirlwind 1 1 res-cold 50 50 res-fire 50 50 0 +Arioc's Needle 100 1 2 1 49 7sr hyperion spear 3 5000 dmg% 180 230 dmg-pois 250 403 403 deadly 50 50 ignore-ac 1 1 allskills 2 4 swing2 30 30 0 +Kuo-Toa's Spear 100 1 1 1 69 7tr stygian pike 5 5000 dgrn dgrn dmg% 200 280 dur 60 60 swing2 25 25 dmg-pois 400 8875 8875 howl 150 150 0 +Breath Of Fire 100 1 2 1 58 7tr Stygian Pike 5 5000 dred dred dmg% 200 250 swing3 30 30 oskill inferno 40 40 res-fire 75 75 pierce-fire 30 30 abs-fire% 25 25 0 +Imperial Passion 100 1 1 1 74 7br mancatcher 5 5000 dmg% 250 300 swing3 30 30 addxp 20 20 cheap 5 5 dmg/lvl 24 rep-dur 50 0 +Viperfork 100 1 2 1 64 7br mancatcher 5 5000 dgrn dgrn dmg% 190 240 dmg-pois 250 333 333 swing2 50 50 att 200 250 hit-skill Poison Explosion 15 9 res-pois 30 50 0 +Spirit of Lachdonan 100 1 1 1 79 7st ghost spear 5 5000 dmg% 175 200 ethereal 1 1 fade 1 1 str/lvl 8 indestruct 1 1 dmg%/lvl 16 ease -25 -25 0 +Spirit Light 100 1 2 1 72 7st Ghost Spear 5 5000 cblu cblu ethereal 1 1 indestruct 1 1 dmg% 200 300 ease 25 25 light 7 7 swing3 50 50 lifesteal 12 15 gethit-skill 60 50 12 0 +Prancing Pike 100 1 1 1 86 7p7 war pike 5 5000 oran oran dmg% 150 200 swing3 60 60 balance3 40 40 allskills 3 3 dur 60 60 sock 1 1 heal-kill 35 50 0 +Steelpillar 100 1 2 1 80 7p7 war pike 3 5000 dmg% 210 260 swing2 25 25 reduce-ac 20 20 ac% 50 80 indestruct 1 1 crush 25 25 sock 3 0 +b12 0 +Polearms 0 +Hylocan Axe 100 1 1 19 20 bar bardiche 5 5000 lred lred dmg-min 20 30 dmg-max 50 70 dmg% 50 70 swing2 35 35 manasteal 4 6 dmg-cold 100 25 50 0 +Knave's Ascendence 100 1 3 1 10 bar bardiche 5 5000 dmg% 100 150 ac 25 50 str 15 15 dex 10 10 vit 10 10 hp 35 35 0 +Dimoaks Hew 0 1 5 1 7 bar Bardiche 5 5000 blac blac dex 15 15 dmg% 100 120 swing2 20 20 ac -8 -8 0 +Simpering Edge 100 1 1 27 23 vou voulge 5 5000 dmg-min 25 35 dmg-max 60 80 allskills 1 1 deadly 35 35 lifesteal 4 6 oskill pole arm mastery 2 2 0 +Oreseeker 100 1 3 1 14 vou voulge 5 5000 blac blac dmg% 115 165 gold% 150 150 mag% 25 25 charged 142 100 5 ease -35 -35 cheap 5 5 0 +Steelgoad 0 1 5 1 11 vou Voulge 5 5000 cgrn cgrn howl 96 96 deadly 30 30 att 30 30 res-all 5 15 dmg% 60 80 dur 20 40 0 +Slayer of Fields 100 1 1 30 26 scy scythe 5 5000 dgry dgry dmg-min 30 40 dmg-max 70 80 dru 1 1 ignore-ac 1 1 fireskill 1 1 swing1 20 20 0 +Rebuker 100 1 3 1 19 scy scythe 5 5000 invpole9 dmg% 120 170 knock 2 2 slow 20 20 stupidity 2 2 sock 2 2 0 +Soul Harvest 0 1 5 1 16 scy Scythe 5 5000 dgry openwounds 30 30 att 45 45 res-all 20 20 dmg% 75 90 manasteal 10 10 enr 5 5 0 +Axe of Sytherdan 100 1 1 31 29 pax poleaxe 5 5000 dmg-min 35 45 dmg-max 80 90 res-fire/lvl 5 res-ltng 25 25 att 100 100 rep-dur 20 sock 1 2 0 +Landsplitter 100 1 3 1 24 pax poleaxe 5 5000 dgrn dgrn dmg% 135 185 swing2 30 30 heal-kill 5 8 skill 235 2 4 skill 244 2 4 skilltab 16 1 2 0 +The Battlebranch 0 1 5 1 21 pax Poleaxe 5 5000 lblu lblu swing3 30 30 dex 10 10 dmg% 75 120 att 50 100 lifesteal 7 7 0 +Snowy Sky 100 1 1 32 31 hal halberd 5 5000 whit whit dmg-min 40 50 dmg-max 80 100 dur 40 40 knock 2 2 dmg-cold 200 25 50 mana-kill 3 5 res-pois 30 30 0 +Ripskin 100 1 3 1 28 hal halberd 5 5000 dmg% 150 200 lifesteal 7 7 manasteal 7 7 regen 5 5 regen-mana 25 25 res-cold 15 25 res-ltng 15 25 0 +Woestave 0 1 5 1 25 hal Halberd 5 5000 dblu dblu slow 50 50 openwounds 50 50 stupidity 3 3 dmg-ac -50 -50 freeze 1 1 light -3 -3 noheal 1 1 dmg% 75 110 enr 20 30 0 +Sommerstrike Edge 100 1 1 33 35 wsc war scythe 5 5000 dmg-min 50 50 dmg-max 110 125 dmg-fire 100 150 res-cold 45 45 res-fire 50 70 mag% 40 40 hp 30 50 0 +Slayer�s Debt 100 1 3 1 32 wsc war scythe 5 5000 oran oran dmg% 175 200 dmg-norm 15 30 allskills 1 1 regen -5 -5 ease 20 20 swing1 15 15 oskill corpse explosion 1 4 0 +The Grim Reaper 0 1 5 1 29 wsc War Scythe 5 5000 lpur lpur deadly 100 100 noheal 1 1 manasteal 5 5 dmg% 70 120 dmg-min 15 15 hp% 20 20 0 +Count Kidran's Scythe 100 1 1 55 44 9b7 lochaber axe 5 5000 dmg% 100 150 dmg-norm 25 50 addxp 3 3 res-all 20 40 balance2 20 20 mana 25 25 dex 15 15 0 +Fire Mephit 100 1 3 1 38 9b7 lochaber axe 5 5000 cred cred invbar dmg% 180 230 dmg%/lvl 9 dmg-fire 1 200 gethit-skill 46 12 13 res-fire 35 45 swing2 20 20 allskills 1 1 0 +The Meat Scraper 100 1 5 1 33 9b7 Lochaber Axe 5 5000 dred dred dmg% 150 200 swing2 30 30 lifesteal 10 10 openwounds 50 50 mag% 25 25 skilltab 13 3 3 0 +Griefspawn Touch 100 1 1 55 47 9vo bill 5 5000 dmg% 100 150 dmg-norm 50 125 swing2 30 30 howl 133 133 lifesteal 6 8 regen-mana 25 25 res-ltng 35 35 0 +Sunderblight 100 1 3 1 42 9vo bill 5 5000 lgry lgry invvou dmg% 185 235 dmg-dem/lvl 12 att-demon 200 200 demon-heal 20 25 hp 50 50 res-all 25 35 gethit-skill 87 7 3 0 +Blackleach Blade 100 1 5 1 37 9vo Bill 5 5000 blac blac ease -25 -25 hit-skill 72 5 5 light -2 -2 lifesteal 8 8 dmg/lvl 10 dmg% 100 140 0 +Harbormaster's Victory 100 1 1 55 50 9s8 battle scythe 5 5000 dblu dblu dmg% 100 125 dmg-norm 50 100 dmg/lvl 12 swing3 40 40 allskills 1 1 sock 2 2 0 +Survival Instinct 100 1 3 1 45 9s8 battle scythe 5 5000 dmg% 190 240 red-dmg% 15 25 res-mag 20 25 balance2 30 30 move2 20 20 swing2 25 25 dmg-cold 100 50 80 0 +Athena's Wrath 100 1 5 1 40 9s8 Battle Scythe 5 5000 dru 1 3 dex 15 15 swing2 30 30 hp/lvl 8 dmg/lvl 8 dmg% 150 180 0 +Moonlight Edge 100 1 1 55 53 9pa partizan 5 5000 dyel dyel dmg% 100 150 dmg-norm 50 120 skill 224 3 3 dru 2 2 oskill pole arm mastery 8 8 swing2 25 25 lifesteal 5 8 mana 35 50 0 +Trial by Fire 100 1 3 1 49 9pa partizan 5 5000 dmg% 200 250 dmg-fire 75 150 pierce-fire 15 20 aura 102 4 7 oskill fire wall 5 8 res-fire-max 5 15 res-fire 20 50 sock 2 2 0 +Pierre Tombale Couant 100 1 5 1 43 9pa Partizan 5 5000 lgld lgld deadly 55 55 bar 3 3 balance2 30 30 manasteal 6 6 dmg-norm 12 20 dmg% 160 220 att 100 200 0 +Kritchan's Ire 100 1 3 1 51 9h9 bec-de-corbin 5 5000 oran oran dmg% 100 150 dmg-norm 40 80 hit-skill 230 100 1 allskills 2 2 reduce-ac 25 25 openwounds 33 33 noheal 1 1 0 +Father of Nations 100 1 1 55 57 9h9 bec-de-corbin 5 5000 dgld dgld invhal dmg% 210 260 all-stats 25 25 hp% 15 15 mana% 15 15 lifesteal 8 11 manasteal 6 9 swing3 40 40 dmg-norm 50 100 0 +Husoldal Evo 100 1 5 1 44 9h9 Bec-de-Corbin 5 5000 regen 20 20 att 200 250 noheal 1 1 dmg-norm 20 32 dmg% 160 200 swing2 20 20 0 +Killer's Glee 100 1 1 55 60 9wc grim scythe 5 5000 lred lred dmg% 100 150 dmg-norm 50 100 ethereal 1 1 rep-dur 20 sock 3 5 0 +Dawn of the Dead 100 1 3 1 53 9wc grim scythe 5 5000 dmg% 220 270 swing3 35 35 oskill skeleton mastery 5 8 oskill revive 5 8 dmg-undead 250 250 dmg-cold 200 35 70 slow 20 20 0 +Grim's Burning Dead 100 1 5 1 45 9wc Grim Scythe 5 5000 cred cred dmg-fire 131 232 res-fire 45 45 dmg% 140 180 thorns 8 8 ease -50 -50 nec 3 3 reduce-ac 50 50 ac% 20 20 att 200 250 0 +Ogre Cheiftain's Law 100 1 2 1 53 7o7 ogre axe 5 5000 dmg% 200 250 swing3 40 40 lifesteal 6 8 crush 33 33 slow 20 20 knock 3 3 rip 1 1 0 +Bonehew 100 1 1 1 65 7o7 ogre axe 3 5000 bwht bwht dmg% 270 320 swing2 30 30 charged Corpse Explosion 30 14 hit-skill Bone Spear 50 16 noheal 1 1 sock 2 2 0 +Titan's Reach 100 1 2 1 60 7vo colossus voulge 5 5000 dgld dgld dmg% 200 275 swing1 15 15 oskill pole arm mastery 12 12 bar 3 3 hp/lvl 16 regen 5 8 abs-cold% 20 20 abs-fire% 20 20 0 +Kydra's Judgement 100 1 1 1 70 7vo Colossus Voulge 5 5000 dmg% 200 250 dmg%/lvl 8 hit-skill 45 20 8 res-cold 45 45 dmg-cold 75 125 freeze 1 3 reduce-ac 10 10 0 +Winter Solstice 100 1 1 1 75 7s8 thresher 5 5000 whit whit dmg% 100 140 dmg-min 75 125 dmg-max 300 400 dmg-cold 255 400 freeze 6 6 oskill whirlwind 5 5 res-cold 80 80 0 +The Reaper's Toll 100 1 2 1 68 7s8 thresher 5 5000 invscy dmg% 190 240 hit-skill Decrepify 33 1 ignore-ac 1 1 lifesteal 11 15 ease -25 -25 deadly 33 33 dmg-cold 4 44 0 +Gatecleaver 100 1 1 1 79 7pa cryptic axe 5 5000 dmg% 220 260 dex 35 35 ease -50 -50 allskills 2 2 lifesteal 12 15 addxp 2 4 res-pois 35 55 0 +Tomb Reaver 100 1 2 1 74 7pa cryptic axe 5 5000 lyel lyel swing2 60 60 light 4 4 dmg% 200 280 dmg-undead 150 230 mag% 50 80 res-all 30 50 att-undead 250 350 reanimate 1 10 10 heal-kill 10 14 sock 1 3 0 +Cloud Giant's Axe 100 1 1 1 83 7h7 great poleaxe 5 5000 dyel dyel dmg% 235 300 swing3 60 60 ease 25 25 str 75 75 heal-kill 15 20 dmg-ltng 1 500 0 +Dreadfear 100 1 2 1 77 7h7 Great Poleaxe 5 5000 dmg% 300 450 sock 3 6 howl 100 100 vit 100 0 +Kraken's Backlash 100 1 1 1 86 7wc giant thresher 5 5000 dmg% 200 240 dmg-norm 100 200 knock 6 6 swing2 20 20 dmg-cold 100 200 hit-skill 243 8 10 sock 2 2 0 +Stormspire 100 1 2 1 81 7wc Giant Thresher 5 5000 dblu dblu res-ltng 50 50 gethit-skill 53 5 5 dmg% 150 250 str 10 10 gethit-skill 38 2 0 swing2 30 30 indestruct 1 1 dmg-ltng 1 237 light-thorns 27 27 dmg-norm 50 100 0 +b13 0 +Staves 0 +Invoker 100 1 1 24 24 sst short staff 5 5000 invsst dmg-min 10 15 dmg-max 40 60 swing2 20 20 cast3 50 50 mana% 15 15 sock 2 2 0 +Puzzler�s Mystery 100 1 3 1 6 sst short staff 5 5000 cblu cblu dmg-norm 5 10 skill-rand 2 36 44 skill-rand 2 36 44 mana 25 25 swing2 20 20 0 +Bane Ash 0 1 5 1 3 sst Short Staff 5 5000 lgrn lgrn fire-min 4 4 fire-max 6 6 res-fire 50 50 mana 30 30 swing2 20 20 dmg% 50 60 skill 36 5 5 skill 37 2 2 0 +Touch of Evil 100 1 1 29 29 lst long staff 5 5000 blac blac invlst dmg-min 20 30 dmg-max 60 90 hit-skill 72 8 2 dru 1 1 balance2 20 20 mana 45 45 skill-rand 4 221 250 0 +Puppeteer's Staff 100 1 3 1 12 lst long staff 5 5000 invstaf3 dmg% 110 150 skill-rand 1 36 50 skill-rand 2 36 50 skill-rand 2 36 50 fireskill 1 1 cast2 20 20 oskill clay golem 5 7 oskill summon spirit wolf 3 3 0 +Serpent Lord1 0 1 5 1 9 lst Long Staff 5 5000 cgrn cgrn dmg-pois 75 40 40 res-pois 50 50 light -1 -1 mana 10 10 dmg% 60 90 manasteal 100 100 reduce-ac 50 50 0 +Wizard's Rule 100 1 1 30 32 cst gnarled staff 5 5000 invcst skill-rand 3 36 65 skill-rand 3 36 65 skill-rand 2 36 65 skill-rand 2 36 65 mana-kill 3 6 cast2 20 20 res-all 20 30 0 +Sage's Retort 100 1 3 1 17 cst gnarled staff 5 5000 oran oran invstaf9 dmg% 125 175 gethit-skill 44 50 2 gethit-skill 42 3 7 regen-mana 75 75 mana% 15 15 kill-skill 6 6 0 +Lazarus Spire 0 1 5 1 13 cst Gnarled Staff 5 5000 lgry res-ltng 75 75 red-dmg 5 5 enr 15 15 skill 53 1 1 skill 49 2 2 skill 42 3 3 regen-mana 43 43 dmg-ltng 1 28 sor 1 1 0 +Staff of the Arch-Magus 100 1 1 31 36 bst battle staff 5 5000 cred cred invbst allskills 2 2 mana/lvl 10 str 25 25 dex 25 25 cast3 30 30 skilltab 4 2 2 rip 1 1 addxp 3 5 0 +Strange Alchemy 100 1 3 1 23 bst battle staff 5 5000 oskill warmth 1 3 oskill frozen armor 1 3 oskill bone armor 1 3 oskill cyclone armor 1 3 dmg-norm 15 30 swing2 20 20 dmg% 100 100 0 +The Salamander 0 1 5 1 19 bst Battle Staff 5 5000 dred dred dmg-fire 15 32 res-fire 30 30 skill 51 1 1 skill 47 2 2 skill 37 3 3 fireskill 2 2 0 +Warmth of Ash 100 1 1 32 40 wst war staff 5 5000 dgry invstaf12 fireskill 5 5 dmg-min 15 25 dmg-max 60 80 dmg-fire 75 100 res-fire 65 65 abs-cold 10 20 skill-rand 2 36 65 0 +Riddlesolver 100 1 3 1 29 wst war staff 5 5000 invstaf2 dmg% 150 200 skill-rand 2 36 65 skill-rand 3 36 65 skill-rand 3 36 65 sor 1 1 cast2 25 25 balance2 30 30 0 +The Iron Jang Bong 0 1 5 1 25 wst War Staff 5 5000 dyel dyel ac 30 30 cast3 20 20 dmg% 100 100 att% 50 50 skill 48 2 2 skill 46 2 2 skill 44 3 3 sor 2 2 0 +Arctic Frost 100 1 1 55 45 8ss jo staff 5 5000 lblu lblu invstaf10 skilltab 5 4 4 extra-cold 25 25 abs-cold% 25 25 pierce-cold 35 35 res-fire 80 100 0 +Staff of Shadows 100 1 3 1 36 8ss jo staff 5 5000 blac blac invsst skill-rand 3 36 65 skill-rand 3 36 65 skill-rand 3 36 65 cast2 20 20 gethit-skill 71 11 2 red-dmg 8 12 stam 75 75 allskills 1 1 0 +Razorswitch 100 1 5 1 28 8ss Jo Staff 5 5000 lpur cast3 30 30 thorns 15 15 mana 175 175 hp 80 80 red-mag 15 15 res-all 50 50 allskills 1 1 0 +Ter'Angreal 100 1 1 55 50 8ls quarterstaff 5 5000 whit whit sor 2 2 cast3 40 40 mana% 50 50 addxp 5 5 mana-kill 20 20 dmg-to-mana 15 25 0 +Silence of the Sphinx 100 1 3 1 39 8ls quarterstaff 5 5000 invstaf4 skill-rand 2 36 65 skill-rand 2 36 65 skill-rand 2 36 65 sor 2 2 dmg-to-mana 15 15 addxp 3 5 cheap 10 15 mag% 60 60 sock 2 2 0 +Ribcracker 100 1 5 1 31 8ls Quarterstaff 5 5000 lblu lblu dmg-norm 30 65 dmg% 200 300 crush 50 50 dex 15 15 ac 100 100 ac% 100 100 balance2 50 50 swing2 50 50 dur 100 100 0 +Staff of Valere 100 1 1 55 56 8cs cedar staff 5 5000 invstaf5 dmg-min 30 50 dmg-max 100 150 swing3 45 45 red-dmg% 15 20 str 15 15 dex 25 25 dru 1 1 cast2 15 15 0 +Rubydawn 100 1 3 1 42 8cs cedar staff 5 5000 cred cred invcst skill-rand 4 66 95 skill-rand 3 66 95 skill-rand 2 66 95 nec 2 2 cast2 20 20 mana% 15 15 oskill fire mastery 3 6 oskill 47 6 8 0 +Chromatic Ire 100 1 5 1 35 8cs Cedar Staff 5 5000 sor 3 3 res-all 20 40 cast1 20 20 light-thorns 20 20 hp% 20 25 skill 61 1 1 skill 63 1 1 skill 65 1 1 0 +Knight's Prophet 100 1 1 55 60 8bs gothic staff 5 5000 invstaf7 dmg-min 35 70 dmg-max 150 250 pal 3 3 res-all 40 50 move2 25 25 swing2 25 25 manasteal 4 7 lifesteal 7 7 oskill whirlwind 3 3 0 +Grace of Isis 100 1 3 1 47 8bs gothic staff 5 5000 dgld dgld invbst dmg% 200 265 dru 1 2 skill-rand 3 221 250 skill-rand 2 221 250 skill-rand 3 221 250 dmg-norm 30 60 swing3 40 40 ignore-ac 1 1 crush 20 20 0 +Warpspear 100 1 5 1 39 8bs Gothic Staff 5 5000 cblu cblu sor 3 3 ignore-ac 1 1 skill 54 3 3 skill 43 3 3 skill 58 3 3 ac-miss 250 250 hit-skill 54 50 1 gethit-skill 54 25 1 0 +Arcane Protection 100 1 1 55 65 8ws rune staff 5 5000 dgrn dgrn sor 2 2 res-all 35 50 red-mag 25 25 gethit-skill 40 13 4 charged 58 27 9 dmg-to-mana 50 50 regen-mana 200 200 0 +Survivor's Sonata 100 1 3 1 50 8ws rune staff 5 5000 invstaf13 skill 48 3 5 skill 60 3 5 hp 75 75 cast2 25 25 allskills 2 2 addxp 5 5 gold% 200 200 mag%/lvl 10 0 +Skullcollector 100 1 5 1 41 8ws Rune Staff 5 5000 invstaf1 mana% 20 20 mana-kill 20 20 allskills 2 2 mag%/lvl 8 0 +Road to Perdition 100 1 1 1 67 6ss walking stick 5 5000 lgry lgry allskills 3 3 mag% 200 300 cast2 40 40 mana/lvl 16 0 +Jadrik's Torment 100 1 2 1 49 6ss Walking Stick 5 5000 invstaf8 enr 35 35 cast3 40 40 move3 50 50 dmg% 200 300 swing3 55 55 str -50 -50 hp 200 200 0 +Ogden's Wisdom 100 1 1 1 69 6ls stalagmite 5 5000 invstaf6 allskills 2 2 cast2 20 20 enr 35 35 addxp 3 5 heal-kill 15 15 cheap 10 10 skill-rand 5 36 65 skill-rand 5 36 65 skill-rand 5 36 65 0 +Gorgon Strength 100 1 2 1 55 6ls Stalagmite 5 5000 oran oran str 100 100 mana 150 150 sor 3 3 mag%/lvl 8 res-all 25 25 ac/lvl 40 0 +Light of Ra 100 1 1 1 73 6cs elder staff 5 5000 whit whit invcst skilltab 4 5 5 skill 57 3 5 skill 63 3 5 light 5 5 thorns/lvl 10 sock 3 3 0 +Ondal's Wisdom 100 1 2 1 66 6cs elder staff 3 5000 cast2 45 45 enr 40 50 allskills 2 4 ac 450 550 addxp 5 5 red-mag 5 8 0 +Summoner�s Risk 100 1 1 1 79 6bs shillelah 5 5000 invbst oskill bloodgolem 6 8 oskill golem mastery 20 20 dmg-to-mana 35 35 cast2 30 30 res-all 100 100 balance3 40 40 regen 3 5 0 +Sanctuary 100 1 2 1 76 6bs Shillelah 5 5000 cred cast2 20 20 mana 220 220 ac 350 550 red-dmg% 50 50 sor 2 2 mana% 25 25 0 +Everkeeper 100 1 1 1 85 6ws archon staff 5 5000 invstaf11 allskills 1 1 dmg-min 150 200 dmg-max 350 400 rep-dur 25 ignore-ac 1 1 swing3 40 40 regen 20 20 dmg% 300 340 0 +Mang Song's Lesson 100 1 2 1 82 6ws archon staff 5 5000 dgld dgld inv8wsu allskills 5 5 pierce-fire 7 15 pierce-ltng 7 15 pierce-cold 7 15 regen-mana 100 100 cast2 30 30 0 +b14 0 +Wands 0 +Acolyte's Wand 100 1 1 24 25 wnd wand 5 5000 lblu lblu invwnd nec 1 1 cast2 20 20 mana 44 44 regen-mana 50 50 skill-rand 2 66 90 skill-rand 2 66 90 0 +Bitter Sorrow 100 1 3 1 9 wnd wand 5 5000 invwand1 dmg-min 2 7 dmg-max 12 20 skill-rand 3 66 75 skill-rand 3 66 75 sock 2 2 0 +Iros Torch 0 1 5 1 5 wnd Wand 5 5000 cred nec 1 1 lifesteal 6 6 dmg-fire 5 9 light 3 3 enr 10 10 regen-mana 5 5 0 +Ladrina's Enchantment 100 1 1 30 30 ywn yew wand 5 5000 dgld dgld invywn charged 66 200 12 charged 82 33 7 charged 42 19 20 charged 57 7 27 charged 17 112 29 charged 154 61 15 charged 235 17 20 0 +Ire of Astaroth 100 1 3 1 19 ywn yew wand 5 5000 skill-rand 1 66 80 skill-rand 2 66 80 skill-rand 2 66 80 skill-rand 3 66 80 nec 1 1 mana 40 40 cast2 25 25 0 +Maelstromwrath 0 1 5 1 14 ywn Yew Wand 5 5000 dblu dblu ltng-min 1 1 ltng-max 9 9 res-ltng 40 40 mana 30 30 cast2 30 30 skill 74 1 3 skill 77 1 3 skill 66 1 3 skill 76 1 3 0 +Skeleton's Claw 100 1 1 34 33 bwn bone wand 5 5000 whit whit invbwn dmg-min 10 20 dmg-max 40 60 lifesteal 10 15 swing2 20 20 skill 69 3 5 hp 50 75 mag% 25 30 allskills 1 1 0 +Child's Laughter 100 1 3 1 26 bwn bone wand 5 5000 skill 72 3 5 skill 77 3 5 skill 81 3 5 levelup-skill nova 100 40 mag% 50 50 addxp 4 4 extra-pois 25 25 0 +Gravenspine 0 1 5 1 20 bwn Bone Wand 5 5000 cgrn str 10 10 dex 10 10 cold-min 4 4 cold-max 8 8 cold-len 75 75 manasteal 5 5 nec 2 2 mana 25 50 0 +Dracolich Fang 100 1 1 37 37 gwn grim wand 5 5000 cgrn cgrn invgwn nec 1 1 lifesteal 100 100 manasteal 100 100 regen 6 10 regen-mana 35 80 red-dmg% 10 10 nofreeze 1 1 res-pois-len 80 90 0 +Scream of Despair 100 1 3 1 32 gwn grim wand 5 5000 nec 2 2 skill-rand 3 66 95 skill-rand 5 66 95 gethit-skill 77 25 8 mana 20 20 hp 50 50 str 20 20 0 +Umes Lament 0 1 5 1 28 gwn Grim Wand 5 5000 lblu lblu nec 2 2 mana 40 40 cast2 20 20 howl 64 64 skill 77 3 3 skill 87 2 2 0 +Epoch's End 100 1 1 55 43 9wn burnt wand 5 5000 invwand2 allskills 1 1 mana/lvl 8 hp/lvl 8 extra-pois 20 30 res-pois 45 65 res-pois-len 50 50 oskill 94 3 5 0 +Apoclypse Fire 100 1 3 1 39 9wn burnt wand 5 5000 dred dred invwnd charged 62 100 30 charged 51 100 30 charged 225 100 30 charged 244 100 30 addxp 10 10 res-all 65 65 cast2 25 25 allskills 1 1 0 +Suicide Branch 100 1 5 1 33 9wn Burnt Wand 5 5000 thorns 25 25 cast2 50 50 res-all 10 10 mana% 10 10 hp 40 40 allskills 1 1 0 +Medusa's Gaze 100 1 1 55 46 9yw petrified wand 5 5000 blac blac pal 2 2 res-all 25 25 cast2 25 25 mana 50 50 regen-mana 75 75 regen 2 6 sock 1 1 0 +Huclavee's Flinch 100 1 3 1 42 9yw petrified wand 5 5000 invywn dmg-norm 40 80 dmg% 175 200 swing3 35 35 knock 3 3 slow 25 25 oskill zeal 8 8 lifesteal 8 8 0 +Carin Shard 100 1 5 1 35 9yw Petrified Wand 5 5000 cblu cblu hp/lvl 10 cast2 10 10 mana/lvl 10 balance2 30 30 regen 5 5 nec 1 1 skilltab 8 2 2 0 +Asylum's Ward 100 1 1 55 49 9bw tomb wand 5 5000 nec 2 2 skill-rand 2 66 95 skill-rand 2 66 95 skill-rand 3 66 95 charged 72 42 8 gethit-skill 81 6 4 red-mag 10 15 0 +Wand of Fireballs 100 1 3 1 45 9bw tomb wand 5 5000 oran oran invbwn gethit-skill 47 100 15 all-stats 20 20 skill-rand 3 66 95 skill-rand 3 66 95 allskills 1 1 cast2 25 25 sock 1 1 0 +Arm of King Leoric 100 1 5 1 36 9bw Tomb Wand 5 5000 skilltab 8 2 2 gethit-skill 93 5 10 mana/lvl 10 gethit-skill 88 10 2 cast1 10 10 skilltab 7 2 2 skill 77 2 2 skill 80 2 2 skill 69 3 3 skill 70 3 3 0 +Nullwand 100 1 1 55 53 9gw grave wand 5 5000 allskills 2 2 res-all 40 60 red-dmg% 20 25 abs-fire 10 10 abs-ltng 10 10 abs-cold 10 10 nofreeze 1 1 res-pois-len 70 70 0 +Gravedancer's Union 100 1 3 1 49 9gw grave wand 5 5000 dgry dgry invgwn skill-rand 1 66 95 skill-rand 2 66 95 skill-rand 3 66 95 skill 69 2 5 mana 100 100 res-cold 25 40 res-pois 50 50 heal-kill 10 15 0 +Blackhand Key 100 1 5 1 41 9gw Grave Wand 5 5000 blac blac skilltab 6 1 1 dmg-to-mana 20 20 hp 50 50 light -2 -2 cast3 30 30 res-fire 37 37 nec 2 2 charged 150 30 13 0 +Voice of Reason 100 1 1 1 68 7wn polished wand 5 5000 invwand3 enr 30 30 addxp 3 5 cheap 15 15 cast2 25 25 skill-rand 3 66 95 skill-rand 3 66 95 skilltab 6 2 3 0 +Darkmantle 100 1 2 1 50 7wn Polished Wand 5 5000 blac blac nec 1 3 oskill dim vision 5 5 mana 50 50 skill-rand 7 66 95 rip 1 1 cast3 40 40 regen 35 35 ac/lvl 24 0 +Chorus of the Cursed 100 1 1 1 73 7yw ghost wand 5 5000 dpur dpur invywn allskills 2 2 skilltab 6 2 2 res-mag 20 20 gethit-skill 91 3 18 dex 20 20 str 20 20 oskill revive 1 1 0 +Specral Immage 100 1 2 1 61 7yw Ghost Wand 5 5000 allskills 1 1 res-all 25 35 addxp 5 10 abs-cold% 15 20 abs-ltng% 15 20 abs-fire% 15 20 skilltab 8 2 5 0 +Vengeance of the Wronged 100 1 1 1 79 7bw lich wand 5 5000 invbwn nec 3 3 skill-rand 10 66 95 death-skill 92 100 50 mana 75 100 hp 75 100 mana-kill 3 5 addxp 3 5 0 +Boneshade 100 1 2 1 70 7bw lich wand 3 5000 dgry dgry nec 2 2 cast2 25 25 skill Teeth 4 5 skill Bone Armor 4 5 skill Bone Spear 2 3 skill Bone Spirit 1 2 skill Bone Wall 2 3 0 +Pull of Darkness 100 1 1 1 86 7gw unearthed wand 5 5000 invgwn allskills 2 2 gold% 125 125 mag% 60 60 skill 93 7 15 move3 40 40 block 25 25 block2 20 20 0 +Deaths's Web 100 1 2 1 80 7gw unearthed wand 5 5000 bwht bwht allskills 2 2 pierce-pois 40 50 heal-kill 7 12 mana-kill 7 12 skilltab 7 1 2 0 +b15 0 +Bows 0 +Freedom's Flight 100 1 1 24 25 sbw short bow 5 5000 dmg-norm 20 40 swing1 20 20 skilltab 0 1 3 att% 50 50 skill 6 10 10 0 +Violetwing 100 1 3 1 7 sbw short bow 5 5000 dpur dpur invsbw dmg-norm 4 8 swing1 15 15 skill 6 3 3 move2 20 20 0 +Pluckeye 0 1 5 1 4 sbw Short Bow 5 5000 cblu cblu att 28 28 dmg% 100 100 hp 10 10 light 2 2 manasteal 3 3 mana-kill 2 2 0 +Target's 100 1 1 25 26 hbw hunter's bow 5 5000 invbow10 dmg-norm 23 45 balance1 20 20 dmg-ltng 1 100 lifesteal 4 6 vit 15 15 oskill 6 1 3 0 +Carrion Wing 100 1 3 1 10 hbw hunter's bow 5 5000 dgry dgry invhbw dmg% 100 150 dmg-max 6 10 dmg-undead 200 200 extra-fire 20 20 sock 1 2 0 +Witherstring 0 1 5 1 7 hbw Hunter�s Bow 5 5000 lred lred swing3 30 30 dmg-min 1 3 dmg-max 6 10 att 50 50 magicarrow 3 3 dmg% 40 50 0 +Elven Bow of Duadon 100 1 1 26 27 lbw long bow 5 5000 dgrn dgrn dmg-norm 25 50 swing2 20 20 allskills 1 1 manasteal 4 6 regen 1 2 balance2 20 20 0 +Gale Song 100 1 3 1 16 lbw long bow 5 5000 invlbw dmg% 100 150 dmg-norm 5 12 ama 1 1 howl 144 144 dex 20 20 ease -25 -25 0 +Rimeraven 0 1 5 1 12 lbw Long Bow 5 5000 dred dred att% 50 50 dex 3 3 explosivearrow 3 3 str 3 3 dmg% 60 100 0 +Silver Oak Bow 100 1 1 27 28 cbw composite bow 5 5000 whit invbow6 dmg-norm 32 55 att 200 200 dmg-undead 200 200 dmg-demon 100 100 regen-mana 50 50 str 15 15 ease -15 -15 0 +Warpwind 100 1 3 1 21 cbw composite bow 5 5000 invcbw dmg% 125 175 dmg-norm 3 15 swing2 25 25 knock 1 1 skill 12 6 6 mana 35 35 0 +Piercerib 0 1 5 1 16 cbw Composite Bow 5 5000 cred cred res-all 10 10 deadly 30 30 att 60 60 dmg-undead 100 100 dmg% 75 100 swing2 50 50 0 +Shayira's Flight 100 1 1 28 29 sbb short battle bow 5 5000 oran oran dmg-fire 80 100 dmg-ltng 10 200 dmg-cold 100 40 50 dmg% 100 130 dmg-norm 10 20 all-stats 5 5 0 +Willowsting 100 1 3 1 22 sbb short battle bow 5 5000 invsbb dmg% 125 175 gethit-skill 67 12 7 ease -25 -25 balance2 20 20 gold% 50 100 lifesteal 6 6 0 +Pullspite 0 1 5 1 19 sbb Short Battle Bow 5 5000 lgrn lgrn dmg-ltng 1 30 str 8 8 att 28 28 pierce 25 25 res-ltng 25 25 dmg% 70 90 0 +Kirre Strike 100 1 1 29 32 lbb long battle bow 5 5000 dgld dgld dmg-norm 35 60 oskill strafe 3 3 res-fire 15 20 res-pois 40 50 knock 1 1 pierce 20 20 0 +Beeswarm 100 1 3 1 26 lbb long battle bow 5 5000 invlbb dmg% 140 190 dmg-norm 10 20 oskill multiple shot 6 6 swing2 25 25 allskills 1 1 0 +Wizendraw 0 1 5 1 23 lbb Long Battle Bow 5 5000 dgrn dgrn magicarrow 5 5 mana 30 30 swing2 20 20 res-cold 26 26 att 50 100 dmg% 70 80 enr 15 15 pierce-cold 20 35 0 +Sylvan Battle Bow 100 1 1 30 35 swb short war bow 5 5000 blac invbow1 dmg-norm 35 70 swing3 30 30 hp% 10 10 mana% 20 20 oskill ice arrow 2 2 ama 1 1 skilltab 0 1 2 0 +Ranger's Sting 100 1 3 1 29 swb short war bow 5 5000 invbow5 dmg-min 5 15 dmg-max 30 45 bar 2 2 swing2 20 20 oskill strafe 6 6 dex 35 35 manasteal 6 6 0 +Hellclap 0 1 5 1 26 swb Short War Bow 5 5000 dred dred swing1 10 10 fire-min 15 15 fire-max 30 50 att 50 75 res-fire 40 40 dex 12 12 dmg% 70 90 fireskill 1 1 0 +Telena's War Bow 100 1 1 31 37 lwb long war bow 5 5000 cblu cblu dmg-norm 40 80 swing2 25 25 lifesteal 5 7 mana-kill 7 7 addxp 2 2 dex 15 15 crush 10 15 0 +Finalflight 100 1 3 1 31 lwb long war bow 5 4000 invlwb dmg-min 10 20 dmg-max 40 60 dmg% 100 125 rip 1 1 addxp 5 5 gethit-skill 54 10 1 ac 100 100 0 +Blastbark 0 1 5 1 28 lwb Long War Bow 5 5000 lyel lyel dmg% 70 130 str 5 5 ama 1 1 manasteal 3 6 skill 16 2 2 0 +Teldicia's Sting 100 1 1 55 44 8sb edge bow 5 5000 invbow8 dmg% 80 125 dmg-norm 25 50 swing2 30 30 dmg-pois 75 333 333 dex 10 10 hp 50 50 gethit-skill 22 7 1 0 +Dead to the World 100 1 3 1 35 8sb edge bow 5 5000 blac blac invsbw dmg% 175 225 swing2 25 25 regen 5 5 oskill guided arrow 4 4 charged 95 8 35 pierce 25 25 lifesteal 6 6 0 +Skystrike 100 1 5 1 29 8sb Edge Bow 5 5000 dmg-ltng 1 250 att 100 100 enr 10 10 swing3 30 30 dmg% 150 200 hit-skill 56 2 6 ama 1 1 0 +Sadira 100 1 1 55 46 8hb razor bow 5 5000 invbow11 dmg% 80 125 dmg-norm 25 50 dmg-ltng 25 100 res-all 20 20 mana-kill 5 5 regen-mana 75 75 sock 1 1 0 +Iceweaver 100 1 3 1 37 8hb razor bow 5 5000 whit whit invhbw dmg% 185 235 oskill ice arrow 5 5 charged 60 3 10 gethit-skill 44 8 4 dmg-cold 300 40 80 extra-cold 15 15 dmg-norm 20 40 0 +Riphook 100 1 5 1 31 8hb Razor Bow 5 5000 cred cred openwounds 30 30 dmg% 180 220 slow 30 30 mana 35 35 swing2 30 30 lifesteal 7 10 0 +Fatima Ravenclaw 100 1 1 55 49 8lb cedarbow 5 5000 dmg% 80 125 dmg-norm 25 50 att% 20 20 oskill pierce 1 1 lifesteal 7 9 mana 30 30 bar 2 2 swing1 15 15 0 +Bow of the Dead 100 1 3 1 40 8lb cedar bow 5 5000 dgry dgry invlbw dmg% 190 240 reanimate 649 10 10 res-cold 35 35 dmg-undead 150 150 manasteal 5 7 dex 20 20 allskills 1 1 0 +Kuko Shakaku 100 1 5 1 33 8lb CedarBow 5 5000 lpur lpur skill 27 3 3 explosivearrow 7 7 dmg% 150 180 pierce 50 50 dmg-fire 40 180 skilltab 0 3 3 0 +Larissa's Aim 100 1 1 55 51 8cb double bow 5 5000 invbow7 dmg% 175 220 dmg-norm 25 50 manasteal 5 5 vit 30 30 dex 15 15 res-cold 35 50 att 200 200 0 +Ranger's Path 100 1 3 1 42 8cb double bow 5 5000 dgrn dgrn invcbw dmg% 195 250 allskills 2 2 swing2 25 25 dmg-max 30 40 charged 247 5 10 deadly/lvl 12 mag% 40 70 0 +Endlesshail 100 1 5 1 36 8cb Double Bow 5 5000 res-cold 35 35 mana 40 40 ac-miss 50 50 skill 26 3 5 dmg% 180 220 dmg-cold 75 15 30 0 +Hailstrike 100 1 1 55 52 8s8 short siege bow 5 5000 dred dred dmg% 160 190 dmg-norm 35 70 skill 11 3 3 skill 21 5 5 gethit-skill 31 4 1 cold-len 300 mag% 40 40 0 +Pride's Paradox 100 1 3 1 45 8s8 short siege bow 5 5000 oran oran invsbb dmg% 200 255 knock 2 2 dmg-cold 1000 1 1 freeze 3 3 pierce 30 30 sock 2 3 0 +Whichwild String 100 1 5 1 39 8s8 Short Siege Bow 5 5000 lblu lblu hit-skill 66 2 5 res-all 40 40 deadly/lvl 8 dmg% 150 170 magicarrow 20 20 sock 2 0 +Heartseeker 100 1 1 55 54 8l8 long siege bow 5 5000 dmg% 175 200 deadly 100 100 noheal 1 1 openwounds 100 100 lifesteal 6 8 att% 50 50 ama 2 2 0 +Crimson Crusade 100 1 3 1 48 8l8 long siege bow 5 5000 dred dred invlbb dmg% 210 265 swing2 20 20 hp 45 45 mana 65 65 bloody 1 1 noheal 1 1 openwounds 100 100 dmg-norm 20 40 0 +Cliffkiller 100 1 5 1 41 8l8 Long Siege Bow 5 5000 dgld ama 2 2 dmg% 190 230 ac-miss 80 80 knock 1 1 hp 50 50 dmg-min 5 10 dmg-max 20 30 0 +Dune Runner 100 1 1 55 56 8sw rune bow 5 5000 invbow2 dmg% 150 190 dmg-norm 35 70 move3 30 30 balance2 35 35 heal-kill 10 12 oskill weaken 1 1 sock 2 2 0 +Remorhaz 100 1 3 1 50 8sw rune bow 5 5000 cblu invbow12 dmg% 225 275 swing3 40 40 ama 1 1 res-fire 75 75 pierce-fire 30 30 oskill immolation arrow 3 3 charged 17 100 10 move3 30 30 0 +Magewrath 100 1 5 1 43 8sw Rune Bow 5 5000 oran manasteal 15 15 red-mag 9 13 skill 22 3 3 att 200 250 dex 10 10 stupidity 1 1 dmg-norm 25 50 dmg% 120 150 ama 1 1 0 +Ghost Mount 100 1 1 55 59 8lw gothic bow 5 5000 dmg% 200 240 dmg-norm 20 40 res-all 25 50 move3 30 30 dex 20 30 move1 15 15 swing1 15 15 skilltab 0 3 3 0 +Harper's Call 100 1 3 1 53 8lw gothic bow 5 5000 oran oran invlwb dmg% 225 275 dmg/lvl 8 allskills 1 1 all-stats 15 15 ease -20 -20 regen 12 15 manasteal 8 8 0 +Godstrike Arch 100 1 5 1 46 8lw Gothic Bow 5 5000 lgry lgry dmg% 200 250 att% 100 150 dmg-undead 100 200 dmg-demon 100 200 hit-skill 121 5 7 swing2 50 50 regen 12 12 0 +Arachnid's Bite 100 1 1 1 65 6sb spider bow 5 5000 dmg% 190 240 openwounds 50 100 dmg-pois 200 750 750 dmg-pois/lvl 56 stupidity 3 3 balance1 15 15 allskills 2 2 0 +Black Widow 100 1 2 1 51 6sb Spider Bow 5 5000 blac blac dmg% 175 225 swing2 20 20 dmg-pois 150 900 900 slow 25 25 fade 1 1 move3 50 50 dex/lvl 4 0 +Foxfire Leaf 100 1 1 1 68 6hb blade bow 5 5000 invbow13 dmg% 180 230 dmg-fire 200 400 dmg-fire/lvl 24 dex 15 15 res-all 20 30 gold% 100 100 mag% 35 35 0 +Razor Strike 100 1 2 1 55 6hb Blade Bow 5 5000 dpur dpur dmg% 200 250 bloody 1 1 noheal 1 1 crush 20 30 dex 25 25 sock 2 2 aura fanaticism 1 1 0 +Golgomere 100 1 1 1 72 6lb shadow bow 5 5000 dmg% 200 240 dmg-norm 35 70 ease -60 -60 magicarrow 1 1 cast2 25 25 mana/lvl 10 sor 2 2 0 +Death Shade 100 1 2 1 60 6lb Shadow Bow 5 5000 blac blac dmg% 220 270 swing2 25 25 oskill revive 5 5 oskill skeleton mastery 5 12 res-all 35 35 knock 2 2 0 +Oathbow 100 1 1 1 74 6cb great bow 5 5000 invbow9 dmg% 160 200 dmg-norm 40 90 addxp 2 5 all-stats 25 25 red-dmg 20 30 red-mag 20 30 res-mag 25 25 0 +Phantom Pegasus 100 1 2 1 64 6cb Great Bow 5 5000 oran invbow4 dmg% 200 300 move3 25 25 vit 35 35 cast2 25 25 allskills 2 2 addxp 12 12 hp% 15 25 0 +Nine Lives Stealer 100 1 1 1 77 6s7 diamond bow 5 5000 blac blac dmg% 200 240 gethit-skill 53 9 9 lifesteal 9 9 heal-kill 9 9 mana-kill 9 9 nofreeze 1 1 skilltab 0 1 3 0 +Celestial Strike 100 1 2 1 67 6s7 Diamond Bow 5 5000 dblu dblu dmg% 100 150 dmg-cold 400 75 100 dmg-fire 250 400 dmg-ltng 1 500 manasteal 8 10 lifesteal 6 12 all-stats 20 30 kill-skill inner sight 100 5 dmg-norm 20 40 0 +Elflord's Victory 100 1 1 1 79 6l7 crusader bow 5 5000 dmg% 190 250 dmg/lvl 8 ama 1 2 skilltab 0 1 3 swing2 30 30 str 20 20 balance3 40 40 0 +Eaglehorn 100 1 2 1 69 6l7 Crusader Bow 5 5000 dgld dgld ignore-ac 1 1 att/lvl 12 dmg%/lvl 16 dmg% 200 200 ama 1 1 dex 25 25 0 +Patron of Perversity 100 1 1 1 82 6sw ward bow 5 5000 invbow3 dmg% 210 250 dru 2 2 swing3 40 40 balance2 25 25 oskill magic arrow 15 20 noheal 1 1 dmg-norm 50 100 skill 224 4 7 0 +Widowmaker 100 1 2 1 71 6sw ward bow 5 5000 dred dred dmg% 150 200 deadly 33 33 ignore-ac 1 1 magicarrow 11 11 oskill Guided Arrow 3 5 dmg-norm 30 60 0 +Adamantine Bow 100 1 1 1 86 6lw hydra bow 5 5000 dmg-min 100 175 dmg-max 275 400 red-dmg% 15 15 res-mag 15 15 gethit-skill 12 6 20 swing1 15 15 allskills 1 2 skilltab 0 2 2 0 +Windforce 100 1 2 1 73 6lw Hydra Bow 5 5000 dyel dyel dex 5 5 dmg/lvl 25 regen-stam 30 30 manasteal 6 8 knock 1 1 dmg% 250 250 swing2 20 20 str 10 10 0 +b16 0 +Arrow Quiver 0 +Replenishing Quiver 100 1 1 6 6 z01 Arrows 5 5000 oran oran invarro1 dmg-norm 2 8 swing1 10 10 0 +Quiver of Piercing 100 1 1 35 35 z01 Arrows 5 5000 cblu cblu invarro2 dmg-norm 4 12 swing1 15 15 pierce 15 15 0 +Quiver of Slaying 100 1 1 65 65 z01 Arrows 5 5000 blac blac invarro3 dmg-norm 5 20 swing1 20 20 pierce 25 25 skilltab 0 1 1 0 +b17 0 +Crossbows 0 +Rethral 100 1 1 25 25 lxb light crossbow 5 5000 dred dred invcbow4 dmg-norm 20 40 swing2 20 20 ama 1 1 dmg-ltng 1 40 red-dmg% 10 15 sock 1 1 0 +Nail Flinger 100 1 3 1 12 lxb light crossbow 5 5000 invlxb dmg% 100 150 dmg-max 7 12 pierce 100 100 red-dmg 4 6 red-mag 2 5 0 +Leadcrow 0 1 5 1 8 lxb Light Crossbow 5 5000 dyel dex 10 10 hp 10 10 dmg% 70 100 res-pois 30 30 deadly 25 25 att 40 40 0 +Piercing Bolt 100 1 1 28 28 mxb crossbow 5 5000 invcbow5 dmg-norm 25 50 pierce 100 100 res-all 20 30 knock 1 1 att% 20 20 lifesteal 5 5 addxp 2 3 0 +Janglebright 100 1 3 1 18 mxb crossbow 5 5000 whit whit invmxb dmg% 125 175 light 3 5 res-all 25 25 balance2 15 15 swing1 10 10 sock 1 1 0 +Ichorsting 0 1 5 1 14 mxb Crossbow 5 5000 lgld dmg-pois 75 102 102 dex 20 20 pierce 50 50 att 50 50 dmg% 50 100 swing2 20 20 0 +Harpo Bogglinn 100 1 1 32 32 hxb heavy crossbow 5 5000 dmg-norm 25 50 swing1 10 10 slow 15 15 pierce-fire 10 10 extra-fire 10 10 allskills 1 1 0 +Spikethrower 100 1 3 1 24 hxb heavy crossbow 5 5000 invhxb dmg% 150 200 dmg-norm 10 20 crush 25 25 deadly 25 25 noheal 1 1 ama 1 1 0 +Hellcast 0 1 5 1 20 hxb heavy crossbow 5 5000 dgld explosivearrow 5 5 res-fire-max 15 15 res-fire 15 15 att 70 70 swing2 20 20 dmg% 70 100 dmg-fire 15 35 0 +Synthalus 100 1 1 33 35 rxb repeating crossbow 5 5000 invcbow3 dmg-norm 35 70 att 200 200 balance2 20 20 dex 15 15 mana 35 35 dmg/lvl 4 magicarrow 1 1 0 +King's Nail 100 1 3 1 30 rxb repeating crossbow 5 5000 invrxb allskills 1 1 oskill guided arrow 3 5 swing2 30 30 lifesteal 5 5 manasteal 3 3 gold% 50 50 hit-skill 87 15 3 dmg-min 10 20 dmg-max 30 50 dmg% 100 125 0 +Doomspittle 0 1 5 1 25 rxb Repeating Crossbow 5 5000 lpur ama 1 1 pierce 35 35 swing3 30 30 hp 15 15 dmg% 60 100 0 +Kyuss' Crossbow 100 1 1 55 48 8lx arbalest 5 5000 invcbow6 dmg% 175 220 swing2 20 20 oskill freezing arrow 1 1 heal-kill 10 15 regen-mana 60 60 mag% 30 50 gold% 75 75 0 +Garlana Firebolt 100 1 3 1 38 8lx arbalest 5 5000 dred dred invlxb dmg% 180 240 swing2 25 25 ama 1 1 oskill exploding arrow 6 6 res-cold 25 40 dmg-norm 15 30 fireskill 2 2 0 +Langer Briser 100 1 5 1 32 8lx Arbalest 5 5000 knock 1 1 dmg% 170 200 mag% 30 60 hp 30 30 dmg-max 10 30 openwounds 33 33 dmg-ltng 1 212 0 +Giant Hair Crossbow 100 1 1 55 53 8mx siege crossbow 5 5000 dmg% 200 250 swing3 40 40 ignore-ac 1 1 lifesteal 6 6 dmg-max 25 25 ease 15 15 pierce-cold 10 15 0 +Raven Myst 100 1 3 1 43 8mx siege crossbow 5 5000 whit whit invmxb dmg% 150 150 dmg/lvl 13 swing3 30 30 oskill raven 5 5 aura 120 3 6 sock 2 2 knock 1 1 0 +Pus Spiter 100 1 5 1 36 8mx Siege Crossbow 5 5000 cgrn cgrn dmg-pois 200 192 192 hit-skill 91 4 1 nec 2 2 ease -60 -60 gethit-skill 92 9 6 swing2 10 10 dmg% 150 220 att/lvl 10 0 +Whyte Stag 100 1 1 55 56 8hx ballista 5 5000 whit whit dmg% 200 300 dmg-cold 300 100 200 freeze 1 1 noheal 1 1 half-freeze 1 1 res-cold 35 50 sock 1 1 0 +Senmet's Boltcaster 100 1 3 1 47 8hx ballista 5 5000 invcbow1 dmg% 200 250 swing2 25 25 heal-kill 15 15 addxp 5 5 charged 149 10 10 balance2 20 20 magicarrow 1 1 0 +Buriza-Do Kyanon 100 1 5 1 41 8hx Balista 5 5000 pierce 100 100 dex 35 35 ac 75 150 dmg/lvl 20 swing2 80 80 dmg% 150 200 freeze 3 3 dmg-cold 200 32 196 0 +Ashira's Stunbeam 100 1 1 55 61 8rx chu-ko-nu 5 5000 invcbow8 dmg-norm 75 150 swing2 30 30 lifesteal 6 8 manasteal 5 7 gethit-skill 38 100 5 all-stats 15 15 mana 40 40 0 +Widow's Refrain 100 1 3 1 54 8rx chu-ko-nu 5 5000 dgry dgry invrxb dmg% 215 270 swing2 20 20 dmg-norm 25 50 charged quickness 17 12 charged fade 22 8 dmg-to-mana 12 12 rip 1 1 0 +Demon Machine 100 1 5 1 49 8rx Chu-Ko-Nu 5 5000 blac blac ac 321 321 mana 36 36 pierce 66 66 explosivearrow 6 6 dmg-max 66 66 dmg% 123 123 att 632 632 0 +Stoneblaster 100 1 1 1 67 6lx pellet bow 5 5000 dgry dgry dmg% 200 240 knock 1 1 slow 20 20 stupidity 1 1 swing2 15 15 addxp 3 3 0 +Doubleshot Machine 100 1 2 1 55 6lx Pellet Bow 6 6000 oskill multiple shot 1 3 pierce 100 100 dmg% 150 250 sock 2 3 dmg-norm 40 80 0 +Wightslayer 100 1 1 1 74 6mx gorgon crossbow 5 5000 invcbow2 dmg% 220 260 dmg-undead 175 175 att-undead 300 300 red-dmg% 10 15 red-mag 15 25 0 +Grey Render 100 1 2 1 64 6mx Gorgon Crossbow 5 5000 lgry lgry dmg% 200 300 swing2 25 25 manasteal 5 8 mana 35 60 ama 2 2 crush 25 25 ignore-ac 1 1 0 +Bluebeard 100 1 1 1 79 6hx colossus crossbow 5 5000 dblu dblu dmg% 175 200 swing2 20 20 allskills 1 1 mag% 30 30 hp 55 55 dmg-cold 200 75 150 pierce-cold 25 25 extra-cold 20 20 0 +Hellrack 100 1 2 1 69 6hx colossus crossbow 5 5000 cred cred dmg% 220 275 dmg-elem 33 63 324 swing2 20 20 att% 100 150 sock 2 2 charged Immolation Arrow 150 18 0 +Thor's Bolt 100 1 1 1 85 6rx demon crossbow 5 5000 invcbow7 dmg-min 50 75 dmg-max 150 200 dmg-ltng 1 555 gethit-skill 49 8 17 res-ltng 40 50 pierce-ltng 35 35 res-mag 15 15 0 +Gutsiphon 100 1 2 1 71 6rx demon crossbow 5 5000 lgrn lgrn dmg% 200 300 pierce 33 33 lifesteal 12 18 slow 25 25 openwounds 33 33 0 +b18 0 +Bolt Quiver 0 +Replenishing Bolt Case 100 1 1 6 6 z02 Bolts 5 5000 oran oran dmg-norm 2 8 swing1 10 10 0 +Bolt Case of Piercing 100 1 1 35 35 z02 Bolts 5 5000 cblu cblu dmg-norm 4 12 swing1 15 15 pierce 15 15 0 +Bolt Case of Slaying 100 1 1 65 65 z02 Bolts 5 5000 blac blac dmg-norm 5 20 swing1 20 20 pierce 15 15 skilltab 0 1 1 0 +b19 0 +Javelins 0 +Hippogriff Wing 100 1 1 20 21 jav javelin 5 5000 dgld dgld dmg-norm 20 40 dmg% 65 90 swing1 15 15 allskills 1 1 move1 15 15 rep-quant 20 0 +Leafrazor 100 1 3 1 6 jav javelin 5 5000 dmg% 100 150 rep-quant 20 stack 75 75 dmg-norm 2 4 deadly 35 35 pierce 40 40 skill 9 1 2 0 +Stone Eater 100 1 5 1 3 jav Javelin 5 5000 lgry lgry dmg-norm 3 8 hp 20 20 ac 45 75 red-dmg 2 4 red-mag 2 4 block 10 15 0 +Amazon's Kiss 100 1 1 24 23 pil pilum 5 5000 lred lred invjav2 dmg% 70 100 dmg-norm 25 45 ama 1 1 lifesteal 5 8 stack 150 150 regen 4 8 res-fire 15 30 0 +Skyglow 100 1 3 1 15 pil pilum 5 5000 oran oran dmg% 100 165 rep-quant 20 stack 75 75 dmg-fire 8 12 ama 1 1 block2 20 20 heal-kill 5 5 0 +Luck Chaser 100 1 5 1 11 pil Pilum 5 5000 invjav6 dmg% 60 100 bar 1 1 ama 1 1 oskill critical strike 1 1 swing1 15 15 res-ltng 25 25 stack 75 75 0 +Chaoskiller 100 1 1 26 27 ssp short spear 5 5000 oran oran dmg% 75 100 dmg-norm 25 50 rep-quant 25 stack 50 50 rip 1 1 addxp 2 5 ignore-ac 1 1 0 +Teeth of Infinity 100 1 3 1 21 ssp short spear 5 5000 invjav9 dmg% 135 185 rep-quant 20 stack 75 75 oskill teeth 5 5 charged 67 250 30 swing2 20 20 lifesteal 7 7 0 +Pridebreaker 100 1 5 1 17 ssp Short Spear 5 5000 whit whit dmg% 60 100 rep-quant 35 dmg-throw 5 10 dmg-demon 75 75 att 75 75 stack 30 30 0 +Cadin'Sor 100 1 1 28 33 glv glaive 5 5000 blac blac dmg% 80 100 dmg-norm 30 55 stack 60 60 res-ltng 20 30 dex 15 15 oskill spear mastery 1 1 res-pois 15 25 0 +Sting of Humiliation 100 1 3 1 28 glv glaive 5 5000 dmg% 160 200 rep-quant 20 stack 100 100 bar 1 1 skill 140 5 5 howl 144 144 res-all 35 35 0 +Mind Creche 100 1 5 1 24 glv Glaive 5 5000 oran dmg% 90 120 mana 40 40 ease -35 -35 cast1 15 15 oskill poison javelin 1 2 deadly 25 40 stupidity 1 1 0 +Dancing Scarecrow 100 1 1 31 37 tsp throwing spear 5 5000 dmg-norm 40 70 swing1 15 15 balance1 15 15 cast1 15 15 block1 15 15 att 200 200 knock 1 1 pierce 33 33 0 +Splinterfreeze 100 1 3 1 32 tsp throwing spear 5 5000 cblu cblu invjav1 dmg% 180 200 rep-quant 20 stack 125 125 hit-skill 44 100 6 aura holy freeze 1 3 oskill frozen orb 1 1 oskill cold mastery 1 1 mana 75 75 0 +Baba Yaga's Needle 100 1 5 1 29 tsp Throwing Spear 5 5000 dmg% 100 145 rep-quant 25 lifesteal 6 8 res-pois 25 40 move2 25 25 stack 40 40 abs-mag 3 7 0 +Aiel Javelins 100 1 1 55 54 9ja war javelin 5 5000 dmg% 100 125 dmg-norm 35 70 dmg/lvl 4 rep-quant 20 swing2 20 20 pierce 25 25 reduce-ac 15 15 0 +Rhyme of the Bard 100 1 3 1 37 9ja war javelin 5 5000 lpur lpur dmg% 200 250 rep-quant 25 stack 60 60 charged 149 200 3 charged 138 200 3 charged 154 200 2 swing3 35 35 0 +Severalkill 100 1 5 1 31 9ja War Javelin 5 5000 dyel dyel dmg% 165 210 rep-quant 20 stack 25 25 res-cold 20 30 res-fire 20 30 heal-kill 12 12 crush 25 25 0 +Gnomebane 100 1 1 55 58 9pi great pilum 5 5000 invjav3 dmg% 100 125 dmg-norm 35 70 dmg/lvl 5 bar 2 2 oskill spear mastery 2 2 stack 65 65 res-ltng 25 35 hp 35 55 0 +Weeping at the Gate 100 1 3 1 43 9pi great pilum 5 5000 lgrn lgrn dmg% 200 250 rep-quant 25 stack 65 65 mana 65 65 dmg-to-mana 25 25 regen-mana 150 150 red-dmg% 10 15 0 +Trump of Jericho 100 1 5 1 35 9pi Great Pilum 5 5000 lyel lyel dmg% 150 200 rep-quant 30 mag% 35 50 ac/lvl 10 swing2 25 25 str 25 25 dex 25 25 stam 75 75 0 +Artimus's Spiculum 100 1 1 55 61 9s9 simbilan 5 5000 oran oran dmg% 100 125 dmg-norm 40 80 dmg/lvl 6 stack 75 75 mag% 35 35 pierce-ltng 25 25 extra-ltng 10 15 0 +Cursed One 100 1 3 1 47 9s9 simbilan 5 5000 blac blac dmg% 200 250 rep-quant 25 stack 70 70 gethit-skill 81 12 7 regen -1 -1 dmg/lvl 6 str 15 25 0 +Hatemonger 100 1 5 1 38 9s9 Simbilan 5 5000 invjav7 dmg% 140 225 ac-hth 200 300 regen 10 15 manasteal 7 7 lifesteal 7 7 dmg-throw 20 40 oskill attract 5 5 0 +Confusion Flight 100 1 1 55 65 9gl spiculum 5 5000 cred cred dmg% 110 140 dmg-norm 45 90 dmg/lvl 7 stack 100 100 rep-quant 40 stupidity 2 2 swing2 20 20 red-dmg 15 20 red-mag 25 25 0 +Blessed One 100 1 3 1 50 9gl spiculum 5 5000 whit whit dmg% 215 265 rep-quant 25 stack 75 75 aura 99 5 15 block 25 25 ac 70 100 all-stats 10 10 allskills 1 1 0 +Warbreeder 100 1 5 1 41 9gl Spiculum 5 5000 blac blac dmg% 175 235 rep-quant 35 res-all 40 50 hp 75 75 oskill charged strike 6 6 pierce-ltng 35 35 slow 15 15 dmg-ltng 1 250 0 +Silver-Tipped Harpoons 100 1 1 55 69 9ts harpoon 5 5000 whit whit dmg% 120 150 dmg-norm 40 80 dmg/lvl 8 stack 45 45 allskills 2 2 res-all 20 30 dmg-undead 200 200 thorns 20 30 dmg-ltng 15 150 0 +Blessings of Osiris 100 1 3 1 54 9ts harpoon 5 5000 oran oran dmg% 225 270 rep-quant 25 stack 80 80 oskill lightning fury 3 5 pierce-ltng 10 10 res-ltng 35 60 abs-ltng% 15 15 hp% 10 15 0 +Bowel Twister 100 1 5 1 45 9ts Harpoon 5 5000 invjav8 dmg% 180 240 rep-quant 35 stack 100 100 aura 118 9 15 res-ltng 65 65 abs-ltng% 35 35 noheal 1 1 0 +Jaguar's Claw 100 1 1 1 70 7ja hyperion javelin 5 5000 dyel dyel dmg% 225 275 rep-quant 40 stack 40 40 reduce-ac 15 20 bloody 1 1 deadly/lvl 8 lifesteal 7 7 0 +Arc of the Rainbow 100 1 2 1 55 7ja Hyperion Javelin 5 5000 dmg% 180 250 ac-miss 200 300 dmg-fire 75 200 dmg-ltng 10 300 dmg-cold 200 100 140 swing3 40 40 addxp 5 10 gold% 200 200 0 +Woundripper 100 1 2 1 61 7pi stygian pilum 5 5000 invjav10 dmg% 225 275 rep-quant 20 stack 65 65 heal-kill 10 10 noheal 1 1 openwounds 88 88 oskill plague javelin 5 5 block2 25 25 0 +Summerstrike 100 1 1 1 74 7pi Stygian Pilum 5 5000 cred cred dmg% 200 250 rep-quant 30 dmg-fire 300 500 abs-fire% 40 50 ethereal 1 1 stack 75 75 0 +Clockwork Horror 100 1 1 1 77 7s7 balrog spear 5 5000 invjav4 dmg% 230 280 rep-quant 25 stack 50 50 crush 22 33 howl 41 41 move2 25 25 str/lvl 5 dex/lvl 5 0 +Demon's Arch 100 1 2 1 66 7s7 balrog spear 5 5000 cred cred dmg% 200 265 dmg-fire 232 323 lifesteal 6 12 rep-quant 30 swing2 30 30 dmg-ltng 23 333 0 +Invisible Stalker 100 1 1 1 81 7gl ghost glaive 5 5000 dmg% 250 300 stack 300 300 dmg/lvl 8 allskills 1 1 fade 1 1 oskill quickness 3 3 move3 60 60 0 +Wraithflight 100 1 2 1 70 7gl ghost glaive 3 5000 dblu dblu dmg% 150 250 rep-quant 40 lifesteal 9 13 mana-kill 15 15 ethereal 1 1 allskills 2 2 0 +Serpent Lord 100 1 1 1 86 7ts winged harpoon 5 5000 invjav5 dmg% 300 400 swing3 40 40 stack 125 125 dmg-pois 75 888 888 res-pois 100 100 slow 50 50 allskills 1 1 pierce 20 20 0 +Gargoyle's Bite 100 1 2 1 75 7ts winged harpoon 5 5000 cgrn cgrn dmg% 200 300 rep-quant 30 dmg-pois 250 300 300 lifesteal 9 15 charged Plague Javelin 60 11 bar 3 3 0 +b20 0 +Throwing Axes 0 +Spectral Slayer 100 1 1 20 20 tax throwing axe 5 5000 dgry dgry dmg% 80 80 dmg-norm 20 40 stack 50 50 rep-quant 15 bar 1 1 fade 1 1 ethereal 1 1 0 +Tonguecutter 100 1 3 1 10 tax throwing axe 5 5000 invaxe17 dmg% 100 150 rep-quant 20 stack 75 75 bloody 1 1 lifesteal 5 7 manasteal 5 7 bar 1 1 0 +Solstice Edge 100 1 5 1 6 tax Throwing Axe 5 5000 oran oran dmg-throw 4 12 stack 60 60 rep-quant 45 dmg-fire 3 9 mag% 20 30 knock 1 1 0 +Oakplume 100 1 1 28 35 bal balanced axe 5 5000 dmg% 80 120 dmg-norm 30 60 stack 75 75 rep-quant 50 skill 140 3 3 swing2 20 20 balance2 20 20 res-pois 35 35 lifesteal 6 6 0 +Timbersplitter 100 1 3 1 27 bal balanced axe 5 5000 lgrn lgrn dmg% 175 200 rep-quant 20 stack 75 75 pierce 25 25 oskill critical strike 1 1 openwounds 35 35 res-ltng 15 15 abs-ltng% 5 5 0 +Spleen Feaster 100 1 5 1 22 bal Balanced Axe 5 5000 dgrn dgrn dmg-norm 10 20 stack 60 60 dmg-pois 150 122 122 noheal 1 1 dex 15 15 att% 30 30 deadly 20 40 0 +Banshee's Cry 100 1 1 55 50 9ta francisca 5 5000 dmg% 120 140 dmg-norm 25 50 dmg-throw 25 40 skilltab 13 2 2 howl 122 122 hit-skill 48 4 5 slow 25 25 knock 2 2 0 +Despicable Behavior 100 1 3 1 43 9ta francisca 5 5000 dblu dblu dmg% 200 250 rep-quant 25 stack 60 60 bar 2 2 att% 25 25 swing2 20 20 block3 30 30 hit-skill 59 5 7 0 +The Scalper 100 1 5 1 36 9ta Francisca 5 5000 rep-quant 30 dmg% 150 200 att% 25 25 swing2 20 20 openwounds 33 33 lifesteal 4 6 mana-kill 4 4 0 +Axes of Jahadra 100 1 1 55 60 9b8 hurlbat 5 5000 lpur lpur dmg% 125 150 dmg-norm 30 60 dmg-throw 30 50 allskills 1 1 mag% 25 35 hit-skill 51 3 1 thorns 25 35 red-mag 10 15 0 +Instigator 100 1 3 1 54 9b8 hurlbat 5 5000 dgld dgld dmg% 225 270 rep-quant 25 stack 60 60 bar 2 2 manasteal 6 6 swing2 25 25 balance3 30 30 gethit-skill 42 7 4 0 +Brainraver 100 1 5 1 47 9b8 Hurlbat 5 5000 dmg% 165 225 rep-quant 30 stack 45 45 swing3 40 40 allskills 1 1 stupidity 2 2 slow 50 50 0 +Winged Serpent 100 1 1 1 77 7ta flying axe 5 5000 cgrn cgrn dmg% 250 300 rep-quant 15 stack 70 70 bar 1 2 dmg-pois 200 1250 1250 res-all 25 30 charged 54 255 1 str 20 20 0 +Gimmershred 100 1 2 1 69 7ta flying axe 5 5000 lgrn dmg% 160 240 dmg-fire 218 483 dmg-cold 100 176 397 dmg-ltng 29 501 stack 60 60 swing2 30 30 0 +Golden Wyndlass 100 1 1 1 85 7b8 winged axe 5 5000 dgld dgld dmg% 250 300 rep-quant 50 skilltab 14 1 3 ignore-ac 1 1 swing2 20 20 stupidity 2 2 mana 50 50 oskill redemption 3 3 move2 20 20 0 +Lacerator 100 1 2 1 75 7b8 winged axe 5 5000 blac blac dmg% 180 250 rep-quant 25 swing2 30 30 noheal 1 1 openwounds 33 33 howl 64 64 hit-skill Amplify Damage 33 3 0 +b21 0 +Throwing Knifes 0 +Sheera's Knives 100 1 1 25 22 tkf throwing knife 5 5000 dred dred dmg% 60 80 dmg-norm 20 40 stack 50 50 rep-quant 20 ac-hth 100 100 pierce 20 20 red-dmg 6 10 res-all 15 15 0 +Subtle Slice 100 1 3 1 7 tkf throwing knife 5 5000 lyel lyel dmg% 100 150 rep-quant 20 stack 75 75 dmg-norm 4 8 deadly 35 35 dmg-pois 125 55 55 gethit-skill 36 14 3 dex 10 10 0 +Splinterbeam 100 1 5 1 4 tkf Throwing Knife 5 5000 lgld dgrn dmg% 65 100 rep-quant 25 swing2 25 25 manasteal 3 6 res-cold 25 35 res-ltng 25 35 reduce-ac 10 15 0 +Deadly Needle 100 1 1 32 38 bkf balanced knife 5 5000 dmg% 85 105 dmg-norm 25 50 stack 90 90 rep-quant 25 str 15 15 deadly/lvl 9 res-fire 20 30 res-ltng 20 30 0 +Initiator 100 1 3 1 22 bkf balanced knife 5 5000 oran dmg% 170 200 rep-quant 25 stack 75 75 dmg-throw 8 20 swing2 25 25 slow 20 20 aura 108 4 4 str 15 15 ease -15 -15 0 +Dead Scoffer 100 1 5 1 19 bkf Balanced Knife 5 5000 blac dmg-throw 10 20 dmg% 75 110 stack 100 100 heal-kill 8 10 oskill raise skeleton 3 5 dmg-undead 350 350 0 +Swarming Blades 100 1 1 55 57 9tk battle dart 5 5000 cblu dmg% 100 100 dmg/lvl 12 swing3 60 60 stack 100 100 rep-quant 20 20 hp 50 60 res-pois 25 35 reduce-ac 15 15 0 +Silt Runner 100 1 3 1 42 9tk battle dart 5 5000 dgry dgry dmg% 200 250 rep-quant 20 stack 55 55 stupidity 2 2 lifesteal 8 8 move3 35 35 all-stats 20 20 hp 50 65 0 +Deathbit 100 1 5 1 34 9tk battle dart 5 5000 deadly 40 40 dmg% 130 180 att 200 450 lifesteal 7 9 manasteal 4 6 rep-quant 25 0 +Darts of Evermeet 100 1 1 55 69 9bk war dart 5 5000 dmg% 120 150 rep-quant 10 stack 60 60 dmg-norm 40 80 allskills 1 1 extra-fire 15 25 dmg-fire 60 120 ac% 20 20 dex 15 20 0 +Ravenstar 100 1 3 1 52 9bk war dart 5 5000 lblu dmg% 220 270 rep-quant 15 stack 75 75 slow 25 25 manasteal 8 8 cast3 30 30 allskills 1 1 mana 30 50 0 +Pain Harvester 100 1 5 1 45 9bk War Dart 5 5000 lpur dmg% 160 200 stack 75 75 att% 50 50 dex 20 20 str 20 20 bar 2 2 nofreeze 1 1 0 +Fallen Glory 100 1 1 1 71 7tk flying knife 5 5000 dred dmg% 260 325 stack 65 65 rep-quant 15 allskills 2 2 skilltab 12 1 3 swing2 20 20 oskill 56 2 2 oskill 61 2 2 0 +Shard of the North Star 100 1 2 1 64 7tk Flying Knife 5 5000 lgld lgld dmg% 200 300 rep-quant 35 res-all 25 40 lifesteal 5 10 dmg-elem 300 50 100 freeze 3 3 hit-skill 56 12 8 0 +Frostbite Shard 100 1 1 1 80 7bk winged knife 5 5000 dmg% 275 325 rep-quant 20 stack 65 65 dmg-cold 250 300 freeze 6 6 dmg/lvl 6 res-cold 35 50 mana 65 65 0 +Warshrike 100 1 2 1 73 7bk winged knife 5 5000 cblu cblu dmg% 200 250 pierce 50 50 swing2 30 30 deadly 50 50 rep-quant 30 hit-skill Nova 25 9 0 +b22 0 +Torso Armors 0 +Hungerpang 100 1 1 1 11 qui quilted armor 5 5000 invbody20 ac 30 50 regen -1 -1 hp 65 65 lifesteal 4 6 regen-mana 40 40 0 +Ashenwrath 100 1 3 1 6 qui quilted armor 5 5000 invbody5 ac 20 30 nec 1 1 fireskill 1 1 res-fire 35 35 sock 1 2 0 +Greyform 0 1 5 1 2 qui Quilted Armor 5 5000 lgry lgry invbody7 red-mag 3 3 res-cold 20 20 res-fire 20 20 dex 10 10 lifesteal 5 5 ac 20 40 0 +Ogden's Shroud 100 1 1 1 13 lea leather armor 5 5000 invbody46 ac 45 65 res-ltng 25 25 balance2 20 20 addxp 2 3 gold% 100 100 0 +JuJu Flame 100 1 3 1 8 lea leather armor 5 5000 invbody49 ac 30 40 sor 1 1 kill-skill teeth 25 4 charged 36 200 17 0 +Blinkbats Form 0 1 5 1 4 lea Leather Armor 5 5000 invbody73 ac-miss 50 50 move2 10 10 ac 20 40 fire-min 1 3 fire-max 6 9 balance2 40 40 0 +Armor of Gloom 100 1 1 1 15 hla hard leather 5 5000 invbody72 ac 70 85 stupidity 1 1 gethit-skill 71 7 2 res-pois 25 35 vit 10 10 sock 1 1 0 +Stinkshroud 100 1 3 1 10 hla hard leather 5 5000 invbody50 ac 50 65 gethit-skill 72 5 9 ass 1 1 skill-rand 2 252 257 res-pois 35 35 0 +The Centurion 0 1 5 1 6 hla Hard Leather 5 5000 invbody12 ac 30 50 att 50 50 red-dmg 2 2 hp 15 25 mana 15 25 stam 15 25 regen 5 5 stamdrain 25 25 0 +Scarab of Protection 100 1 1 1 17 stu studded leather 5 5000 invbody4 ac 100 115 red-dmg 8 12 res-mag 10 10 res-all 10 20 hp 35 45 block2 15 15 0 +Gemini Coat 100 1 3 1 12 stu studded leather 5 5000 invbody27 ac% 80 100 ac 25 40 ama 1 1 aura might 1 1 dur% 25 25 dmg-throw 5 10 0 +Twitchthroe 0 1 5 1 8 stu Studded Leather 5 5000 lgrn lgrn swing2 20 20 dex 10 10 block 25 25 ac 25 50 str 10 10 balance2 20 20 0 +The Shadowed One 100 1 1 1 19 rng ring mail 5 5000 blac blac ac 115 130 light -3 -3 ass 1 1 move2 20 20 ignore-ac 1 1 mana 25 35 0 +Steelflesh 100 1 3 1 14 rng ring mail 5 5000 invbody2 ac% 125 200 bar 1 1 dmg% 20 25 red-dmg% 10 15 oskill sacrifice 3 5 0 +Darkglow 0 1 5 1 11 rng Ring Mail 5 5000 dgrn dgrn invbody51 att% 20 20 res-all-max 5 5 light 3 5 ac-hth 50 50 res-all 10 20 ac% 200 300 0 +Red Dragon Scails 100 1 1 1 21 scl scale mail 5 5000 dred dred invbody25 ac 130 150 red-dmg% 10 15 res-fire 45 65 abs-fire% 10 15 pierce-fire 10 15 extra-fire 10 15 0 +Mandrake's Bloom 100 1 3 1 16 scl scale mail 5 5000 ac% 125 170 pal 1 1 gethit-skill 68 5 8 block 10 15 dmg-fire 10 20 0 +Hawkmail 0 1 5 1 13 scl Scale Mail 5 5000 invbody37 ac% 80 100 res-cold-max 15 15 res-cold 15 30 nofreeze 1 1 move2 10 10 0 +Ghosly Chainmail 100 1 1 1 23 chn chain mail 5 5000 whit ac 155 175 ethereal 1 1 rep-dur 25 res-cold 30 50 pierce-cold 10 15 extra-cold 10 15 0 +Xanadu Dreams 100 1 3 1 18 chn chain mail 5 5000 invbody8 ac% 100 200 dru 1 1 dur% 100 100 hit-skill 71 5 2 res-all 15 25 lifesteal 3 3 0 +Sparking Mail 0 1 5 1 15 chn Chain Mail 5 5000 lyel lyel ac% 75 85 dmg-ltng 1 20 light-thorns 10 14 res-ltng 30 30 0 +Breastplate of Gond 100 1 1 1 25 brs breast plate 5 5000 dgrn invbody70 ac 180 200 pierce-ltng 10 15 abs-ltng% 10 15 extra-ltng 10 15 res-ltng 40 60 dmg-ltng 1 75 0 +Shattering Blow 100 1 3 1 21 brs breast plate 5 5000 invbody68 ac% 120 150 crush/lvl 8 deadly/lvl 8 dmg-norm 5 10 swing1 15 15 0 +Venomsward 0 1 5 1 17 brs Breast Plate 5 5000 res-pois-max 15 15 res-pois-len 50 50 res-pois 90 90 light 2 2 ac% 60 100 0 +Starsong 100 1 1 1 26 spl splint mail 5 5000 lgry lgry ac 205 220 hp 25 50 mana 25 55 mana-kill 3 5 heal-kill 8 12 res-pois 30 50 0 +Russetfire 100 1 3 1 22 spl splint mail 5 5000 invbody35 ac 100 150 mag% 35 50 res-fire 40 50 abs-fire% 20 20 aura holy fire 3 5 sock 1 2 0 +Iceblink 0 1 5 1 19 spl Splint Mail 5 5000 lgld lgld freeze 1 1 res-cold 30 30 light 4 4 red-mag 1 3 ac% 70 110 0 +Armor of Warmth 100 1 1 1 28 plt plate mail 5 5000 dred cred invbody71 ac 225 250 oskill warmth 3 5 att 75 75 dex 15 15 enr 15 15 0 +Celestial Revelation 100 1 3 1 25 plt plate mail 5 5000 invbody21 extra-fire 15 20 extra-cold 15 20 extra-pois 15 20 extra-ltng 15 20 cast1 20 20 ac% 125 150 0 +Boneflesh 0 1 5 1 21 plt Plate Mail 5 5000 dgld dgld lifesteal 5 5 ac% 100 120 att 35 35 openwounds 25 25 red-dmg% 8 12 0 +Dreamscape 100 1 1 1 29 fld field plate 5 5000 blac blac ac 260 300 stupidity 2 2 vit 20 20 block 15 15 block2 30 30 sock 1 2 0 +Ion Storm 100 1 3 1 26 fld field plate 5 5000 dgrn dgrn invbody43 dmg-elem 200 1 65 ac 150 190 addxp 4 7 sock 3 4 0 +Rockfleece 0 1 5 1 22 fld Field Plate 5 5000 dgry dgry ease -10 -10 ac% 100 130 red-dmg% 10 20 red-dmg 5 10 str 5 15 0 +A Knight's Tale 100 1 1 1 30 gth gothic plate 5 5000 lgry lgry ac 300 350 allskills 1 1 sock 4 4 0 +Golem�s Gain 100 1 3 1 26 gth gothic plate 5 5000 invbody66 ac% 120 200 str 25 25 oskill golem mastery 8 8 oskill bloodgolem 2 5 charged 75 5 30 res-all 25 25 0 +Rattlecage 0 1 5 1 23 gth Gothic Plate 5 5000 dpur dpur howl 52 52 att 45 100 crush 25 25 ac 200 250 0 +Kaz's Battle Armor 100 1 1 1 32 ful full plate mail 5 5000 invbody67 ac 355 400 dmg-norm 5 10 swing1 15 15 balance2 20 20 gethit-skill 58 6 3 mana 35 35 0 +Halcyon Shroud 100 1 3 1 28 ful full plate mail 5 5000 invful allskills 1 1 res-fire 25 40 res-cold 25 40 res-ltng 25 40 red-dmg 10 20 regen 8 12 ac% 100 200 0 +Goldskin 0 1 5 1 25 ful Full Plate Mail 5 5000 dgld dgld ac% 120 150 res-all 35 35 thorns 10 10 light 2 2 gold% 100 200 mag% 30 50 0 +Greyhawk Dragon 100 1 1 1 34 aar ancient armor 5 5000 dgry dgry ac 400 500 balance3 40 40 heal-kill 5 5 addxp 2 3 sock 2 2 res-all 35 35 0 +Killing Blow 100 1 3 1 30 aar ancient armor 5 5000 oran oran ac% 150 200 dmg-max 25 25 noheal 1 1 bloody 1 1 deadly 33 33 oskill attract 2 4 knock 1 1 0 +Victors Silk 0 1 5 1 26 aar Ancient Armor 5 5000 manasteal 5 5 allskills 1 1 light 2 2 ac% 100 120 lifesteal 6 8 0 +Weightless Grace 100 1 1 1 36 ltp light plate 5 5000 invbody53 ac 300 400 ease -50 -50 dex 25 25 allskills 1 1 gold% 50 50 mag% 35 50 hp 50 60 0 +Madness of Chthulu 100 1 3 1 30 ltp light plate 5 5000 invbody19 ac 200 250 ease 25 25 all-stats 20 20 rip 1 1 ignore-ac 1 1 death-skill poison nova 100 15 0 +Heavenly Garb 0 1 5 1 27 ltp Light Plate 5 5000 cblu lblu invbody75 ac% 100 125 res-all 10 20 regen-mana 75 75 enr 15 15 dmg-undead 100 100 att-undead 100 100 0 +Rainbow Cloak 100 1 3 1 32 xui ghost armor 5 5000 invbody23 ac% 100 150 res-all 15 25 allskills 1 1 res-all-max 5 5 0 +Gillian's Brazier 100 1 1 1 41 xui ghost armor 5 5000 invbody42 ac% 90 125 ama 1 2 ass 1 2 sor 1 2 dex 35 35 str 25 25 cast2 25 25 0 +The Spirit Shroud 100 1 5 1 28 xui Ghost Armor 3 5000 invbody26 nofreeze 1 1 allskills 1 1 red-mag 7 11 regen 10 10 ac% 150 150 0 +Anaconda Skin 100 1 1 1 42 xea serpentskin armor 5 5000 invbody13 ac% 110 160 crush 20 40 move2 25 25 res-pois 25 35 str 15 15 red-dmg 10 20 red-mag 10 15 0 +Scavanger's Carapace 100 1 3 1 34 xea serpentskin armor 5 5000 invbody38 ac% 95 130 rep-dur 20 red-dmg% 10 20 res-cold 35 50 res-cold-max 10 10 ac/lvl 10 dmg-und/lvl 24 0 +Skin of the Vipermagi 100 1 5 1 29 xea SerpentSkin Armor 3 5000 dblu dblu ac% 120 120 res-all 20 35 cast3 30 30 red-mag 9 13 allskills 1 1 0 +Torn Flesh of Souls 100 1 1 1 43 xla demonhide armor 5 5000 dgry dgry ac% 120 170 dmg-demon 75 75 nec 1 1 skill-rand 3 66 95 skill-rand 3 66 95 cast2 20 20 mana/lvl 6 0 +The Defiler's Flesh 100 1 3 1 36 xla demonhide armor 5 5000 invbody24 ac% 100 150 heal-kill 5 8 mag% 40 40 balance2 25 25 lifesteal 4 6 hp 40 40 skill 83 1 3 skill 89 1 3 0 +Skin of the Flayerd One 100 1 5 1 31 xla Demonhide Armor 3 5000 invbody10 rep-dur 10 regen 15 25 dur 30 30 lifesteal 5 7 ac% 150 190 thorns 15 15 0 +Lilt of the Dryad 100 1 1 1 44 xtu trellised armor 5 5000 invbody16 ac% 130 180 swing3 20 20 ama 1 1 skilltab 1 1 2 res-fire 20 40 res-ltng 20 40 slow 15 15 0 +Cold Comfort 100 1 3 1 38 xtu trellised armor 5 5000 dblu dblu invbody57 ac% 110 150 ac 200 250 gethit-skill 60 15 8 res-cold 50 50 pierce-cold 15 15 abs-cold% 10 10 extra-cold 5 10 nofreeze 1 1 0 +Ironpelt 100 1 5 1 33 xtu Tresllised Armor 3 5000 dgry dgry dur 125 125 hp 25 25 red-mag 10 16 red-dmg 15 20 ac/lvl 24 ac% 50 100 0 +Heartbane 100 1 3 1 41 xng linked mail 5 5000 invbody17 ac% 135 185 dex 15 15 enr 15 15 heal-kill 5 9 addxp 1 3 red-mag 10 15 red-dmg% 10 15 noheal 1 1 0 +Adamantine Mail 100 1 1 1 45 xng linked mail 5 5000 whit whit invrng ac/lvl 48 ac 150 200 red-dmg% 25 35 sock 3 3 allskills 1 1 0 +Spiritforge 100 1 5 1 35 xng Linked Mail 3 5000 invbody52 light 4 4 hp/lvl 10 dmg-fire 20 65 res-fire 5 5 ac% 120 160 str 15 15 sock 2 0 +Weakest Link 100 1 1 1 47 xcl tigulated mail 5 5000 invbody36 ac% 140 190 rep-dur 15 dmg% 20 30 balance2 20 20 manasteal 6 8 dmg-to-mana 8 8 allskills 1 1 0 +Panic of Thousands 100 1 3 1 43 xcl tigulated mail 5 5000 cblu cblu invscl ac% 125 150 gethit-skill 77 33 3 oskill enchant 4 4 res-fire 35 50 res-ltng 35 50 dex/lvl 4 dur 100 100 0 +Crow Caw 100 1 5 1 37 xcl Tigulated Mail 3 5000 invbody40 openwounds 35 35 ac% 150 180 dex 15 15 balance2 15 15 swing2 15 15 charged 221 3 5 0 +Drow Chainmail 100 1 3 1 44 xhn mesh armor 5 5000 blac blac ac% 150 200 ease -25 -25 extra-fire 10 15 res-all 20 30 all-stats 10 10 hp/lvl 8 mana 40 60 0 +Disparate Paths 100 1 1 1 49 xhn mesh armor 5 5000 oran oran invchn ac% 125 150 dru 1 1 skill-rand 3 221 250 skill-rand 3 221 250 skill-rand 3 221 250 swing1 10 10 res-all 20 30 0 +Shaftstop 100 1 5 1 38 xhn Mesh Armor 3 5000 dgry ac-miss 250 250 red-dmg% 30 30 hp 60 60 ac% 180 220 0 +Tesla's Cuirass 100 1 3 1 47 xrs cuirass 5 5000 dgry dgry invbody39 ac% 160 210 dmg/lvl 2 reduce-ac 10 15 move1 15 15 lifesteal 4 4 sock 2 2 0 +Mother�s Milk 100 1 1 1 51 xrs cuirass 5 5000 lgry lgry invbody58 ac% 125 150 ama 1 1 swing2 25 25 skill-rand 5 6 35 ease -25 -25 balance2 20 20 aura 113 1 6 0 +Duriel's Shell 100 1 5 1 41 xrs Cuirass 3 5000 oran oran str 15 15 ac/lvl 10 hp/lvl 8 ac% 160 200 res-fire 20 20 res-ltng 20 20 res-pois 20 20 res-cold 50 50 nofreeze 1 1 dur 100 100 0 +Brimstone Hearth 100 1 3 1 50 xpl russet armor 5 5000 dred dred ac% 170 220 res-fire 30 50 pierce-fire 20 20 dmg-fire 25 100 nofreeze 1 1 fireskill 2 2 dur 30 30 0 +Scars of the Forefathers 100 1 1 1 55 xpl russet armor 5 5000 lblu dblu invbody28 ac% 125 150 ac/lvl 20 bar 1 1 red-dmg% 15 30 skill-rand 3 126 155 dmg% 40 60 block2 20 20 att-skill 82 3 12 0 +Skullder's Ire 100 1 5 1 42 xpl Russet Armor 3 5000 invbody60 allskills 1 1 mag%/lvl 10 ac% 160 200 dur 60 60 red-mag 10 10 rep-dur 20 *vit -10 -10 *enr -10 -10 0 +Demonspike Coat 100 1 1 1 57 xlt templar coat 5 5000 invbody63 ac% 180 230 dmg-demon 75 75 demon-heal 25 25 lifesteal 6 6 regen 1 4 thorns/lvl 16 0 +Succulent Sin 100 1 3 1 52 xlt templar coat 5 5000 dpur invbody41 ac% 140 165 ass 1 1 skill-rand 4 251 280 skill-rand 3 251 280 oskill zeal 1 1 deadly 15 15 vit/lvl 4 0 +Guardian Angel 100 1 5 1 45 xlt Templar Coat 3 5000 lgry lgry light 4 4 pal 1 1 ac% 180 200 block2 30 30 res-all-max 15 15 att-dem/lvl 5 block 20 20 0 +Bloodlust Frenzy 100 1 1 1 59 xld sharktooth armor 5 5000 lgrn lgrn invbody56 ac 185 235 swing2 20 20 allskills 1 1 skilltab 13 1 1 skill 147 1 4 lifesteal 6 8 hp% 10 20 0 +Seaflame 100 1 3 1 55 xld sharktooth armor 5 5000 oran invbody59 ac% 150 175 res-all 50 60 red-mag 12 15 heal-kill 8 12 regen 10 12 regen-mana 50 50 mag% 60 60 dur 200 200 0 +Toothrow 100 1 5 1 48 xld Sharktooth Armor 3 5000 whit whit thorns 20 40 ac 40 60 str 10 10 openwounds 40 40 res-fire 15 15 ac% 160 220 dur 15 15 0 +Royal Plate 100 1 1 1 60 xth embossed plate 5 5000 invbody33 ac% 190 240 red-dmg% 15 25 res-mag 10 15 res-all 15 30 cheap 5 10 ease -15 -15 rep-dur 20 0 +Nightcrawler 100 1 3 1 56 xth embossed plate 5 5000 blac blac invgth ac 300 500 oskill fade 2 4 rip 1 1 sock 4 4 ease -35 -35 0 +Atma's Wail 100 1 5 1 50 xth Embossed Plate 3 5000 dex 15 15 regen 10 10 mana% 15 15 balance2 30 30 ac/lvl 16 dur 50 50 ac% 120 160 mag% 20 20 0 +Shambling Mound 100 1 1 1 61 xul chaos armor 5 5000 invbody65 ac/lvl 40 ac% 50 75 res-pois 40 70 dmg-pois 200 400 400 extra-pois 15 15 move1 10 10 sock 2 2 0 +Evening Sky 100 1 3 1 57 xul chaos armor 5 5000 oran invbody61 ac% 175 210 dmg-fire 75 150 res-fire 75 75 move3 30 30 cast2 20 20 mana 50 50 hp 50 50 0 +Black Hades 100 1 5 1 51 xul Chaos Armor 3 5000 dgry light -2 -2 att-demon 200 250 half-freeze 1 1 sock 3 ac% 140 200 dmg-demon 30 60 0 +Adamantine Plate 100 1 1 1 62 xar ornate armor 5 5000 whit whit invbody62 ac% 200 250 red-dmg% 30 30 res-all 50 75 rep-dur 20 0 +Iceskin 100 1 3 1 58 xar ornate armor 5 5000 cblu cblu invaar ac% 180 215 gethit-skill 60 50 8 gethit-skill 64 5 5 res-cold 100 100 extra-cold 15 20 allskills 1 1 block2 25 25 0 +Corpsemourn 100 1 5 1 52 xar Ornate Armor 3 5000 blac blac str 8 8 vit 10 10 res-cold 35 35 gethit-skill 76 6 2 ac% 150 180 dmg-fire 12 36 charged 74 40 5 0 +Plate of Fistindantalas 100 1 1 1 63 xtp mage plate 5 5000 invbody54 ac 400 600 ac% 50 75 allskills 1 1 mana/lvl 8 hp/lvl 8 addxp 1 3 sock 2 3 0 +Serendipity 100 1 3 1 59 xtp mage plate 5 5000 invbody47 ac% 185 225 ac/lvl 16 allskills 1 1 cast3 30 30 mana 75 75 mag% 30 30 light 1 3 addxp 5 5 0 +Que-Hegan's Wisdon 100 1 5 1 53 xtp Mage Plate 3 5000 cast2 20 20 mana-kill 3 3 red-mag 6 10 enr 15 15 balance2 20 20 ac% 140 160 allskills 1 1 0 +Blanket of Stars 100 1 1 1 65 uui dusk shroud 5 5000 invbody3 ac% 100 200 regen 4 8 regen-mana 50 50 enr 25 25 vit 25 25 block2 25 25 balance3 30 30 0 +Ormus' Robes 100 1 2 1 55 uui dusk shroud 3 5000 invbody34 ac 125 250 cast2 20 20 extra-fire 10 15 extra-cold 10 15 extra-ltng 10 15 regen-mana 10 15 skill-rand 3 36 60 0 +Ancient Dragon 100 1 1 1 67 uea wyrmhide 5 5000 invbody14 ac% 100 200 allskills 1 1 mana% 15 25 hp/lvl 16 stupidity 1 1 knock 2 2 sock 1 1 0 +Crescent of Secrets 100 1 2 1 57 uea Wyrmhide 5 5000 invbody11 ac% 75 100 allskills 1 1 gold% 75 75 all-stats 50 50 mag% 50 50 sock 2 2 0 +Zaratan Hide 100 1 1 1 69 ula scarab husk 5 5000 invbody1 ac% 160 200 move3 30 30 red-dmg 35 50 red-mag 20 25 res-cold 75 75 half-freeze 1 1 gold% 100 100 0 +Aprhodite's Girdle 100 1 2 1 60 ula Scarab Husk 5 5000 invbody64 ac 300 500 ama 2 2 sor 2 2 ass 2 2 move3 30 30 enr 35 35 mana 75 75 cast2 20 20 0 +Wyrmbane 100 1 1 1 73 utu wire fleece 5 5000 invbody15 ac% 120 200 allskills 1 1 red-dmg% 10 15 balance2 25 25 dmg-demon 65 65 dex 25 25 ac 150 250 0 +The Gladiator's Bane 100 1 2 1 64 utu Wire Fleece 3 5000 invbody44 ac% 150 200 red-mag 15 20 red-dmg 15 20 thorns 20 20 res-pois-len 50 50 dur 103 103 balance2 30 30 ac 50 50 nofreeze 1 1 0 +Adamantine Links 100 1 1 1 73 ung diamond mail 5 5000 invbody6 ac% 150 200 red-dmg% 25 35 allskills 1 1 all-stats 10 10 res-fire 35 55 res-ltng 35 55 mag% 40 60 0 +Feathering Mithril 100 1 2 1 65 ung Diamond Mail 5 5000 invbody9 ac% 100 200 ease -75 -75 red-dmg% 15 20 move3 40 40 rep-dur 30 nofreeze 1 1 hp% 20 20 0 +Will-O'-Wisp 100 1 1 1 74 ucl loricated mail 5 5000 whit oran ac% 100 200 allskills 1 2 move3 40 40 oskill teleport 7 7 ethereal 1 1 rep-dur 25 fade 1 1 oskill evade 1 1 0 +Golden Lotus 100 1 2 1 67 ucl Loricated Mail 5 5000 dgld dgld invbody18 ac% 100 150 gold% 300 300 mag%/lvl 12 res-all-max 5 5 att% 20 25 allskills 1 1 0 +Chains of the Abyss 100 1 2 1 68 uhn boneweave 5 5000 lred lred ac% 100 200 red-dmg 20 30 abs-ltng% 20 25 swing1 -5 -5 move1 -5 -5 cast2 25 25 mana 80 120 abs-fire% 20 30 0 +Tarrasque Hide 100 1 1 1 86 uhn Boneweave 5 5000 blac blac ac% 200 300 ac 300 500 red-dmg% 30 50 red-mag 35 50 res-fire 50 60 res-ltng 70 80 res-cold 30 50 res-pois 45 80 0 +Celestial Unicorn 100 1 2 1 70 urs great hauberk 5 5000 invbody48 ac% 100 120 allskills 2 2 regen 12 15 aura 109 4 7 mag% 40 50 gold%/lvl 21 0 +Gray God's Mantle 100 1 1 1 77 urs Great Hauberk 5 5000 blac blac invbody30 ac% 125 150 allskills 4 4 sock 3 3 0 +Diablo's Scale 100 1 1 1 78 upl balrog skin 5 5000 oran lgry invbody74 ac% 150 200 allskills 1 1 block 25 40 block3 35 35 move2 30 30 res-all 35 65 red-dmg% 20 30 0 +Arkaine's Valor 100 1 2 1 71 upl Balrog Skin 3 5000 invbody31 ac% 150 180 balance2 30 30 allskills 1 2 red-dmg 10 15 vit/lvl 4 0 +Hellshifter 100 1 1 1 80 ult hellforged plate 5 5000 dred dred invbody22 ac% 100 200 pal 2 2 skill-rand 1 96 125 skill-rand 3 96 125 skill-rand 2 96 125 dmg% 30 50 balance2 30 30 0 +Lunatic Fringe 100 1 2 1 73 ult Hellforged Plate 5 5000 dgld dgld invbody32 ac% 130 165 block 25 35 block2 25 25 howl 88 88 stupidity 3 3 hp% 15 15 0 +Ocean�s Embrace 100 1 1 1 82 uld kraken shell 5 5000 lblu dblu invbody69 ac% 100 200 regen 3 5 regen-mana 200 200 cast2 20 20 abs-cold 20 30 res-fire 60 80 extra-cold 15 20 0 +Leviathan 100 1 2 1 74 uld kraken shell 5 5000 cgrn cgrn ac% 170 200 ac 100 150 red-dmg% 15 25 str 40 50 indestruct 1 1 0 +Lachdonnan's Heart 100 1 1 1 83 uth lacquered plate 5 5000 lred lred invbody45 ac% 160 200 ease 20 20 dmg% 140 160 swing3 60 60 dmg-max 25 25 allskills 1 1 0 +Pride of the Barony 100 1 2 1 75 uth Lacquered Plate 5 5000 cblu cblu ac% 120 150 ac 200 300 swing2 25 25 dmg% 50 50 dmg-norm 15 30 balance2 30 30 addxp 25 25 0 +Shadowtrick 100 1 1 1 84 uul shadow plate 5 5000 ac% 130 200 oskill fade 3 4 oskill quickness 3 4 oskill teleport 3 4 oskill shout 3 4 oskill battle orders 3 4 allskills 2 2 aura 123 3 4 0 +Steel Carapice 100 1 2 1 77 uul shadow plate 3 5000 dgry dgry invbody29 ac% 190 220 balance2 20 20 red-dmg 9 14 res-cold 40 60 regen-mana 10 15 rep-dur 5 gethit-skill Iron Maiden 8 6 0 +Tyrael's Might 100 1 1 1 85 uar sacred armor 5 5000 dblu ease -100 -100 indestruct 1 1 ac% 120 150 rip 1 1 dmg-demon 50 100 nofreeze 1 1 move2 20 20 res-all 20 30 str 20 30 0 +Templar's Might 100 1 2 1 78 uar sacred armor 5 5000 cgrn cgrn ac% 170 220 balance2 20 20 ac-miss 250 300 stam 40 50 str 10 15 vit 10 15 skilltab 10 2 2 pal 1 1 0 +Heaven�s Treasure 100 1 1 1 87 utp archon plate 5 5000 dyel dyel invbody55 ac% 100 200 allskills 2 2 mag% 200 350 sock 2 4 0 +Legacy of the Eternals 100 1 2 1 79 utp Archon Plate 5 5000 oran oran ac% 75 100 allskills 1 2 cast2 30 30 mana% 25 25 abs-cold% 25 35 abs-fire% 25 35 abs-ltng% 25 35 res-all-max 10 10 block 20 20 0 +b23 0 +Helms 0 +Nameweaver 100 1 1 1 8 cap cap 5 5000 invhelm41 ac 20 30 dex 10 10 dmg-max 5 10 regen-mana 45 45 res-ltng 35 35 0 +Trickster�s Guise 100 1 3 1 5 cap cap 5 5000 invcap ac 15 25 enr 15 15 red-dmg% 8 8 gethit-skill 38 27 3 res-all 6 13 sock 1 1 0 +War Bonnet 0 1 5 1 3 cap Cap 5 5000 dpur hp 15 25 att 30 30 dmg% 30 30 mana 15 25 ac 14 20 0 +Liespreader 100 1 1 1 16 skp skull cap 5 5000 invhelm42 ac 30 40 cheap 15 15 skilltab 1 1 1 hp 25 25 mana 25 25 sock 1 1 0 +Devourer of Dreams 100 1 3 1 13 skp skull cap 5 5000 lgld lgld ac 15 25 cast1 15 15 oskill warmth 1 1 regen 3 5 knock 1 1 0 +Tarnhelm 0 1 5 1 10 skp Skull Cap 5 5000 oran oran gold% 75 75 mag% 25 50 allskills 1 1 0 +Barbazu's Smile 100 1 1 1 20 hlm helm 5 5000 invhelm16 ac 25 40 skilltab 3 1 1 mana% 15 15 regen-mana 40 40 mana-kill 3 5 cast1 15 15 0 +Freedom's Fa�ade 100 1 3 1 16 hlm helm 5 5000 cred invhelm24 ac 18 30 swing1 10 10 move1 10 10 dex 20 20 regen -1 -1 dmg-to-mana 10 10 0 +Coif of Glory 0 1 5 1 14 hlm Helm 5 5000 whit light-thorns 7 7 stupidity 1 1 res-ltng 15 15 ac-miss 100 100 ac 10 20 0 +Harpy's Call 100 1 1 1 23 fhl full helm 5 5000 invhelm27 ac 30 40 skilltab 6 1 1 res-all 20 20 addxp 3 3 aura 99 1 1 gethit-skill 77 7 3 0 +Equinox Visor 100 1 3 1 20 fhl full helm 5 5000 invhelm22 ac 20 33 hp 35 35 res-ltng 20 25 skill 224 1 3 oskill summon spirit wolf 3 3 mana-kill 3 5 0 +Duskdeep 0 1 5 1 17 fhl Full Helm 5 5000 lgry invhelm28 light -2 -2 res-all 15 15 red-dmg 7 7 dmg-max 8 8 ac 10 20 ac% 30 50 0 +Satyr's Speech 100 1 1 1 27 bhm bone helm 5 5000 lred lred ac 40 50 skilltab 10 1 1 rep-dur 20 att 125 125 freeze 1 2 howl 77 77 res-all 15 15 0 +Hindsight 100 1 3 1 24 bhm bone helm 5 5000 invbhm ac 25 35 cast1 15 15 balance2 20 20 mag% 20 30 oskill meditation 2 2 pal 1 1 res-mag 5 8 gethit-skill 82 10 3 0 +Wormskull 0 1 5 1 21 bhm Bone Helm 5 5000 lgrn nec 1 1 lifesteal 5 7 mana 10 25 res-pois 25 25 dmg-pois 200 102 102 0 +Fool's Crest 100 1 1 1 30 ghm great helm 5 5000 invhelm30 ac 40 50 skilltab 12 1 1 dmg% 25 25 hp 50 50 lifesteal 4 6 mag% 30 40 res-cold 25 25 0 +Morning's Tears 100 1 3 1 28 ghm great helm 5 5000 dblu invhelm11 ac 32 40 regen 5 5 hp% 10 10 mana% 10 10 allskills 1 1 gold% 50 50 res-pois-max 75 75 res-pois 40 60 0 +Howltusk 0 1 5 1 25 ghm Great Helm 5 5000 dgry dgry red-mag 2 2 thorns 3 5 ac% 80 100 dmg-to-mana 35 35 knock 1 1 howl 33 33 0 +Gaiden's Loss 100 1 3 1 30 crn crown 5 5000 dyel dyel ac 45 55 skilltab 16 1 1 mana 25 25 manasteal 3 5 ignore-ac 1 1 gold% 50 50 dmg-undead 100 100 0 +Crown of Thorns 100 1 1 1 32 crn crown 5 5000 ac 37 50 gethit-skill 235 15 5 gethit-skill 40 10 3 mana 20 30 bar 1 1 res-fire 30 30 res-cold 30 30 red-dmg 8 10 aura 103 1 2 0 +Undead Crown 0 1 5 1 27 crn Crown 5 5000 blac blac lifesteal 5 5 ac 40 40 res-pois 50 50 half-freeze 1 1 ac% 30 60 dmg-undead 50 50 att-undead 50 100 skill 69 3 3 0 +Blindman's Bluff 100 1 1 1 28 msk mask 5 5000 ac 50 65 skilltab 19 1 1 fade 1 1 res-all 20 20 dex 15 15 str 15 15 rep-dur 25 0 +Treachery�s Allure 100 1 3 1 23 msk mask 5 5000 ac% 80 120 slow 15 15 manasteal 3 5 cheap 15 15 all-stats 10 10 sock 2 2 0 +The Face of Horror 0 1 5 1 20 msk Mask 5 5000 lblu lblu invmsk howl 64 64 str 20 20 res-all 10 10 dmg-undead 50 50 ac 25 50 0 +Pepin's Blessing 100 1 1 1 36 xap war hat 5 5000 invhelm08 ac% 100 125 regen 5 8 regen-mana 75 75 addxp 3 3 allskills 1 1 mana-kill 4 4 manasteal 5 5 0 +Hushmaker 100 1 3 1 33 xap war hat 5 5000 lyel lyel invhelm40 ac% 80 120 red-dmg 8 12 red-mag 6 8 balance2 20 20 slow 10 15 sock 1 1 oskill resist lightning 1 1 0 +Peasent Crown 100 1 5 1 28 xap War Hat 3 5000 lgry enr 20 20 vit 20 20 allskills 1 1 move2 15 15 regen 6 12 ac% 100 100 0 +Drunken Fury 100 1 3 1 36 xkp sallet 5 5000 dgry dgry ac% 100 125 oskill lightning fury 14 14 dex -10 -10 dmg% 25 25 dmg-max 20 30 swing1 -10 -10 balance2 -10 -10 0 +Neonate's Sallet 100 1 1 1 40 xkp sallet 5 5000 cblu cblu invskp ac% 80 120 sor 1 2 skill-rand 2 36 65 skill-rand 2 36 65 mana 75 75 cast2 20 20 res-mag 15 15 dmg-to-mana 15 20 0 +Rockstopper 100 1 5 1 31 xkp Sallet 3 5000 invhelm07 res-ltng 20 40 red-dmg% 10 10 balance2 30 30 ac% 160 220 res-fire 20 50 res-cold 20 40 vit 15 15 0 +Helm of Ra 100 1 3 1 39 xlm casque 5 5000 invhelm26 ac% 110 135 allskills 1 1 str 15 15 extra-fire 15 15 res-fire 25 25 res-fire-max 10 10 move1 10 10 0 +Troubled Thoughts 100 1 1 1 43 xlm casque 5 5000 invhelm25 ac% 80 120 nec 1 2 skill-rand 2 66 95 skill-rand 2 66 95 rep-dur 10 heal-kill 15 20 mana-kill 8 12 str 15 15 0 +Stealskull 100 1 5 1 35 xlm Casque 3 5000 invhelm04 manasteal 5 5 lifesteal 5 5 balance2 10 10 swing2 10 10 ac% 200 240 mag% 30 50 0 +Knight's Crest 100 1 3 1 43 xhl basinet 5 5000 invhelm13 ac% 120 140 pal 1 2 block 10 10 skill-rand 2 96 125 heal-kill 10 15 cast2 20 20 slow 15 15 0 +Wraithwatch 100 1 1 1 46 xhl basinet 5 5000 invhelm12 ac% 80 120 ass 1 2 skill-rand 2 251 280 skill-rand 2 251 280 move2 25 25 swing1 10 10 sock 2 2 ease -30 -30 0 +Darksight Helm 100 1 5 1 38 xhl Basinet 3 5000 blac blac invfhl light -4 -4 ac/lvl 16 nofreeze 1 1 manasteal 5 5 gethit-skill 71 6 3 charged 264 30 5 res-fire 20 40 0 +Gotterdamerung 100 1 3 1 50 xhm winged helm 5 5000 invhelm10 ac% 90 130 res-all 35 35 red-dmg% 15 15 abs-fire 8 12 balance2 25 25 hit-skill 67 4 7 wounds/lvl 8 0 +Embracing Solitude 100 1 1 1 53 xhm winged helm 5 5000 invhelm20 ac% 80 120 dru 1 2 skill-rand 2 221 250 skill-rand 2 221 250 swing1 15 15 lifesteal 6 6 cast1 15 15 res-all 15 25 0 +Valkiry Wing 100 1 5 1 44 xhm Winged Helm 3 5000 invhelm39 ac% 150 200 move2 20 20 balance2 20 20 ama 1 2 mana-kill 2 4 0 +Usurper's Ambition 100 1 3 1 53 xrn grand crown 5 5000 invhelm36 ac% 130 150 allskills 2 2 sock 3 3 addxp 3 3 mag% 25 25 0 +Reign of Deceit 100 1 1 1 56 xrn grand crown 5 5000 oran oran invcrn ac% 80 120 bar 1 2 skill-rand 2 126 155 skill-rand 2 126 155 dmg% 20 25 dmg-norm 5 10 block2 20 20 hp 25 50 0 +Crown of Thieves 100 1 5 1 49 xrn Grand Crown 3 5000 dgld dgld dex 25 25 lifesteal 9 12 hp 50 50 mana 35 35 res-fire 33 33 ac% 160 200 gold% 80 100 0 +Wraithshadow 100 1 3 1 44 xsk death mask 5 5000 dred dred invhelm15 ac% 135 155 fade 1 1 move2 20 20 swing2 20 20 gold% 35 35 mag% 20 20 vit 20 20 0 +Archnemesis 100 1 1 1 48 xsk death mask 5 5000 invhelm1 ac% 80 120 ama 1 2 skill-rand 2 6 35 skill-rand 2 6 35 swing1 15 15 move1 15 15 dmg-cold 100 25 50 mag% 20 30 0 +Blackhorn's Face 100 1 5 1 41 xsk Death Mask 3 5000 invhelm17 light-thorns 25 25 slow 20 20 noheal 1 1 abs-ltng 20 20 res-ltng 15 15 ac% 180 220 0 +Ghoulslayer 100 1 3 1 46 xh9 grim helm 5 5000 cblu cblu ac% 140 165 rip 1 1 allskills 1 1 dmg-undead 100 100 str 15 25 vit 15 25 red-dmg% 5 10 sock 1 1 0 +Skull of Fistindantalas 100 1 1 1 49 xh9 grim helm 5 5000 dgrn dgrn ac% 80 120 pal 1 2 skill-rand 2 96 125 skill-rand 2 96 125 rip 1 1 cast1 15 15 mana 50 50 dex 15 15 0 +Vampiregaze 100 1 5 1 42 xh9 Grim Helm 3 5000 cgrn cgrn manasteal 6 8 lifesteal 6 8 stamdrain 15 15 red-dmg% 15 20 red-mag 10 15 ac% 100 100 dmg-cold 100 6 22 0 +Bane's Dark Wisdom 100 1 1 1 55 uap shako 5 5000 invhelm38 ac 100 125 allskills 1 1 addxp 50 50 0 +Harlequin Crest 100 1 2 1 61 uap Shako 3 5000 cgrn cgrn allskills 2 2 hp/lvl 12 mana/lvl 12 mag% 50 50 red-dmg% 10 10 str 2 2 dex 2 2 vit 2 2 enr 2 2 0 +Overlord's Helm 100 1 1 1 67 ukp hydraskull 5 5000 lblu lblu allskills 4 4 sock 2 2 ac% 100 120 cast3 30 30 mana/lvl 8 0 +Slaver's Price 100 1 2 1 58 ukp Hydraskull 5 5000 invhelm09 ac 80 120 regen -15 -15 dmg% 50 50 dmg-max 15 15 swing2 20 20 rip 1 1 0 +Steelshade 100 1 2 1 62 ulm armet 5 5000 dgry dgry invhelm23 ac% 100 130 abs-fire 5 11 manasteal 4 8 regen 10 18 red-dmg% 10 10 res-all 25 25 sock 1 1 0 +Darkfear 100 1 1 1 73 ulm armet 5 5000 blac invhelm05 ac% 150 200 allskills 1 1 stupidity 2 2 oskill terror 2 5 hp 75 75 str 15 15 dex 15 15 res-pois 25 25 0 +Conch of Dismay 100 1 1 1 74 uhl giant conch 5 5000 invhelm21 hp 300 350 mana 125 150 mag% 50 50 ac/lvl 16 allskills 1 1 half-freeze 1 1 res-pois-len 60 70 0 +Judas Kiss 100 1 2 1 66 uhl Giant Conch 5 5000 blac blac invhelm19 ac 200 300 heal-kill 15 15 ignore-ac 1 1 res-pois-len 50 50 nofreeze 1 1 move2 20 20 res-all 15 25 0 +Veil of Steel 100 1 2 1 75 uhm Spired Helm 3 5000 invhelm10 res-all 50 50 ac% 60 60 str 15 15 vit 15 15 light -4 -4 dur 20 20 ac 140 140 *mana -30 -30 0 +Nightwing's Veil 100 1 1 1 86 uhm spired helm 3 5000 invhelm29 ac% 90 120 allskills 2 2 dex 10 20 abs-cold 5 9 half-freeze 1 1 extra-cold 8 15 ease -50 -50 0 +King Conan's Rule 100 1 1 1 84 urn corona 5 5000 ac% 150 200 allskills 2 2 addxp 2 4 gold% 200 200 dmg-norm 10 30 res-all 25 25 str/lvl 8 dex 35 35 0 +Crown of Ages 100 1 2 1 78 urn corona 3 5000 dgld dgld balance2 30 30 res-all 20 30 allskills 1 1 ac 100 150 indestruct 1 1 red-dmg% 10 15 ac% 50 50 sock 1 2 0 +Lolith's Crest 100 1 1 1 77 usk demonhead 5 5000 invhelm18 allskills 1 1 ac% 150 200 heal-kill 10 15 block 15 20 balance2 30 30 cast2 20 20 dmg-to-mana 15 15 0 +Andariel's Visage 100 1 2 1 72 usk demonhead 3 5000 dred dred ac% 100 150 res-pois 70 70 allskills 2 2 res-pois-max 10 10 swing2 20 20 str 25 30 gethit-skill 92 15 15 charged 278 20 3 lifesteal 8 10 res-fire -30 -30 0 +Kiss of the Vampire 100 1 1 1 81 uh9 bone visage 5 5000 dred dred ac 200 300 allskills 1 1 hp 50 75 mana 50 75 regen 10 15 regen-mana 75 75 manasteal 8 10 lifesteal 8 10 mana-kill 5 7 heal-kill 10 15 0 +Giantskull 100 1 2 1 72 uh9 bone visage 3 5000 whit whit invbhm ac 250 320 str 25 35 crush 50 50 sock 2 2 knock 1 1 hp 100 100 0 +b24 0 +Circlets 0 +Fairweather 100 1 1 1 32 ci0 circlet 5 5000 cblu cblu ac% 140 175 abs-fire 10 15 abs-ltng 10 15 abs-cold 10 15 res-pois-len 65 90 half-freeze 1 1 sock 1 1 allskills 1 1 0 +Muddled Thoughts 100 1 3 1 25 ci0 circlet 5 5000 cred cred invhelm03 ac% 100 120 cast2 25 25 enr 15 15 sor 1 1 res-all 15 25 hp 35 50 dmg-to-mana 10 15 0 +Charon's Token 100 1 5 1 18 ci0 Circlet 5 5000 invhelm06 ac 20 30 move1 15 15 block1 15 15 balance1 15 15 cast1 15 15 swing1 15 15 all-stats 15 15 res-all 15 15 0 +Gainsayer 100 1 1 1 54 ci1 coronet 5 5000 dpur dpur ac% 140 175 addxp 4 7 cheap 5 10 all-stats 15 20 res-all 20 30 balance2 25 25 skilltab 14 1 3 skilltab 6 1 3 0 +Threatspeaker 100 1 3 1 47 ci1 coronet 5 5000 cblu cblu ac 50 75 dmg-min 5 10 dmg-max 20 25 swing1 10 10 oskill war cry 10 10 oskill conversion 1 6 res-all 15 20 0 +Sorcerer's Cache 100 1 5 1 39 ci1 Coronet 5 5000 invhelm02 ac% 100 135 allskills 1 1 cast2 30 30 mana 50 50 extra-fire 10 15 extra-cold 10 15 extra-ltng 10 15 0 +Gillian's Hairpin 100 1 1 1 72 ci2 tiara 5 5000 ac 75 150 block 15 25 block2 20 20 slow 25 25 oskill teleport 2 4 regen 8 8 0 +Mystic Angel 100 1 3 1 68 ci2 tiara 5 5000 ac% 100 140 oskill valkyrie 1 1 mana% 20 20 res-ltng 20 35 res-fire 25 35 red-dmg% 10 15 0 +Kira's Guardian 100 1 5 1 60 ci2 tiara 3 5000 blac blac ac 50 120 res-all 50 70 nofreeze 1 1 balance2 20 20 red-dmg% 25 25 sock 1 1 0 +Blindsight 100 1 1 1 86 ci3 diadam 5 5000 ac/lvl 20 allskills 2 2 stupidity 3 3 att% 35 35 dex 20 20 res-ltng 65 65 gethit-skill 28 12 5 0 +Royal Circlet 100 1 3 1 83 ci3 diadam 5 5000 invhelm32 ac% 130 175 ac/lvl 10 allskills 1 1 gold% 250 300 str 25 25 dex 15 15 gethit-skill 53 7 4 0 +Griffon's Eye 100 1 5 1 76 ci3 diadem 5 5000 ac 100 200 cast2 25 25 allskills 1 1 extra-ltng 10 15 pierce-ltng 15 20 0 +b25 0 +Shields 0 +Lightreaper 100 1 1 1 8 buc buckler 5 5000 invshld2 ac 20 35 block 15 25 dmg-ltng 10 20 balance2 25 25 cast2 20 20 vit 5 5 0 +Traitor�s Mark 100 1 3 1 5 buc buckler 5 5000 invshld3 ac 15 30 block 10 20 block2 15 15 lifesteal 3 4 swing1 10 10 dmg% 25 25 0 +Pelta Lunata 0 1 5 1 2 buc Buckler 5 5000 ac 30 30 vit 10 10 str 5 5 enr 10 10 ac% 30 40 block 20 20 block2 40 40 dur 8 12 0 +Golgomere's Shield 100 1 1 1 14 sml small shield 5 5000 invshld35 ac% 100 120 block2 20 20 heal-kill 5 8 regen-mana 50 50 extra-cold 10 10 res-cold 25 25 hp 20 20 0 +Skein of Deceit 100 1 3 1 11 sml small shield 5 5000 cred cred invshld34 ac 25 35 block 15 20 block2 20 20 skill 36 1 3 skill 38 1 3 skill 39 1 3 mana 25 25 0 +Umbral Disk 0 1 5 1 7 sml Small Shield 5 5000 lgry lgry stupidity 1 1 dex 10 10 ac 30 40 hp 20 30 light -2 -2 ac% 40 50 block 30 30 dur 10 15 0 +Crest of the Horned Society 100 1 1 1 19 lrg large shield 5 5000 invshld11 ac% 80 120 block 20 30 swing2 15 15 thorns 8 12 noheal 1 1 manasteal 4 4 0 +Lepertouch 100 1 3 1 16 lrg large shield 5 5000 cgrn cgrn invlrg ac% 100 125 block 15 25 block2 20 20 res-pois-len 50 50 res-pois 75 75 dmg-pois 100 100 100 oskill poison dagger 1 1 0 +Stormguild 0 1 5 1 13 lrg Large Shield 5 5000 dgry dgry invshld5 red-mag 1 1 res-ltng 25 25 ac 30 40 dmg-ltng 1 6 ac% 50 60 block 30 30 light-thorns 3 3 dur 10 15 0 +Doomhedge 100 1 1 1 28 bsh bone shield 5 5000 invshld22 ac 25 50 block2 30 30 balance2 15 15 dmg-max 8 8 gethit-skill 66 13 1 deadly 10 10 res-all 15 15 0 +Cacophony 100 1 3 1 24 bsh bone shield 5 5000 dyel dyel invbsh ac% 100 125 block 20 25 block2 20 20 gethit-skill 77 20 2 howl 35 35 res-all 10 15 sock 1 2 0 +Wall of the Eyeless 0 1 5 1 20 bsh Bone Shield 5 5000 lgld mana-kill 5 5 manasteal 3 5 cast2 20 20 res-pois 20 20 ac% 30 40 ac 10 20 nec 1 1 0 +Fanged Shield 100 1 1 1 22 spk spiked shield 5 5000 invshld38 ac% 100 125 thorns/lvl 8 block2 20 20 allskills 1 1 ignore-ac 1 1 mana 25 35 openwounds 30 80 0 +Debtfinisher 100 1 3 1 18 spk spiked shield 5 5000 lyel lyel invspk ac% 110 135 block 20 30 block2 25 25 oskill zeal 1 5 pal 1 1 skill 96 1 3 skill 104 1 3 ac 30 40 0 +Swordback Hold 0 1 5 1 15 spk Spiked Shield 5 5000 lred thorns 5 10 block 20 20 openwounds 50 50 bloody 3 5 ac% 30 60 thorns 5 5 ac 10 30 0 +Crest of Avalon 100 1 1 1 24 kit kite shield 5 5000 invshld1 ac% 120 140 block 60 70 block3 40 40 red-dmg% 50 50 0 +Killhunger 100 1 3 1 21 kit kite shield 5 5000 invshld7 ac% 115 140 block 25 30 block2 30 30 lifesteal 3 5 heal-kill 6 6 nec 1 1 skill 69 1 3 skill 80 1 3 0 +Steelclash 0 1 5 1 17 kit Kite Shield 5 5000 invshld6 block 25 25 pal 1 1 red-dmg 3 3 light 3 3 ac% 60 100 block2 20 20 res-all 15 25 ac 20 30 dur 15 20 0 +Bigby's Crushing Fist 100 1 3 1 25 tow tower shield 5 5000 invshld8 ac 40 70 crush/lvl 5 dmg% 20 30 allskills 1 1 sock 2 3 0 +Knight's Dawn 100 1 1 1 29 tow tower shield 5 5000 ac% 120 150 block 25 35 block2 30 30 bar 1 1 oskill holy shield 1 1 oskill vengeance 1 3 oskill concentration 1 2 sock 2 3 0 +Bverrit Keep 0 1 5 1 23 tow Tower Shield 5 5000 invshld13 ac 30 50 res-fire 75 75 str 5 5 red-mag 5 10 ac% 80 120 block 10 10 dur 80 100 0 +Pathfinder 100 1 1 1 31 gts gothic shield 5 5000 invshld15 ac% 100 125 ac/lvl 8 str/lvl 3 dex/lvl 3 vit/lvl 3 enr/lvl 3 move3 30 30 0 +Shield of Osirus 100 1 3 1 29 gts gothic shield 5 5000 oran oran invshld27 ac% 125 150 block 30 40 block3 40 40 allskills 1 1 res-all 25 25 balance2 30 30 aura 109 6 6 regen-mana 35 35 regen 2 5 0 +The Ward 0 1 5 1 26 gts Gothic Shield 5 5000 blac ac 40 60 red-mag 2 5 str 10 10 block 10 20 ac% 100 100 res-all 30 50 0 +Yemista's Defender 100 1 1 1 37 xuc defender 5 5000 blac invshld29 ac 70 100 nec 1 1 cast1 15 15 hp 100 100 regen 3 5 mana-kill 3 5 0 +Mongolian Trust 100 1 3 1 34 xuc defender 5 5000 oran oran invbuc ac% 80 130 ass 1 1 skill-rand 2 251 280 skill-rand 2 251 280 block 15 30 lifesteal 3 5 stupidity 1 1 reduce-ac 10 10 0 +Visceratuant 100 1 5 1 28 xuc Defender 3 5000 invshld37 sor 1 1 block2 30 30 block 30 30 ac% 100 150 light-thorns 10 10 0 +Doom's Mirror 100 1 1 1 39 xml round shield 5 5000 invshld30 block 30 40 block2 25 25 ac/lvl 12 abs-fire 15 15 abs-ltng 15 15 abs-cold 15 15 stupidity 2 2 sock 2 2 0 +Undead Buckler 100 1 3 1 35 xml round shield 5 5000 dgry dgry invsml ac% 80 130 dru 1 1 skill-rand 2 221 250 skill-rand 2 221 250 block 15 30 oskill skeleton mastery 5 5 oskill revive 1 1 dmg-to-mana 10 10 0 +Mosers Blessed Circle 100 1 5 1 31 xml Round Shield 3 5000 res-all 25 25 block 25 25 sock 2 ac% 180 220 block2 30 30 0 +Savant Sin 100 1 1 1 42 xrg scutum 5 5000 invshld16 ac% 120 150 pal 1 1 oskill holy shield 1 1 balance2 20 20 dmg-norm 5 15 swing1 10 10 res-ltng 35 35 0 +Spellbreaker 100 1 3 1 39 xrg scutum 5 5000 invshld9 ac% 80 130 sor 1 1 skill-rand 2 36 65 skill-rand 2 36 65 block 15 30 cast1 15 15 mana-kill 4 7 mana% 10 10 0 +Stormchaser 100 1 5 1 35 xrg Scutum 3 5000 cblu cblu dmg-ltng 1 60 block 20 20 half-freeze 1 1 res-ltng 50 50 att 150 150 ac% 160 220 block2 10 10 gethit-skill 59 4 6 gethit-skill 245 4 5 0 +Under Dragon's Wing 100 1 1 1 48 xit dragon shield 5 5000 blac invshld17 dmg% 50 50 ac 75 100 block2 50 50 red-dmg% 10 15 res-mag 10 15 res-all 35 35 dmg-fire 10 77 0 +Wishgranter 100 1 3 1 44 xit dragon shield 5 5000 invshld18 ac% 80 130 bar 1 1 skill-rand 2 126 155 skill-rand 2 126 155 block 15 30 dmg-max 10 15 hp% 10 10 att 200 200 0 +Tiamat's Rebuke 100 1 5 1 38 xit Dragon Shield 3 5000 lgry lgry dmg-cold 150 27 53 dmg-fire 35 95 dmg-ltng 1 120 res-all 25 35 ac% 140 200 gethit-skill 44 5 9 gethit-skill 48 5 7 gethit-skill 62 3 6 dur 40 40 0 +Adamantine Shield 100 1 1 1 53 xow pavise 5 5000 invshld21 ac/lvl 20 red-dmg% 20 20 indestruct 1 1 allskills 1 1 red-mag 25 25 str 15 15 ease 10 10 sock 1 1 0 +Shieldmaiden's Shield 100 1 3 1 49 xow pavise 5 5000 invshld19 ac% 80 130 ama 1 1 skill-rand 2 6 35 skill-rand 2 6 35 block 15 30 oskill spear mastery 1 1 red-dmg% 10 15 manasteal 5 5 0 +Kerke's Sanctuary 100 1 5 1 44 xow Pavise 3 5000 invshld20 red-dmg 11 16 red-mag 14 18 regen 15 15 ac% 180 240 dur 100 100 block 30 30 res-all 20 30 0 +Dreadwall 100 1 3 1 55 xts ancient sheild 5 5000 invshld36 ac% 140 170 block 25 35 gethit-skill 77 100 1 thorns/lvl 8 mag% 25 25 mana-kill 2 4 addxp 3 5 0 +Crusader's Wall 100 1 1 1 59 xts ancient shield 5 5000 invshld24 ac% 80 130 pal 1 1 skill-rand 2 96 125 skill-rand 2 96 125 block 15 30 res-all 25 35 red-dmg% 10 10 block2 20 20 0 +Radimant's Sphere 100 1 5 1 50 xts Ancient Shield 3 5000 dmg-pois 100 204 204 gethit-skill 92 5 5 res-pois 75 75 ac% 160 200 block 20 20 block2 20 20 charged 83 40 6 dur 20 20 0 +Ghoulspawner 100 1 1 1 48 xsh grim shield 5 5000 invshld41 ac% 100 125 reanimate 10 10 15 charged 95 10 10 dmg-undead 75 75 att% 20 20 lifesteal 6 6 res-all 30 40 rep-dur 12 0 +Lichward 100 1 3 1 45 xsh grim shield 5 5000 blac blac invbsh ac% 80 130 nec 1 1 skill-rand 2 66 95 skill-rand 2 66 95 block 15 30 mana 35 35 aura 119 1 4 pierce-pois 15 15 0 +Lidless Wall 100 1 5 1 41 xsh Grim Shield 3 5000 dgld dgld light 1 1 allskills 1 1 cast2 20 20 mana-kill 3 5 ac% 80 130 enr 10 10 mana% 10 10 0 +Raptarfang 100 1 1 1 46 xpk barbed shield 5 5000 ac/lvl 10 balance2 25 25 block 20 40 gold% 100 100 dmg-max 15 30 red-dmg 15 20 sock 1 1 0 +Razorbite Deflecter 100 1 3 1 41 xpk barbed sheild 5 5000 oran oran invspk ac% 80 130 allskills 1 1 openwounds 33 33 dmg-norm 5 15 dmg% 25 40 red-dmg 10 15 red-mag 10 15 block 25 45 0 +Lance Guard 100 1 5 1 35 xpk Barbed Shield 3 5000 hp 50 50 balance2 30 30 dmg-to-mana 15 15 thorns 47 47 ac% 70 120 deadly 20 20 0 +Chill of Winter 100 1 1 1 58 uuc heater 5 5000 invshld39 ac% 150 200 dmg-cold 100 200 freeze 4 4 extra-cold 15 25 abs-cold% 15 15 res-cold 75 100 pierce-cold 60 60 0 +Sun Tormenter 100 1 2 1 53 uuc Heater 5 5000 invshld23 ac/lvl 20 res-fire 50 50 abs-fire% 25 25 dmg-fire 75 200 pierce-fire 25 25 extra-fire 20 25 res-fire-max 20 20 block 20 30 block2 30 30 0 +Solar Eclipse 100 1 1 1 67 uml luna 5 5000 invshld28 ac% 100 150 lifesteal 8 8 stupidity 2 2 slow 10 10 block 30 30 allskills 1 1 res-all 20 20 regen-mana 25 25 0 +Blackoak Shield 100 1 2 1 61 uml Luna 3 5000 dex/lvl 4 ac% 160 200 abs-cold/lvl 5 gethit-skill 72 4 5 dur 45 45 hp/lvl 10 block2 50 50 half-freeze 1 1 0 +Shield of Myth 100 1 1 1 70 urg hyperion 5 5000 invshld31 ac% 150 200 block 25 35 block2 20 20 balance3 40 40 hp 150 150 mag% 75 75 0 +Safewarden 100 1 2 1 65 urg Hyperion 5 5000 invshld33 ac% 100 200 red-dmg% 15 20 abs-mag% 25 25 res-all 20 30 block 75 75 block3 60 60 0 +Fortress of Solitude 100 1 1 1 78 uit monarch 5 5000 dgrn dgrn invshld4 ac% 200 225 block 35 50 balance2 35 35 enr 40 40 addxp 3 5 red-dmg% 15 25 res-mag 15 25 sock 1 2 0 +Stormshield 100 1 2 1 73 uit Monarch 3 5000 ac/lvl 30 red-dmg% 35 35 str 30 30 indestruct 1 1 block2 35 35 res-ltng 25 25 block 25 25 res-cold 60 60 light-thorns 10 10 0 +Lachdonnan's Guard 100 1 1 1 82 uow aegis 5 5000 invshld12 ac% 100 120 ac/lvl 16 allskills 3 3 ease 50 50 block 50 75 balance1 15 15 abs-fire% 30 30 0 +Medusa's Gaze1 100 1 2 1 76 uow aegis 3 5000 lred lred ac% 150 180 slow 20 20 gethit-skill Lower Resist 10 7 lifesteal 5 9 death-skill Nova 100 44 res-cold 40 80 0 +Braced for Battle 100 1 1 1 86 uts ward 5 5000 invshld10 ac% 100 150 ease -70 -70 block2 30 30 balance3 60 60 swing2 20 20 sock 2 2 0 +Spirit Ward 100 1 2 1 80 uts ward 3 5000 dyel dyel invshld14 ac% 130 180 abs-cold 6 11 res-all 30 40 block 20 30 block2 25 25 gethit-skill Fade 5 8 0 +Cry of the Wretched 100 1 1 1 80 ush troll nest 5 5000 lgrn lgrn invbsh gethit-skill 91 15 3 ac% 200 240 block 25 35 cast2 25 25 nec 1 2 mana 50 50 manasteal 5 5 0 +Headhunter's Glory 100 1 2 1 75 ush troll nest 3 5000 invshld40 ac 320 420 ac-miss 300 350 res-pois 30 40 sock 1 3 res-fire 20 30 heal-kill 5 7 0 +Baal's Wing 100 1 1 1 74 upk blade barrier 5 5000 ac 200 300 block 35 50 block1 15 15 move2 20 20 oskill bone wall 5 5 hit-skill 53 33 8 noheal 1 1 openwounds 77 77 0 +Spike Thorn 100 1 2 1 68 upk blade barrier 5 5000 dyel dyel ac% 120 150 thorns/lvl 11 dur 250 250 balance2 30 30 red-dmg% 15 20 sock 1 1 0 +b26 0 +Gloves 0 +Blisterpain 100 1 1 1 11 lgl gloves 5 5000 invglov5 ac 30 40 dmg-max 5 5 dmg-fire 12 18 res-fire 45 45 swing1 15 15 noheal 1 1 0 +Goblin Touch 100 1 3 1 8 lgl gloves 5 5000 lgrn lgrn invglov10 ac 10 20 res-pois 15 25 dmg-demon 100 100 rep-dur 10 att 75 75 demon-heal 10 15 0 +The Hand of Broc 0 1 5 1 4 lgl Gloves 5 5000 cblu cblu manasteal 3 3 lifesteal 3 3 res-pois 10 10 mana 20 20 ac 10 10 ac% 50 100 0 +Healing Touch 100 1 1 1 18 vgl heavy gloves 5 5000 ac 35 45 lifesteal 6 8 regen 3 5 heal-kill 4 7 mana-kill 4 6 regen-mana 35 35 0 +Fiendfeast 100 1 3 1 14 vgl heavy gloves 5 5000 invglov21 ac 20 25 dmg-demon 50 50 dmg-undead 50 50 manasteal 4 6 mag% 15 15 gold% 25 25 red-dmg% 5 10 0 +Bloodfist 0 1 5 1 9 vgl Heavy Gloves 5 5000 oran oran dmg-min 5 5 hp 40 40 balance2 30 30 ac 10 10 ac% 50 75 swing2 10 10 0 +Horseman's Gloves 100 1 3 1 20 mgl bracers 5 5000 ac 40 50 red-dmg% 7 10 swing1 10 10 skilltab 10 1 3 res-pois 15 25 hp 30 30 0 +Green God's Bracers 100 1 1 1 25 mgl bracers 5 5000 dgrn dgrn ac% 100 115 swing1 15 15 skilltab 1 1 3 dmg-ltng 1 35 res-all 10 20 hp 25 35 0 +Chance Guards 0 1 5 1 15 mgl Bracers 5 5000 lred lred gold% 200 200 mag% 25 40 att 25 25 ac 15 15 light 2 2 ac% 50 100 0 +Prismatic Gauntlets 100 1 1 1 30 tgl light gauntlets 5 5000 oran oran invglov2 ac 45 55 res-all 20 30 dmg-elem 150 15 30 swing2 20 20 dmg-min 3 5 all-stats 5 5 0 +Skein of Pain 100 1 3 1 27 tgl light gauntlets 5 5000 invglov11 ac% 120 140 dmg-norm 3 12 swing1 10 10 mana 20 20 res-fire 15 25 res-ltng 20 40 skilltab 13 1 3 0 +Magefist 0 1 5 1 23 tgl Light Gauntlets 5 5000 invglov13 cast3 20 20 regen-mana 25 25 fireskill 1 1 dmg-fire 1 6 ac 10 10 ac% 50 100 0 +Viridian Gloves 100 1 1 1 33 hgl gauntlets 5 5000 invglov20 ac 50 60 ease -25 -25 dex 15 15 att 75 75 slow 15 15 res-fire 25 25 cast1 10 10 0 +Firesign 100 1 3 1 31 hgl gauntlets 5 5000 dred dred ac% 125 150 dmg-fire 20 35 res-fire 35 45 res-fire-max 5 5 abs-fire% 5 5 half-freeze 1 1 light 2 4 fireskill 1 1 0 +Frostburn 0 1 5 1 28 hgl Gauntlets 5 5000 cblu cblu ac 30 30 dmg% 5 5 mana% 40 40 dmg-cold 50 1 6 ac% 50 100 0 +Devil�s Invocation 100 1 1 1 37 xlg demonhide gloves 5 5000 invglov17 ac% 100 125 cast2 20 20 mana 30 50 dmg-to-mana 10 15 regen-mana 50 50 mana-kill 3 5 manasteal 5 5 0 +Marauder's Claw 100 1 3 1 34 xlg demonhide gloves 5 5000 invglov4 ac% 100 125 dmg-min 6 10 reduce-ac 5 5 crush 5 5 gold% 100 100 res-cold 20 30 res-fire 20 30 0 +Venom Grip 100 1 5 1 29 xlg Demonhide Gloves 5 5000 dgrn res-pois 30 30 res-pois-max 5 5 dmg-pois 100 153 153 crush 5 5 lifesteal 5 5 ac 15 25 ac% 130 160 0 +Lady of the Lake 100 1 1 1 43 xvg sharkskin gloves 5 5000 dblu dblu ac% 120 140 sor 1 1 ama 1 1 ass 1 1 mana 40 50 cast2 15 15 swing1 15 15 0 +Conspiracy of Thieves 100 1 3 1 39 xvg sharkskin gloves 5 5000 invglov1 ac% 125 160 dex 25 25 mag% 20 30 abs-fire 10 15 abs-cold 10 15 abs-ltng 10 15 ac 20 40 0 +Gravepalm 100 1 5 1 32 xvg Sharkskin Gloves 5 5000 invglov8 enr 10 10 str 10 10 dmg-undead 100 200 att-undead 100 200 ac% 140 180 0 +Spikefiend Bracers 100 1 1 1 48 xmg heavy bracers 5 5000 dgry dgry ac% 140 160 bar 1 1 pal 1 1 dmg% 20 20 hp 25 35 lifesteal 5 5 0 +Rapturous Blessings 100 1 3 1 44 xmg heavy bracers 5 5000 invglov15 ac 75 125 oskill salvation 1 1 cast1 10 10 res-all-max 5 5 res-all 10 15 enr 15 15 vit 15 15 0 +Ghoulhide 100 1 5 1 36 xmg Heavy Bracers 5 5000 att-und/lvl 16 dmg-und/lvl 16 manasteal 4 5 hp 20 20 ac% 150 190 0 +Warlock's Touch 100 1 1 1 54 xtg battle gauntlets 5 5000 invglov7 ac% 125 150 nec 1 1 dru 1 1 cast2 20 20 mana-kill 5 5 mana 25 40 regen-mana 50 50 0 +Gryphon's Claw 100 1 3 1 50 xtg battle gauntlets 5 5000 invglov12 ac% 130 160 dmg-max 6 10 swing1 10 10 skilltab 15 1 3 heal-kill 5 5 res-pois 25 25 res-cold 20 30 0 +Lavagout 100 1 5 1 42 xtg Battle Guantlets 5 5000 invglov14 res-fire 24 24 half-freeze 1 1 hit-skill 52 2 10 swing2 20 20 ac% 150 200 dmg-fire 13 46 dur 20 20 0 +Burning Soul 100 1 1 1 60 xhg war gauntlets 5 5000 invglov16 ac 100 150 dmg-fire 77 122 res-fire 35 35 abs-fire% 10 15 extra-fire 10 15 pierce-fire 10 15 0 +Threat of Retribution 100 1 3 1 55 xhg war gauntlets 5 5000 lgrn lgrn ac/lvl 16 ac% 50 75 charged 8 77 10 gethit-skill 42 3 1 hit-skill 39 3 10 balance1 15 15 move1 15 15 0 +Hellmouth 100 1 5 1 47 xhg War Gauntlets 5 5000 dmg-fire 15 72 abs-fire 15 15 ac% 150 200 dur 15 15 hit-skill 56 2 4 hit-skill 225 4 12 0 +Mephisto's Claw 100 1 1 1 64 ulg bramble mitts 5 5000 oran oran ac% 125 150 aura 114 4 6 dmg-cold 70 100 freeze 2 2 mag% 35 50 0 +Griefspawn 100 1 2 1 57 ulg Bramble Mitts 5 5000 invglov18 ac% 75 100 dmg-norm 10 20 swing3 40 40 rip 1 1 hp% 10 10 0 +Bloodyearn 100 1 1 1 71 uvg vampirebone gloves 5 5000 cred cred invglov6 ac% 120 150 hp% 25 25 hp/lvl 6 lifesteal 6 6 swing2 20 20 skilltab 13 1 3 0 +Dracul's Grasp 100 1 2 1 65 uvg vampirebone gloves 5 5000 dred dred ac% 90 120 lifesteal 7 10 openwounds 25 25 hit-skill Life Tap 5 10 heal-kill 5 10 str 10 15 0 +Lachdonnan's Bracers 100 1 1 1 80 umg vambraces 5 5000 ac% 160 200 ease 40 40 skilltab 9 1 3 dmg-norm 10 20 dmg-demon 100 100 res-fire 25 35 mag% 35 50 0 +Souldrain 100 1 2 1 74 umg vambraces 5 5000 dred dred ac% 90 120 manasteal 4 7 lifesteal 4 7 hit-skill Weaken 8 3 dmg-ac -50 -50 0 +Paladin's Quest 100 1 1 1 84 utg crusader gauntlets 5 5000 invglov9 allskills 1 1 addxp 1 1 skill 117 1 3 skill 122 1 3 block 20 20 block2 20 20 0 +Hellhunger 100 1 2 1 78 utg Crusader Gauntlets 5 5000 invglov3 ac 75 125 allskills 1 1 oskill lower resist 1 5 cast1 10 10 mag% 20 25 0 +Black Lotus 100 1 1 1 87 uhg ogre gauntlets 5 5000 invglov22 ac/lvl 24 swing3 30 30 dex 25 25 deadly 25 25 pierce 20 20 res-all 15 20 0 +Steelrend 100 1 2 1 83 uhg ogre gauntlets 3 5000 invglov19 ac 170 210 str 15 20 dmg% 45 60 crush 10 15 swing1 15 15 dmg-norm 5 10 0 +b27 0 +Boots 0 +Cheetah Speed 100 1 1 1 10 lbt leather boots 5 5000 invboot19 ac 20 30 move1 30 30 balance2 30 30 res-all 10 15 heal-kill 5 5 0 +Marathon Slipper 100 1 3 1 7 lbt leather boots 5 5000 invboot25 ac 15 25 move2 25 25 balance2 20 20 dex 15 15 res-ltng 20 30 gold% 35 35 0 +Hotspur 0 1 5 1 5 lbt Leather Boots 5 5000 cred cred res-fire-max 15 15 hp 15 20 dmg-fire 3 6 ac 6 12 res-fire 45 45 ac% 35 60 0 +Gillian's Boots 100 1 1 1 16 vbt heavy boots 5 5000 invboot4 ac 25 35 move2 15 15 kick 3 3 gold% 50 50 skilltab 19 1 1 str 10 10 0 +Angel's Tread 100 1 3 1 13 vbt heavy boots 5 5000 whit whit ac 25 35 skilltab 11 1 3 skill 99 1 3 balance2 25 25 res-all 15 20 mag% 15 25 res-fire 25 35 0 +Gorefoot 0 1 5 1 9 vbt Heavy Boots 5 5000 dblu dblu bloody 3 5 move2 20 20 manasteal 2 2 thorns 2 2 ac 12 15 ac% 40 70 skill 132 2 2 0 +Pepin's Grace 100 1 1 1 23 mbt chain boots 5 5000 invboot23 ac 30 40 move2 20 20 regen 2 5 oskill dodge 1 1 thorns 6 9 dex 10 15 0 +Bonemesh 100 1 3 1 19 mbt chain boots 5 5000 invboot3 ac% 100 125 skilltab 8 1 2 oskill skeleton mastery 5 5 oskill raise skeleton 1 1 dmg-undead 75 75 regen 3 3 0 +Treads of Cthon 0 1 5 1 15 mbt Chain Boots 5 5000 lgrn lgrn move3 30 30 ac-miss 50 50 stamdrain 50 50 hp 10 10 ac 12 18 ac% 50 75 0 +Dawn Scion 100 1 1 1 30 tbt light plated boots 5 5000 invboot21 ac 35 45 skilltab 11 1 2 str 5 15 addxp 1 2 balance2 15 15 mag% 20 30 light 1 3 0 +Lilith's Heels 100 1 3 1 26 tbt light plate boots 5 5000 invboot5 ac% 115 140 dmg-to-mana 5 10 skill 54 1 2 skill 37 1 1 mana 25 25 move2 20 20 res-cold 25 40 0 +Goblin Toe 0 1 5 1 22 tbt Light Plate Boots 5 5000 crush 25 25 red-dmg 1 1 red-mag 1 1 ac 15 20 light -1 -1 ac% 50 80 0 +Dark Familiar 100 1 1 1 34 hbt plate boots 5 5000 lgry lgry ac 40 50 kick 3 5 skilltab 6 1 1 lifesteal 3 4 mana 25 35 red-mag 10 15 0 +Grimleaper 100 1 3 1 31 hbt plate boots 5 5000 invboot15 ac% 125 150 dmg% 20 30 skill 143 1 2 heal-kill 2 3 aura prayer 1 6 gethit-skill 68 5 8 res-pois 25 35 hp% 5 5 0 +Tearhaunch 0 1 5 1 28 hbt Plate Boots 5 5000 invboot22 ac 35 40 str 5 5 dex 5 5 move3 20 20 res-all 10 15 ac% 60 80 skill 115 2 2 0 +Hooves of Satan 100 1 1 1 40 xlb demonhide boots 5 5000 invboot10 ac% 90 140 cast3 30 30 rep-dur 15 slow 10 15 dex -5 -5 res-fire 45 60 extra-fire 10 10 kick 3 5 0 +Deepwander 100 1 3 1 35 xlb demonhide boots 5 5000 invboot6 ac 50 80 move2 25 25 stam 50 50 light 1 7 nofreeze 1 1 hp 25 25 mana 25 25 0 +Infernostride 100 1 5 1 29 xlb Demonhide Boots 5 5000 lred dmg-fire 12 33 move2 20 20 res-fire-max 10 10 res-fire 30 30 light 2 2 ac% 120 150 gold% 40 70 ac 15 15 gethit-skill 46 5 8 0 +Asheara's Slippers 100 1 1 1 45 xvb sharkskin boots 5 5000 invboot12 ac% 110 150 ac 25 40 res-pois 25 35 res-cold 30 40 move3 25 25 balance2 20 20 extra-cold 10 15 0 +Road to Perdition1 100 1 3 1 38 xvb sharkskin boots 5 5000 lgry lgry invboot1 ac% 120 160 move1 15 15 balance2 25 25 dex 20 20 rep-dur 8 addxp 4 7 charged 96 250 20 0 +Waterwalk 100 1 5 1 32 xvb Sharkskin Boots 5 5000 invboot26 ac-miss 100 100 move2 20 20 dex 15 15 ac% 180 210 hp 45 65 stam 40 40 res-fire-max 5 5 regen-stam 50 50 0 +Whirling Dervish 100 1 1 1 48 xmb mesh boots 5 5000 invboot2 ac% 130 170 oskill whirlwind 7 12 skilltab 12 1 1 dmg% 10 15 swing1 10 10 ease 25 25 move2 20 20 0 +Zebrastride 100 1 3 1 43 xmb mesh boots 5 5000 invboot28 ac% 125 165 move3 40 40 dex/lvl 5 crush 10 10 kick 5 10 res-all 15 15 regen-mana 25 25 0 +Silkweave 100 1 5 1 36 xmb Mesh Boots 5 5000 ac% 150 190 mana-kill 5 5 ac-miss 200 200 mana% 10 10 move2 30 30 0 +Halbu's Gift 100 1 1 1 56 xtb battle boots 5 5000 invboot20 ac% 125 160 ac/lvl 6 addxp 2 4 cheap 5 5 red-dmg 10 15 res-all 15 25 dex 15 25 0 +Zero Effect 100 1 3 1 49 xtb battle boots 5 5000 dred dred invboot7 ac% 130 170 red-dmg% 15 20 abs-cold% 10 15 abs-fire% 10 15 abs-ltng% 10 15 res-pois-len 50 50 nofreeze 1 1 0 +Wartraveler 100 1 5 1 42 xtb Battle Boots 5 5000 invboot27 vit 10 10 str 10 10 mag% 30 50 dur 30 30 move2 25 25 ac% 150 190 dmg-norm 15 25 thorns 5 10 stamdrain 40 40 0 +River Stalker 100 1 1 1 60 xhb war boots 5 5000 invboot17 ac 100 140 balance2 30 30 oskill avoid 1 1 oskill dodge 1 1 oskill evade 1 1 mana 30 50 regen-mana 35 35 mana-kill 3 5 0 +Doubtraiser 100 1 3 1 55 xhb war boots 5 5000 invboot13 ac/lvl 12 str 20 20 move1 15 15 addxp -3 -3 heal-kill 12 12 res-all 25 25 kick 5 5 mag% 25 25 0 +Gorerider 100 1 5 1 47 xhb War Boots 5 5000 invboot16 ease -25 -25 deadly 15 15 move2 30 30 crush 15 15 openwounds 10 10 ac% 160 200 dur 10 10 stam 20 20 0 +Dance of the Cobra 100 1 2 1 56 ulb wyrmhide boots 5 5000 invboot8 ac% 100 150 stupidity 1 1 move1 15 15 res-pois 75 75 res-pois-len 25 80 dmg-pois 100 777 777 noheal 1 1 0 +Stallion Hooves 100 1 1 1 67 ulb Wyrmhide Boots 5 5000 ac% 100 125 dex 35 35 balance3 40 40 move3 40 40 rep-dur 60 dmg-max 20 20 crush 15 15 deadly 20 25 0 +Thoqqua's Slipper 100 1 2 1 64 uvb scarabshell boots 5 5000 invboot11 ac 75 120 mag% 40 80 res-all 10 15 hp 35 50 move3 50 50 0 +Sandstorm Trek 100 1 1 1 70 uvb scarabshell boots 5 5000 ac% 140 170 move2 20 20 balance2 20 20 stam/lvl 8 stamdrain 50 50 res-pois 40 70 rep-dur 5 str 10 15 vit 10 15 0 +Hollowed Ground 100 1 1 1 76 umb boneweave boots 5 5000 invboot29 pal 1 1 ac% 150 200 balance2 25 25 dmg-undead 125 125 rip 1 1 aura 119 2 5 dex 15 25 0 +Marrowwalk 100 1 2 1 69 umb boneweave boots 5 5000 dgry dgry ac% 170 200 move2 20 20 charged Bone Prison 13 33 charged Life Tap 10 12 regen-stam 10 10 regen-mana 10 10 half-freeze 1 1 str 10 20 dex 17 17 skill Skeleton Mastery 1 2 0 +Stairway to Heaven 100 1 2 1 74 utb mirrored boots 5 5000 invboot9 ac% 125 160 mag% 20 30 res-fire 40 70 res-cold 40 70 res-ltng 40 70 res-pois 40 70 half-freeze 1 1 res-pois-len 50 50 0 +Lustwander 100 1 1 1 81 utb Mirrored Boots 5 5000 invboot24 ac 100 150 oskill teleport 2 5 gold% 150 250 mag% 75 75 res-all 25 25 allskills 1 1 0 +Wild Horses 100 1 1 1 87 uhb myrmidon greaves 5 5000 invboot14 ac% 125 200 kick 10 15 move3 40 40 balance3 40 40 str 25 25 dex 25 25 deadly 10 20 0 +Shadowdancer 100 1 2 1 80 uhb myrmidon greaves 5 5000 invboot18 ac% 70 100 move2 30 30 balance2 30 30 dex 15 25 skilltab 19 1 2 ease -20 -20 0 +b28 0 +Belts 0 +Bloodrune 100 1 1 1 8 lbl sash 5 5000 dred dred ac 15 25 regen 3 5 res-fire 40 45 res-cold 40 45 res-pois 40 45 res-ltng 40 45 0 +Ivywrap 100 1 3 1 5 lbl sash 5 5000 lyel lyel ac 10 20 dmg-pois 75 77 77 res-pois 35 35 res-pois-len 25 50 slow 10 10 mag% 10 15 0 +Lenyms Cord 0 1 5 1 3 lbl Sash 5 5000 cgrn cgrn mana 15 30 regen-mana 30 30 res-all 5 10 light 1 1 ac 5 10 0 +Ratman's Rope 100 1 1 1 15 vbl light belt 5 5000 oran oran invbelt12 ac 10 20 dmg-demon 100 100 res-ltng 20 30 mana 35 35 cast1 10 10 dex 5 5 0 +Spiritseeker 100 1 3 1 12 vbl light belt 5 5000 invbelt6 ac 15 25 dmg-undead 50 50 addxp 3 5 cheap 5 5 mana 20 30 regen-mana 50 50 0 +Snakecord 0 1 5 1 9 vbl Light Belt 5 5000 blac blac dmg-pois 75 40 40 res-pois 25 25 ac 10 10 ac% 35 60 regen 5 10 res-pois-len 50 50 0 +Belt of Evil 100 1 1 1 25 mbl belt 5 5000 invbelt1 ac 20 30 regen -1 -1 addxp -2 -2 gold% 75 75 mag% 75 75 hp 55 75 0 +Wreath of Suffering 100 1 3 1 21 mbl belt 5 5000 invbelt15 ac% 100 125 dmg-max 5 15 manasteal 2 5 crush 5 10 regen -2 -2 str 10 10 vit 10 10 0 +Nightsmoke 0 1 5 1 18 mbl Belt 5 5000 lyel lyel res-all 10 15 dmg-to-mana 50 50 mana 20 30 red-dmg 2 5 ac 15 15 ac% 40 70 0 +Crocodile Wrap 100 1 1 1 30 tbl heavy belt 5 5000 ac% 75 100 red-dmg% 10 10 move2 20 20 red-mag 3 5 res-all 10 10 str 10 10 vit 10 10 0 +Pirate's Faith 100 1 3 1 28 tbl heavy belt 5 5000 oran oran mag% 25 25 gold% 100 100 move2 20 20 ac% 120 145 dex 15 15 red-dmg% 5 10 fade 1 1 0 +Goldwrap 0 1 5 1 25 tbl Heavy Belt 5 5000 lblu lblu mag% 30 30 light 2 2 ac 25 35 swing2 10 10 ac% 40 60 gold% 50 80 0 +Pandemonium 100 1 1 1 32 hbl girdle 5 5000 dred dred ac/lvl 10 str/lvl 4 hp/lvl 8 dmg/lvl 2 res-cold/lvl 4 res-fire/lvl 4 0 +Leash of Cerebus 100 1 3 1 30 hbl girdle 5 5000 invbelt11 ac% 125 150 all-stats 10 10 regen 3 3 skilltab 15 1 3 skill 227 1 3 skill 237 1 3 balance1 15 15 res-ltng 35 45 0 +Bladebuckle 0 1 5 1 28 hbl Girdle 5 5000 dyel dyel thorns 8 15 ac 30 40 red-dmg 3 5 str 5 5 dex 10 10 ac% 80 100 balance2 30 30 0 +Warriv's Coin 100 1 1 1 39 zlb demonhide sash 5 5000 invbelt16 ac% 100 120 gold% 200 200 cheap 5 15 mag% 25 25 manasteal 5 5 mana/lvl 6 0 +Xenophobe 100 1 3 1 36 zlb demonhide sash 5 5000 invbelt14 ac% 100 150 ac/lvl 6 hit-skill 77 10 3 skilltab 6 1 1 skill 77 3 3 dmg% 15 20 dmg-to-mana 5 10 0 +String of Ears 100 1 5 1 30 zlb Demonhide Sash 5 5000 lred red-mag 10 15 red-dmg% 10 15 lifesteal 6 8 ac% 150 180 ac 15 15 dur 10 10 0 +Megladon Wrap 100 1 1 1 41 zvb sharkskin belt 5 5000 invbelt3 ac/lvl 10 heal-kill 6 10 lifesteal 4 7 crush 15 15 dmg-max 10 15 res-ltng/lvl 6 dex 10 10 0 +Wave Whipper 100 1 3 1 37 zvb sharkskin belt 5 5000 invbelt8 ac% 110 160 res-cold 25 35 crush 5 10 deadly 5 10 oskill increased stamina 1 1 move2 20 20 0 +Razortail 100 1 5 1 32 zvb Sharkskin Belt 5 5000 thorns/lvl 8 dex 15 15 pierce 33 33 ac 15 15 ac% 120 150 dmg-max 10 10 0 +Meshif's Coil 100 1 1 1 45 zmb mesh belt 5 5000 invbelt2 ac% 130 160 move3 30 30 balance1 15 15 gethit-skill 76 4 14 regen-mana 30 30 res-cold-max 10 10 res-cold/lvl 7 0 +Weakling�s Whimper 100 1 3 1 40 zmb mesh belt 5 5000 invbelt13 ac% 125 165 gethit-skill 72 20 1 charged 72 15 25 str 20 20 res-pois 10 25 res-fire 35 35 0 +Gloomstrap 100 1 5 1 36 zmb Mesh Belt 5 5000 light -3 -3 mana% 15 15 manasteal 5 5 ac% 120 150 vit 15 15 regen-mana 15 15 0 +Assassin Vine 100 1 1 1 48 ztb battle belt 5 5000 cred cred ass 1 1 dex 20 30 str 20 30 balance2 20 20 kick 5 5 knock 2 2 res-ltng 20 40 0 +Fortune's Fool 100 1 3 1 46 ztb battle belt 5 5000 dgld dgld ac% 75 100 ac 50 75 gold%/lvl 16 mag%/lvl 6 enr -5 -5 vit -5 -5 oskill find item 1 1 0 +Snowclash 100 1 5 1 42 ztb Battle Belt 5 5000 gethit-skill 59 5 0 abs-cold 15 15 res-cold-max 15 15 dmg-cold 75 13 21 ac% 130 170 skill 59 2 2 skill 55 3 3 skill 60 2 2 0 +The Art of War 100 1 1 1 54 zhb war belt 5 5000 invbelt4 ac% 150 180 str 35 35 dmg/lvl 4 swing1 15 15 bar 1 1 vit/lvl 4 res-ltng 35 35 0 +Shattered Dreams 100 1 3 1 50 zhb war belt 5 5000 blac blac ac% 135 175 dmg-max 5 15 stupidity 1 1 swing1 10 10 regen 5 5 deadly 10 10 oskill mace mastery 1 1 0 +Thudergod's Vigor 100 1 5 1 47 zhb War Belt 5 5000 oran oran gethit-skill 121 5 7 dmg-ltng 1 50 res-ltng-max 10 10 abs-ltng 20 20 ac% 160 200 vit 20 20 str 20 20 skill 34 3 3 skill 35 3 3 0 +Duskwreath 100 1 1 1 60 ulc spiderweb sash 5 5000 invbelt10 ac 100 150 fade 1 1 res-all 35 35 balance2 20 20 cast1 15 15 enr 30 30 0 +Arachnid Mesh 100 1 2 1 55 ulc spiderweb sash 5 5000 blac blac ac% 90 120 cast2 20 20 charged Venom 11 3 allskills 1 1 slow 10 10 mana% 5 5 0 +Dreadfury 100 1 1 1 64 uvc vampirefang belt 5 5000 invbelt7 ac% 120 150 dru 1 1 swing1 10 10 howl 33 33 skill-rand 2 221 250 extra-fire 10 15 0 +Nosferatu's Coil 100 1 2 1 59 uvc Vampirefang Belt 5 5000 str 15 15 mana-kill 2 2 slow 10 10 lifesteal 10 10 swing2 10 10 light -3 -3 0 +Kashya's Ward 100 1 1 1 73 umc mithril coil 5 5000 invbelt9 ac% 100 150 red-dmg% 15 15 res-mag 15 15 abs-fire/lvl 3 abs-ltng/lvl 3 abs-cold/lvl 3 regen-mana 50 50 0 +Verdugo's Hearty Cord 100 1 2 1 67 umc mithril coil 5 5000 invbelt5 ac% 90 140 vit 30 40 stam 100 120 balance2 10 10 red-dmg% 10 15 regen 10 13 0 +Wisdom's Wrap 100 1 1 1 78 utc troll belt 5 5000 ac% 150 200 addxp 3 3 enr/lvl 5 allskills 1 1 rep-dur 30 dmg-to-mana 15 15 res-all 15 20 0 +Ecstacy of Ishtar 100 1 2 1 71 utc Troll Belt 5 5000 cblu cblu ac% 100 200 hp% 25 25 mana% 25 25 red-dmg 25 30 red-mag 25 30 pierce 50 50 0 +Lachdonnan's Wrap 100 1 1 1 85 uhc colossus girdle 5 5000 ac 200 300 ease 20 20 lifesteal 8 8 manasteal 8 8 res-all 30 40 block2 20 20 allskills 1 1 dmg% 20 25 0 +Ogre's Embrace 100 1 2 1 80 uhc Colossus Girdle 5 5000 dgrn dgrn ac% 175 200 ac 75 125 red-dmg% 10 15 str/lvl 8 vit/lvl 8 res-pois 100 100 rep-dur 25 oskill iron skin 1 1 0 +b29 0 +Amazon Bows 0 +Damsel of Destruction 100 1 1 26 26 am1 stag bow 5 5000 blac blac invam1 dmg% 80 120 dmg-norm 20 40 skilltab 0 1 3 swing1 15 15 dmg-elem 200 10 25 mana-kill 5 5 balance2 20 20 0 +Bleeding Branch 100 1 3 1 18 am1 stag bow 5 5000 cred cred invam1 dmg% 120 160 dmg-norm 10 20 lifesteal 5 8 noheal 1 1 openwounds 57 57 bloody 1 1 sock 2 2 0 +Angelsong 100 1 5 1 12 am1 Stag Bow 5 5000 dgld dgld invam1 dmg% 75 110 dmg-norm 5 10 swing1 10 10 aura defiance 1 3 pierce-fire 45 45 pierce-cold 45 45 dmg-demon 200 200 0 +Kashya's Retort 100 1 1 33 39 am2 reflex bow 5 5000 oran oran invam2 dmg% 80 120 dmg-norm 25 50 skilltab 0 1 3 ama 1 1 gethit-skill 47 17 4 light 2 4 mana 35 35 manasteal 5 5 0 +Elvenbrand 100 1 3 1 32 am2 reflex bow 5 5000 dgrn dgrn invam2 dmg% 125 175 ama 1 1 skilltab 0 1 3 swing2 20 20 dex 20 20 ignore-ac 1 1 pierce 20 20 dmg-norm 10 20 0 +Ragnarok Sliver 100 1 5 1 24 am2 Reflex Bow 5 5000 whit whit invam2 dmg% 100 150 crush 25 25 deadly/lvl 12 dex 15 15 ease -50 -50 ama 1 1 allskills 1 1 dmg-norm 10 25 0 +Brightmangler 100 1 1 55 51 am6 ashwood bow 5 5000 invbow15 dmg% 100 100 dmg-min 25 35 dmg-max 50 100 skilltab 1 1 3 deadly 35 35 openwounds 50 50 res-ltng 35 35 res-pois 20 30 0 +Abyssal Torment 100 1 3 1 41 am6 ashwood bow 5 5000 lgry lgry invam1 dmg% 200 250 skilltab 0 3 3 dmg/lvl 8 res-fire 35 50 extra-fire 15 15 oskill amplify damage 1 1 rip 1 1 0 +Adamantine Leaf 100 1 5 1 33 am6 Ashwood Bow 5 5000 lgrn lgrn invam1 dmg% 125 190 skilltab 0 1 3 swing2 20 20 res-all 35 50 pierce 25 25 move2 25 25 magicarrow 1 1 att% 25 25 skill 32 7 7 dmg-norm 20 40 0 +Distance Strike 100 1 1 55 60 am7 ceremonial bow 5 5000 dyel dyel invam2 dmg% 100 100 dmg-min 25 50 dmg-max 90 140 swing3 30 30 allskills 1 1 dex 15 15 move2 25 25 att 250 250 knock 2 2 0 +The Four Winds 100 1 3 1 50 am7 ceremonial bow 5 5000 dred dred invam2 dmg% 220 270 dmg-norm 15 30 dmg-ltng 1 200 oskill teleport 1 1 move3 50 50 ama 1 2 skilltab 1 1 3 0 +Lycander's Aim 100 1 5 1 42 am7 Ceremonial Bow 5 5000 invam2 ama 2 2 skilltab 0 2 2 dmg% 150 200 swing2 20 20 ac% 20 20 dex 20 20 enr 20 20 manasteal 5 8 dmg-norm 25 50 0 +Laurana's Elven Bow 100 1 1 1 73 amb matriachal bow 5 5000 dblu dgld invbow14 dmg% 260 325 dmg-norm 25 50 dmg%/lvl 14 swing3 50 50 pierce 40 40 balance2 30 30 sock 1 1 0 +Bloodraven's Charge 100 1 4 1 62 amb matriarchal bow 5 5000 dgld dgld invam1 dmg% 200 300 att% 50 50 explosivearrow 13 13 skilltab 0 2 4 charged Revive 30 5 dmg/lvl 12 0 +Wintershock 100 1 1 1 87 amc grand matron bow 5 5000 lgld lgld invam2 dmg% 300 350 dmg-norm 40 80 dmg-cold 600 25 50 dmg-ltng 1 444 res-ltng 65 65 res-cold 65 65 extra-cold 10 15 pierce-cold 10 15 0 +Quicksilver 100 1 4 1 80 amc Grand Matron Bow 5 5000 whit invam2 dmg% 200 300 dmg/lvl 16 dmg-norm 40 100 swing3 50 50 skilltab 0 3 3 ama 2 2 addxp 10 10 res-all 15 50 0 +b30 0 +Amazon Javelins 0 +Shieldmaiden's Toss 100 1 1 40 50 am5 maiden javelin 5 5000 cgrn cgrn dmg% 75 100 dmg-norm 30 50 skilltab 2 1 3 stack 80 80 rep-quant 25 pierce 25 25 dex 15 15 deadly 15 15 0 +Hastemaster 100 1 3 1 27 am5 maiden javelin 5 5000 dyel dyel dmg% 120 160 dmg-throw 15 15 rep-quant 25 stack 55 55 swing3 40 40 block2 30 30 0 +Raptorshaft 100 1 5 1 20 am5 Maiden Javelin 5 5000 dmg% 75 115 ama 1 1 skilltab 2 1 1 dmg-throw 10 20 stack 50 50 rep-quant 75 noheal 1 1 addxp 15 15 0 +Lady in Waiting 100 1 1 55 65 ama ceremonial javelin 5 5000 cred cred dmg% 100 125 dmg-norm 30 60 dmg/lvl 6 stack 100 100 rep-quant 90 skilltab 2 1 3 allskills 1 1 rip 1 1 0 +Sister of the Sun 100 1 3 1 49 ama ceremonial javelin 5 5000 oran oran dmg% 215 270 ama 2 2 dmg-fire 75 100 res-cold 35 35 res-fire 50 50 skilltab 2 1 3 rep-quant 25 stack 155 155 0 +Titan's Revenge 100 1 5 1 42 ama Ceremonial Javelin 5 5000 ama 2 2 skilltab 2 2 2 dmg% 150 200 move2 30 30 rep-quant 30 str 20 20 dex 20 20 lifesteal 5 9 dmg-norm 25 50 stack 60 60 0 +Pegasus Wing 100 1 1 1 80 amf matriarchal javelin 5 5000 dyel dyel dmg% 275 350 stack 100 100 rep-quant 25 skilltab 2 1 3 allskills 2 2 addxp 3 5 oskill teleport 3 3 balance3 40 40 block3 40 40 0 +Thunderstroke 100 1 2 1 69 amf matriarchal javelin 5 5000 dblu dblu dmg% 150 200 dmg-ltng 1 511 hit-skill Lightning 20 14 swing2 15 15 pierce-ltng 15 25 skill Lightning Bolt 3 3 skilltab 2 2 4 rep-quant 15 0 +b31 0 +Amazon Spears 0 +Wrathshifter 100 1 2 1 15 am3 maiden spear 5 5000 dmg-min 15 30 dmg-max 50 70 skilltab 2 1 1 swing1 15 15 oskill 68 2 2 gethit-skill 54 7 1 0 +Hawkfire 100 1 5 1 8 am3 Maiden Spear 5 5000 lred lred dmg% 80 120 dmg-fire 5 18 res-fire 35 35 ama 1 1 hp 35 35 swing1 25 25 0 +Stygian Harlot 100 1 2 1 25 am4 maiden pike 5 5000 dmg-min 30 55 dmg-norm 80 120 swing2 20 20 skilltab 2 1 3 hp 40 50 lifesteal 6 8 sock 2 2 0 +Sepia Shard 100 1 5 1 19 am4 Maiden Pike 5 5000 cblu cblu dmg% 100 125 dmg-cold 200 10 15 res-cold 20 40 skilltab 2 2 2 mana 35 50 att 100 200 manasteal 6 9 0 +Vile Temptress 100 1 2 1 40 am8 ceremonial spear 5 5000 blac blac dmg% 175 225 dmg/lvl 12 ama 1 1 skilltab 1 2 2 hit-skill 53 8 3 addxp 3 5 dex 25 25 ease -20 -20 0 +Silverdawn 100 1 5 1 33 am8 Ceremonial Spear 5 5000 whit dmg% 140 200 ama 1 1 skilltab 1 2 4 dmg-ltng 1 200 res-ltng 50 65 light 4 7 dmg-demon 100 200 hp/lvl 9 dmg-norm 20 60 0 +Murder�s Mistress 100 1 2 1 50 am9 ceremonial pike 5 5000 dgry dgry dmg% 190 250 dmg%/lvl 12 ama 1 1 swing3 50 50 deadly 18 28 oskill spear mastery 3 5 rep-dur 10 heal-kill 10 15 mag% 35 70 0 +Lycander's Flank 100 1 5 1 42 am9 Ceremonial Pike 5 5000 ama 2 2 skilltab 2 2 2 dmg% 150 200 swing2 30 30 ac% 20 20 str 20 20 vit 20 20 lifesteal 5 9 dmg-norm 25 50 0 +Valkyrie's Calling 100 1 1 1 73 amd matriarchal spear 5 5000 whit whit dmg% 250 300 dmg-norm 40 80 skill 32 4 7 charged 258 10 15 skilltab 2 3 3 allskills 1 1 res-all 30 30 mana 75 75 0 +Stoneraven 100 1 2 1 64 amd matriarchal spear 5 5000 dgry dmg% 230 280 dmg-mag 101 187 res-all 30 50 ac 400 600 skilltab 2 1 3 sock 2 5 0 +Amanda's Point 100 1 1 1 80 ame matriarchal pike 5 5000 dmg% 275 325 dmg/lvl 16 skilltab 2 2 2 sock 4 6 ama 2 2 0 +Manticore's Retort 100 1 2 1 70 ame Matriarchal Pike 5 5000 oran lred dmg% 200 300 dmg-norm 50 100 swing3 35 35 dmg-pois 100 999 999 skilltab 2 1 3 res-pois-max 15 15 res-pois 50 50 res-pois-len 75 75 0 +b32 0 +Sorcerress Orbs 0 +Queasespreader 100 1 1 1 11 ob1 eagle orb 5 5000 dmg-norm 8 16 swing2 25 25 sor 1 1 dmg-ltng 1 30 res-ltng 35 50 mana 25 25 0 +Sparrow's Trill 100 1 3 1 6 ob1 Eagle Orb 5 5000 oran oran dmg-norm 3 8 skilltab 5 1 1 mana 25 35 ac 15 25 move1 20 20 0 +Cyanstrike 100 1 1 1 16 ob2 sacred orb 5 5000 dyel dyel dmg-min 6 12 dmg-max 20 25 hit-skill 45 8 3 res-pois 25 25 extra-cold 10 10 heal-kill 5 5 0 +Flicker Cinch 100 1 3 1 12 ob2 Sacred Globe 5 5000 dmg% 80 120 block 20 30 block2 20 20 hp 25 25 mana 35 35 mag% 20 30 0 +Revoker 100 1 1 1 23 ob3 smoked sphere 5 5000 dred dred allskills 1 1 mag% 20 20 cast2 25 25 extra-ltng 10 15 res-all 20 20 hp 35 50 charged 138 60 8 0 +Soul Stinger 100 1 3 1 18 ob3 Smoked Sphere 5 5000 dgry dgry sor 1 1 rip 1 1 oskill weaken 2 2 hit-skill 66 75 5 dmg-norm 10 20 manasteal 35 35 0 +Zapcaster 100 1 1 1 27 ob4 clasped orb 5 5000 lgry lgry skilltab 4 2 2 pierce-ltng 10 15 abs-ltng 8 12 hit-skill 49 6 5 dmg-min 10 10 dmg-max 25 35 swing2 20 20 0 +Thought Splinter 100 1 3 1 22 ob4 Clasped Orb 5 5000 enr/lvl 8 skilltab 4 1 2 skilltab 3 1 2 skilltab 5 1 2 heal-kill 5 5 addxp 5 5 0 +Unseeing Eye 100 1 5 1 26 ob5 dragon stone 5 5000 allskills 1 1 addxp 10 10 cast2 20 20 mana 45 45 ac 70 100 dmg-to-mana 10 15 mana-kill 4 6 0 +Eternity Cable 100 1 1 1 32 ob5 Dragon Stone 5 5000 blac blac sor 2 2 skill-rand 3 36 60 skill-rand 3 36 60 skill-rand 3 36 60 res-all 25 40 str 25 25 dex 15 15 0 +Terminus Rod 100 1 1 1 36 ob6 glowing orb 5 5000 sor 2 2 extra-fire 10 15 pierce-fire 10 15 abs-fire% 15 20 res-fire 75 75 dex 15 15 enr 15 15 0 +Dreamsplitter 100 1 3 1 29 ob6 Glowing Orb 5 5000 cgrn cgrn allskills 1 1 skill-rand 7 36 60 all-stats 15 25 mag% 15 25 gold% 50 100 sock 2 2 regen 10 15 0 +Drehya's Globe 100 1 1 1 40 ob7 crystalline globe 5 5000 sor 2 2 extra-cold 10 15 pierce-cold 10 15 abs-cold% 15 20 res-cold 75 75 str 15 15 vit 15 15 0 +Murdering Shard 100 1 3 1 33 ob7 Crystalline Globe 5 5000 lpur lpur dmg-norm 15 30 dmg% 100 150 swing2 40 40 manasteal 100 100 slow 35 35 sor 2 2 heal-kill 10 15 0 +Starbreaker 100 1 1 1 43 ob8 cloudy sphere 5 5000 blac blac sor 2 2 extra-ltng 10 15 pierce-ltng 10 15 abs-ltng% 15 20 res-ltng 75 75 str 15 15 dex 15 15 0 +Dawn Shadow 100 1 3 1 35 ob8 Cloudy Sphere 5 5000 mana% 25 25 hp% 20 20 allskills 2 2 cast2 25 25 regen-mana 200 200 balance2 30 30 block 25 25 0 +Manna from Heaven 100 1 1 1 48 ob9 sparkling ball 5 5000 cblu cblu sor 1 2 res-all-max 5 15 res-all 50 50 mana/lvl 20 gethit-skill 58 10 12 balance1 15 15 abs-mag 10 20 res-pois-len 50 90 0 +Desecration Sigil 100 1 3 1 50 ob9 Sparkling Ball 5 5000 whit oskill skeleton mastery 5 12 oskill revive 1 3 oskill raise skeletal mage 6 10 cast3 40 40 cheap 10 10 mana/lvl 8 sor 2 2 oskill grim ward 1 1 0 +Quandry of the Queen 100 1 3 1 39 oba swirling crystal 5 5000 dgld dgld allskills 1 2 vit/lvl 4 enr/lvl 4 str/lvl 3 dex/lvl 3 cast3 30 30 red-dmg% 10 15 regen 8 10 regen-mana 75 75 0 +The Oculus 100 1 1 1 42 oba Swirling Crystal 5 5000 sor 3 3 gethit-skill 54 25 1 res-all 20 20 cast2 30 30 ac% 20 20 vit 20 20 enr 20 20 mana-kill 5 5 mag% 50 50 0 +Star of Bethlehem 100 1 1 1 59 obb heavenly stone 5 5000 lyel lyel aura 99 1 1 aura 120 1 1 aura 123 1 1 allskills 1 1 charged 117 100 5 charged 155 5 25 addxp 5 10 0 +Haven of Light 100 1 3 1 50 obb Heavenly Stone 5 5000 whit sor 2 2 pierce-ltng 30 50 pierce-fire 30 50 pierce-cold 30 50 skill 65 3 3 skill 63 3 3 skill 61 3 3 mana-kill 4 7 0 +Utterance of Power 100 1 1 1 65 obc eldritch orb 5 5000 cgrn cgrn extra-fire 35 50 extra-cold 35 50 extra-ltng 35 50 charged 154 200 7 charged 91 20 5 0 +Eschuta's temper 100 1 3 1 58 obc eldritch orb 5 5000 cred sor 1 3 cast2 40 40 extra-fire 10 20 extra-ltng 10 20 enr 20 30 res-all 25 50 0 +Star of David 100 1 1 1 72 obd demon heart 5 5000 allskills 2 2 dex 25 25 enr 25 25 ac% 25 25 heal-kill 15 25 res-mag 65 65 move2 20 20 sock 1 1 0 +Soul Of The Tanar'Ri 100 1 3 1 68 obd Demon Heart 5 5000 blac blac allskills 1 1 red-dmg% 20 20 abs-fire% 15 20 abs-cold% 15 20 abs-ltng% 15 20 fade 1 1 move3 30 30 mana% 40 40 0 +Unity of Mind 100 1 3 1 73 obe vortex orb 5 5000 oran oran sor 2 2 mana/lvl 24 dmg-to-mana 25 25 regen-mana 100 100 skill 58 7 7 cast3 50 50 res-all 40 40 addxp 5 5 0 +Athena�s Tirade 100 1 1 1 78 obe Vortex Orb 5 5000 sor 3 3 aura conviction 5 5 ac 200 200 dmg-to-mana 25 25 all-stats 35 35 regen 25 25 regen-mana 150 150 mag% 50 75 gold% 100 200 0 +Oracle's Riddle 100 1 1 1 84 obf dimensional shard 5 5000 dmg-min 25 50 dmg-max 175 225 swing3 50 50 manasteal 20 20 ignore-ac 1 1 oskill valkyrie 7 7 block 25 35 block3 50 50 balance3 40 40 allskills 1 3 0 +Fathom 100 1 3 1 79 obf dimensional shard 3 5000 lblu sor 3 3 extra-cold 15 30 cast2 20 20 res-fire 25 40 res-ltng 25 40 mana/lvl 16 hp/lvl 8 0 +b33 0 +Necromancer Heads 0 +Blanched Death 100 1 1 1 8 ne1 preserved head 5 5000 invnec7 ac/lvl 8 skill 69 1 3 skill-rand 2 66 76 res-cold 25 35 gethit-skill 67 8 4 nec 1 1 0 +Goblin Grin 100 1 3 1 4 ne1 Preserved Head 5 5000 lgrn lgrn ac% 40 60 block 15 20 block1 20 20 skill 69 1 2 skill 70 1 2 cast1 15 15 0 +Paingiver 100 1 1 1 12 ne2 zombie head 5 5000 invnec8 ac 25 40 skilltab 6 1 3 skill 66 1 3 mana 25 25 block 15 25 block1 15 15 0 +Death�s Witness 100 1 3 1 9 ne2 Zombie Head 5 5000 dgry dgry ac% 45 65 block 10 20 move2 25 25 nec 1 1 ac 15 25 res-pois 35 35 cast1 15 15 0 +Death Mauler 100 1 1 1 19 ne3 unraveller head 5 5000 ac% 75 100 skilltab 7 1 3 charged 126 100 10 att 200 200 dmg-norm 5 10 block2 20 20 balance2 25 25 0 +Scalphunter 100 1 3 1 15 ne3 Unraveller Head 5 5000 lyel lyel ac% 50 70 block 20 30 block2 25 25 skilltab 6 1 3 red-dmg 8 10 demon-heal 15 15 cast2 20 20 0 +Steel Weevil 100 1 1 1 24 ne4 gargoyle head 5 5000 whit ac% 80 110 skilltab 8 1 3 skill-rand 2 66 89 skill-rand 3 66 89 red-dmg% 10 15 block3 30 30 res-all 20 30 0 +Sinister Smile 100 1 3 1 20 ne4 Gargoyle Head 5 5000 invnec1 ac% 55 75 balance2 25 25 block2 25 25 nec 1 1 res-ltng 25 40 res-fire 35 50 hp 50 50 0 +Undead Beholder 100 1 1 1 30 ne5 demon head 5 5000 dyel dyel ac 45 60 allskills 1 1 block 25 25 balance2 15 15 move2 20 20 rep-dur 20 heal-kill 8 10 mana-kill 5 7 0 +Chiaroscuro Visage 100 1 3 1 26 ne5 Demon Head 5 5000 lred lred ac% 60 80 block 25 35 swing2 20 20 skilltab 7 1 3 dmg-norm 10 20 crush 35 35 dmg-pois 175 100 100 cast2 25 25 0 +King Tut 100 1 1 1 35 ne6 mummified trophy 5 5000 ac% 110 140 nec 1 2 gold% 100 100 mag%/lvl 8 block 20 40 block2 25 25 skill-rand 2 66 95 skill-rand 3 66 95 0 +Janus' Face 100 1 3 1 29 ne6 Mummified Trophy 5 5000 invnec2 ac% 90 110 balance3 25 25 block2 20 20 skilltab 8 1 3 mana-kill 3 5 pierce-pois 25 25 dmg-to-mana 10 15 cast2 20 20 0 +Mystery of Life 100 1 1 1 39 ne7 fetish trophy 5 5000 ac 75 125 skill 69 4 6 skill 70 3 5 skill 80 2 4 skill 95 1 3 hp% 20 20 hp/lvl 8 heal-kill 10 10 addxp 3 5 0 +Carrion Comfort 100 1 3 1 33 ne7 Fetish Trophy 5 5000 lpur lpur ac% 100 140 block 15 40 balance2 25 25 skill-rand 3 66 95 skill-rand 2 76 95 skill-rand 1 88 95 abs-mag 8 12 cast2 25 25 0 +Fallen Hero's Disgrace 100 1 1 1 43 ne8 sextan trophy 5 5000 dgry dgry ac/lvl 16 140 nec 2 2 dex 30 30 str 30 30 block 25 50 red-dmg 10 15 red-mag 10 15 death-skill 59 100 37 0 +Rictus of the Joker 100 1 3 1 36 ne8 Sexton Trophy 5 5000 invnec6 ac% 100 150 block 25 50 block2 30 30 skilltab 6 1 3 nec 2 2 sock 2 2 0 +Vow of Revenge 100 1 1 1 47 ne9 cantor trophy 5 5000 ac% 120 170 skilltab 7 nec 2 2 block 25 50 death-skill chain lightning 100 51 dex 20 20 res-fire 50 50 res-ltng 50 50 res-pois-len 50 95 0 +Dreamsender 100 1 3 1 40 ne9 Cantor Trophy 5 5000 blac blac ac% 120 170 balance3 25 25 move2 25 25 nec 2 2 mag% 50 50 str 20 20 dex 20 20 rep-dur 10 dur 100 100 0 +Putrid Defiler 100 1 1 1 52 nea heirophant trophy 5 5000 invnec3 ac 135 170 skilltab 8 nec 2 2 block 35 35 levelup-skill nova 100 60 vit 20 20 res-pois 50 50 res-cold 50 50 nofreeze 1 1 0 +Homunculus 100 1 3 1 42 nea Heirophant Trophy 5 5000 nec 2 2 skilltab 6 2 2 ac% 150 200 block2 30 30 block 40 40 enr 20 20 regen-mana 75 75 mana-kill 5 5 res-all 40 40 0 +Venomlord's Visage 100 1 1 1 55 neb minion skull 5 5000 dgrn dgrn ac% 150 200 oskill inferno 15 20 res-fire 75 100 allskills 1 1 demon-heal 35 50 skill-rand 3 66 95 skill-rand 3 66 95 addxp 3 5 0 +Reaper's Trophy 100 1 3 1 49 neb Minion Skull 5 5000 ac% 130 180 block 20 40 block2 30 30 allskills 2 2 hp/lvl 9 mana/lvl 9 res-cold 45 45 rip 1 1 0 +Devil's Advocate 100 1 1 1 63 neg hellspawn skull 5 5000 invnec10 ac 150 200 block 25 35 balance3 40 40 mana-kill 10 10 regen-mana 50 50 oskill warmth 1 1 skill 95 4 7 sock 2 2 0 +Dreadmother 100 1 3 1 57 neg Hellspawn Skull 5 5000 oran oran ac% 160 200 balance2 25 25 move2 25 25 skilltab 7 1 3 oskill poison javelin 5 8 oskill plague javelin 5 8 pierce-pois 20 20 extra-pois 20 20 skill 92 5 5 0 +Nihlathak's Spirit 100 1 1 1 71 ned overseer skull 5 5000 invnec4 ac/lvl 12 skill 74 8 15 skill 95 1 3 move2 20 20 oskill teleport 3 3 mana/lvl 12 red-dmg 15 15 res-all 25 35 0 +Basilisk's Kiss 100 1 3 1 66 ned Overseer Skull 5 5000 ac% 200 300 block 50 60 block3 45 45 red-dmg% 20 30 sock 2 2 dmg-to-mana 10 15 cast2 30 30 0 +Prayer for the Dying 100 1 1 1 78 nee succubae skull 5 5000 invnec9 ac% 150 200 rip 1 1 addxp 10 10 nec 3 3 block 50 50 sock 2 2 0 +Boneflame 100 1 3 1 72 nee succubae skull 3 5000 dred dred ac% 120 150 move2 20 20 gethit-skill Terror 15 3 nec 2 3 res-all 20 30 abs-mag% 25 25 0 +Ravings of the Mad 100 1 1 1 86 nef bloodlord skull 5 5000 invnec5 ac 200 300 mag% 65 65 gold% 150 150 all-stats 25 25 cast3 40 40 pierce 100 100 oskill spear mastery 8 12 swing2 20 20 dmg-norm 10 30 nec 1 2 0 +Darkforge Spawn 100 1 3 1 80 nef bloodlord skull 5 5000 cred cred ac% 140 180 cast2 30 30 mana% 10 10 skilltab 6 1 3 skilltab 7 1 3 skilltab 8 1 3 red-dmg% 15 20 nec 2 2 0 +b34 0 +Paladin Heraldic Shields 0 +Validator 100 1 1 1 8 pa1 targe 5 5000 invshld25 ac 20 25 block 15 25 block1 15 15 res-all 15 20 dmg% 15 20 dmg-max 4 9 swing1 10 10 0 +Seafarer's Security 100 1 3 1 5 pa1 Targe 5 5000 lblu ac 10 15 dmg% 15 25 block 20 25 sock 2 2 0 +Swiftfoot Slash 100 1 1 1 17 pa2 rondache 5 5000 ac% 75 100 block 20 30 block1 15 15 dmg% 20 30 swing2 20 20 move2 20 20 balance2 25 25 0 +Grip of the Gorgon 100 1 3 1 10 pa2 Rondache 5 5000 cgrn cgrn ac% 50 80 block 15 30 str 25 25 skilltab 10 1 3 slow 15 25 howl 75 75 0 +Felix's Brace 100 1 1 1 22 pa3 heraldic shield 5 5000 oran oran ac% 75 100 block 25 35 block2 20 20 dex 15 15 res-all 15 25 red-dmg 10 15 balance3 40 40 0 +Secret of Steel 100 1 3 1 15 pa3 Heraldic Shield 5 5000 ac% 75 100 ac 20 40 red-dmg% 10 20 ease -15 -15 block 15 15 block2 25 25 balance2 25 25 0 +Bastion of Hope 100 1 3 1 20 pa4 aerin shield 5 5000 cgrn cgrn ac% 90 130 block 25 35 block2 20 20 str 20 20 vit 15 15 addxp 3 5 pal 1 1 res-all 15 25 0 +Hellchatter 100 1 1 1 27 pa4 Aerin Shield 5 5000 dred dred ac 40 50 pal 1 1 res-all 60 70 dmg-fire 16 24 hit-skill 76 10 7 rep-dur 7 dur 45 45 0 +King's Guard 100 1 3 1 25 pa5 crown shield 5 5000 cred cred ac 75 120 block 30 40 block2 20 20 dmg% 20 30 red-dmg% 15 15 res-mag 15 15 lifesteal 5 5 cheap 10 15 0 +Devourer of Worlds 100 1 1 1 32 pa5 Crown Shield 5 5000 blac blac ac% 75 100 dmg% 25 50 sock 2 4 pal 1 2 0 +Death to the Soul 100 1 1 1 37 pa6 akaran targe 5 5000 blac blac ac% 120 150 block 30 45 balance2 25 25 allskills 1 1 res-all 40 60 rip 1 1 sock 1 1 charged 258 5 12 0 +Twilight Presence 100 1 3 1 30 pa6 Akaran Targe 5 5000 lgry lgry ac% 100 140 stupidity 3 3 att% 25 25 dmg-demon 100 100 move2 30 30 block 25 25 balance2 25 25 0 +Knight's Holy Sigil 100 1 1 1 41 pa7 akaran rondache 5 5000 whit whit ac/lvl 16 block 35 45 block2 30 30 dmg-demon 200 200 dmg-undead 200 200 dmg% 30 40 att% 25 25 pal 1 2 skill 117 2 3 0 +Voice of the Prophet 100 1 3 1 34 pa7 Akaran Rondache 5 5000 charged 138 1 30 charged 149 1 30 charged 155 1 30 res-all 40 40 allskills 2 2 hp% 15 15 rip 1 1 0 +Defender of Innocence 100 1 1 1 45 pa8 protector shield 5 5000 ac% 120 160 ac 35 50 block 25 30 balance2 35 35 aura 104 5 8 gethit-skill 40 17 1 res-mag 15 15 0 +Black Rain 100 1 3 1 39 pa8 Protector Shield 5 5000 blac blac ac% 100 150 res-fire 50 50 res-cold 35 35 dmg-ltng 10 100 stupidity 2 2 kill-skill static field 100 3 aura 118 5 8 0 +Crusader's Will 100 1 1 1 50 pa9 guilded shield 5 5000 oran oran ac% 125 175 block 35 35 balance2 20 20 cast3 40 40 mana 70 125 mana-kill 6 10 all-stats 10 10 pal 2 2 0 +Herald of Zakarum 100 1 3 1 42 pa9 Guilded Shield 5 5000 pal 2 2 skilltab 9 2 2 ac% 150 200 block2 30 30 block 30 30 str 20 20 vit 20 20 att% 20 20 res-all 50 50 0 +Wisdom of Thoth 100 1 3 1 46 paa royal shield 5 5000 dpur dpur ac 100 150 allskills 2 2 skilltab 10 1 3 enr 25 25 addxp 3 5 res-all 30 60 sock 2 2 0 +Grand Inquisitor 100 1 1 1 54 paa Royal Shield 5 5000 ac% 125 200 block 35 35 block3 50 50 balance3 50 50 addxp 10 15 dmg% 50 50 pal 3 3 res-all-max 10 10 0 +Ancients' Epiphany 100 1 3 1 55 pab sacred targe 5 5000 dgld dgld ac 125 225 block 50 60 block3 30 30 dmg% 60 60 dmg/lvl 2 charged 138 25 7 charged 149 25 7 charged 155 25 7 0 +Cleric's Rebuke 100 1 1 1 72 pab Sacred Targe 5 5000 invshld32 ac 150 200 aura thorns 15 20 gethit-skill 82 15 5 res-all 75 75 pal 2 2 gold% 200 200 mag%/lvl 8 regen 10 15 0 +Dawn Blesser 100 1 1 1 68 pac sacred rondache 5 5000 lyel lyel ac% 150 200 dmg% 35 50 res-all 35 50 red-dmg% 15 25 abs-fire% 10 15 abs-ltng% 15 15 abs-cold% 15 20 res-pois-len 50 50 pal 2 2 0 +Alma Negra 100 1 3 1 64 pac sacred rondache 5 5000 blac blac ac% 180 210 block2 30 30 pal 1 2 block 20 20 red-mag 5 9 att% 40 75 dmg% 40 75 sock 2 2 0 +Faith's Promise 100 1 3 1 70 pad ancient shield 5 5000 dyel dyel ac 150 200 block 35 35 balance2 30 30 aura 108 5 7 skill-rand 8 96 125 rep-dur 12 demon-heal 10 15 0 +Dawnfall 100 1 1 1 77 pad Ancient Shield 5 5000 gethit-skill 62 3 30 ac% 140 200 red-dmg% 35 35 res-all 50 75 skill-rand 3 96 125 move2 25 25 swing3 30 30 0 +God's Word 100 1 1 1 81 pae zakarum shield 5 5000 cblu cblu ac/lvl 24 dmg% 50 50 dmg-norm 15 40 sock 4 4 pal 1 3 0 +Dragonscale 100 1 3 1 78 pae zakarum shield 3 5000 dgrn dgrn ac% 170 200 abs-fire% 10 20 res-fire-max 5 5 str 15 25 dmg-fire 211 371 oskill Hydra 10 10 extra-fire 15 15 sock 2 2 0 +Valorsong 100 1 2 1 84 paf vortex shield 5 5000 oran oran allskills 2 2 ac/lvl 19 res-all 60 100 gethit-skill 149 5 4 gethit-skill 138 5 4 openwounds 88 88 dmg-to-mana 15 20 oskill warmth 1 1 0 +Fortress of Morpheus 100 1 1 1 88 paf Vortex Shield 5 5000 pal 4 4 ac% 175 175 red-dmg% 10 15 res-all 30 50 sock 4 4 0 +b35 0 +Barbarian Tribal Helms 0 +Madman's Bluster 100 1 1 1 12 ba1 jawbone cap 5 5000 ac% 75 100 res-all 10 20 skill 130 1 3 dmg-max 4 6 dmg-min 1 2 swing1 10 10 balance2 20 20 enr -5 -5 0 +Chaos Kin 100 1 3 1 6 ba1 Jawbone Cap 5 5000 oran ac 10 15 bar 1 1 red-dmg 3 5 dmg-max 8 12 res-ltng 15 25 howl 66 66 0 +Pitykiller 100 1 3 1 11 ba2 fanged helm 5 5000 ac 20 40 dmg% 15 15 levelup-skill frozen armor 100 25 res-fire 20 30 res-cold 25 25 nofreeze 1 1 0 +Darkhunger 100 1 1 1 18 ba2 Fanged Helm 5 5000 cred ac 10 20 stupidity 2 2 res-all 10 15 lifesteal 5 8 heal-kill 3 5 skilltab 12 1 3 0 +Kygragond 100 1 1 1 23 ba3 horned helm 5 5000 invhelm34 ac% 90 120 skilltab 12 1 3 block 15 15 block1 15 15 move1 10 10 manasteal 3 3 mana-kill 2 4 0 +Dragonkin 100 1 3 1 17 ba3 Horned Helm 5 5000 dgrn ac% 50 75 dur 25 25 rep-dur 20 att 125 125 dmg-fire 10 20 block 15 15 red-dmg% 5 10 0 +Invader's Glee 100 1 1 1 27 ba4 assault helm 5 5000 ac% 100 125 lifesteal 3 5 gold% 75 75 mag% 25 25 rep-dur 15 dur 25 25 openwounds 44 44 skilltab 13 1 3 0 +Blood Dancer 100 1 3 1 22 ba4 Assault Helmet 5 5000 dred ac 25 50 bloody 1 1 noheal 1 1 move3 25 25 dex 15 15 hp 25 25 res-cold 25 25 res-fire 20 35 0 +Warsummoner 100 1 1 1 31 ba5 avenger guard 5 5000 ac% 100 125 skilltab 14 1 3 oskill revive 2 2 addxp 3 5 res-pois 35 35 abs-fire 8 10 abs-ltng 8 10 str 15 20 0 +Chimera's Chaos 100 1 3 1 26 ba5 Avenger Guard 5 5000 lyel ac% 100 140 dmg% 25 25 red-dmg% 10 10 res-all 15 15 oskill inferno 6 10 str 20 20 0 +Helms Deep 100 1 1 1 35 ba6 jawbone visor 5 5000 dgrn dgrn ac 100 120 dmg% 30 50 red-dmg% 15 15 res-mag 15 15 indestruct 1 1 all-stats 10 15 hp 50 75 0 +Deadgaze 100 1 3 1 30 ba6 Jawbone Visor 5 5000 dgry blac ac% 100 150 rip 1 1 howl 99 99 manasteal 8 8 mana 35 50 res-ltng 25 25 block 15 15 0 +Malefactor�s Reward 100 1 1 1 40 ba7 lion helm 5 5000 lyel ac% 110 135 bar 2 2 oskill bone armor 3 3 oskill life tap 1 1 regen 3 5 res-pois-len 50 50 half-freeze 1 1 balance2 20 20 0 +Gambler's Glory 100 1 3 1 33 ba7 Lion Helm 5 5000 dgld lpur ac 50 100 gold% 100 100 mag%/lvl 8 bar 1 1 skilltab 12 1 3 balance2 20 20 swing1 15 15 0 +Primal Lust 100 1 1 1 43 ba8 rage mask 5 5000 invhelm31 ac% 150 170 dmg-max 15 20 dmg-min 3 5 balance3 30 30 swing1 15 15 block2 20 20 vit 15 15 str 10 10 0 +Slayer's Glee 100 1 3 1 36 ba8 Rage Mask 5 5000 whit ac% 125 150 skill 147 5 5 skill 152 2 2 bar 1 1 kill-skill decrepify 50 1 gethit-skill 77 25 2 regen 10 10 0 +Hanabal's Crown 100 1 1 1 46 ba9 savage helm 5 5000 cgrn cgrn ac% 150 200 dur 75 75 allskills 2 2 skilltab 13 1 1 cheap 15 15 hp% 10 10 skill-rand 3 126 155 skill-rand 2 126 155 0 +Hellraiser's Casque 100 1 3 1 39 ba9 Savage Helmet 5 5000 lred ac%/lvl 16 rep-dur 9 bar 2 2 heal-kill 10 10 addxp 5 5 sock 2 3 0 +Vanguard 100 1 1 1 51 baa slayer guard 5 5000 blac ac 150 190 bar 1 1 skill-rand 5 126 155 skill-rand 4 126 155 skill-rand 3 126 155 red-dmg 20 30 res-mag 20 30 abs-fire 10 20 block 25 25 0 +Arreat's Face 100 1 3 1 42 baa Slayer Guard 5 5000 bar 2 2 skilltab 12 2 2 ac% 150 200 balance2 30 30 att% 20 20 str 20 20 dex 20 20 lifesteal 3 6 res-all 30 30 0 +Conqueror's Feast 100 1 3 1 50 bab carnage helm 5 5000 lred lred ac/lvl 14 ac 75 100 heal-kill 10 15 regen 10 15 mana 50 50 move2 20 20 sock 2 2 res-all 25 25 0 +Foul Embrace 100 1 1 1 57 bab Carnage Helm 5 5000 dgrn dgrn ac% 150 200 allskills 3 3 dmg-pois 200 1200 1200 res-all 55 75 regen -12 -12 dmg-to-mana 10 10 manasteal 6 8 regen-mana 100 100 0 +Death Knight's Mask 100 1 1 1 66 bac fury visor 5 5000 invhelm35 ac 200 240 skilltab 12 2 4 skilltab 13 2 4 skilltab 14 2 4 lifesteal 10 10 manasteal 8 8 howl 50 50 freeze 3 3 0 +Wolfhowl 100 1 3 1 60 bac fury visor 3 5000 cred cred ac% 120 150 skilltab 14 2 3 str 8 15 dex 8 15 vit 8 15 oskill Wearwolf 3 6 charged Summon Fenris 18 15 oskill Shape Shifting 3 6 oskill Feral Rage 3 6 0 +Wasteland Visage 100 1 1 1 74 bad destroyer helm 5 5000 ac% 150 200 regen -2 -2 bar 2 2 dmg-norm 10 15 hp 60 60 res-cold 50 50 res-fire 40 50 res-ltng 31 40 crush/lvl 4 0 +Demonhorn's Edge 100 1 3 1 70 bad destroyer helm 5 5000 ac% 120 160 swing2 10 10 lifesteal 3 6 thorns 55 77 skilltab 12 1 3 skilltab 13 1 3 skilltab 14 1 3 0 +Insight of the Ancients 100 1 1 1 82 bae conqueror helm 5 5000 dgld dgld ac% 100 140 ethereal 1 1 rep-dur 50 enr 40 40 addxp 20 20 bar 2 2 0 +Halaberd's Reign 100 1 3 1 77 bae conqueror crown 3 5000 ac% 140 170 skilltab 13 1 1 bar 2 2 balance2 20 20 regen 15 23 skill Battle Orders 1 2 skill Battle Command 1 2 sock 2 2 0 +Archon's Ache 100 1 3 1 81 baf guardian crown 5 5000 cblu cblu ac% 180 210 dmg% 35 35 swing2 20 20 crush 33 33 deadly 50 50 wounds/lvl 12 dmg/lvl 4 charged 87 33 15 0 +Conscience of the King 100 1 1 1 87 baf Guardian Crown 5 5000 ac 200 300 res-all 25 25 red-dmg% 15 15 allskills 1 3 mag% 50 50 addxp 5 10 block 20 30 all-stats 20 30 slow 20 30 0 +b36 0 +Druid Pelts 0 +Morning After 100 1 1 1 12 dr1 wolf head 5 5000 ac% 100 120 regen 8 12 skilltab 15 1 2 dmg% 20 20 mana-kill 5 5 sock 1 1 0 +Blood Brother 100 1 3 1 4 dr1 Wolf Head 5 5000 lred lred ac% 50 75 lifesteal 5 8 hp 25 25 mana 20 20 skill 223 1 3 skill 224 1 3 0 +Swift Slaughter 100 1 1 1 16 dr2 hawk helm 5 5000 ac% 105 125 dmg-max 6 10 swing2 20 20 lifesteal 5 5 skilltab 16 1 2 att 75 75 0 +Copperbite 100 1 3 1 8 dr2 Hawk Helm 5 5000 lgld lgld ac% 55 75 ac 10 15 rep-dur 7 res-pois 55 55 dmg-pois 100 100 100 dru 1 1 0 +Yamanda's Token 100 1 1 1 21 dr3 antlers 5 5000 ac% 110 130 dru 1 1 block 15 25 extra-fire 10 10 skill 226 1 3 rep-dur 15 dmg-to-mana 5 10 0 +Silverskin 100 1 3 1 14 dr3 Antlers 5 5000 whit whit ac 35 45 red-dmg 8 10 move2 25 25 skilltab 16 1 3 deadly 15 25 dmg-cold 10 15 freeze 2 2 0 +Voltar's Feather 100 1 1 1 24 dr4 falcon mask 5 5000 dgry dgry ac% 115 135 res-mag 15 15 dmg-ltng 1 75 res-ltng 25 25 enr 25 25 move2 15 15 stam 35 35 0 +Eye of Heaven 100 1 3 1 19 dr4 Falcon Mask 5 5000 ac% 65 100 half-freeze 1 1 enr 15 15 vit 15 15 stam 75 75 cast2 25 25 0 +Lion's Pride 100 1 1 1 30 dr5 spirit mask 5 5000 dyel dyel ac% 120 140 str 20 20 deadly 20 20 dmg-min 5 5 balance2 20 20 str/lvl 6 mag% 30 30 0 +Wraith Whisper 100 1 3 1 25 dr5 Spirit Mask 5 5000 dgry dgry ac% 80 110 dru 1 1 skilltab 17 1 2 light 3 5 ethereal 1 1 rep-dur 15 res-cold/lvl 8 0 +Moonshadow 100 1 3 1 29 dr6 alpha helm 5 5000 blac blac ac% 150 180 fade 1 1 res-ltng 15 30 hp 20 40 heal-kill 10 10 ac/lvl 8 charged 267 15 3 0 +The King's Heart 100 1 1 1 34 dr6 Alpha Helm 5 5000 ac% 100 125 allskills 1 1 res-all 25 25 dmg% 25 25 skill 238 7 7 hp% 20 25 0 +Gathering of Hawks 100 1 1 1 38 dr7 griffon headress 5 5000 lred lred ac 75 100 skill 221 4 4 skill-rand 1 221 250 skill-rand 2 221 250 skill-rand 3 221 250 res-cold/lvl 8 hp/lvl 4 res-fire/lvl 4 0 +Creeper's Canopy 100 1 3 1 33 dr7 Griffon Headress 5 5000 invhelm14 ac% 100 135 skill 223 5 7 skill 232 5 7 skill 242 5 7 howl 75 75 balance2 25 25 regen-mana 50 50 0 +Centaur's Sight 100 1 3 1 37 dr8 hunters guise 5 5000 oran oran ac% 155 185 skill-rand 2 221 250 skill-rand 3 221 250 skilltab 17 2 2 balance2 20 20 ignore-ac 1 1 stamdrain 35 35 dmg-undead 75 75 0 +Night Prowler 100 1 1 1 43 dr8 Hunter's Guise 5 5000 blac blac ac% 120 150 dru 2 2 skilltab 15 1 2 move3 50 50 balance2 25 25 swing2 15 15 block3 40 40 0 +Phoenix Fall 100 1 1 1 46 dr9 sacred feathers 5 5000 cred cred ac% 160 190 dmg-fire 35 100 dru 1 1 extra-fire 10 15 res-all 25 25 light 1 3 sock 1 1 0 +Falcon Sharp 100 1 3 1 40 dr9 Sacred Feathers 5 5000 ac% 125 150 dmg% 50 50 res-all 20 20 dmg-ltng 15 250 dmg-fire 25 60 oskill inner sight 2 4 all-stats 15 15 skilltab 16 1 2 0 +Leader of the Pack 100 1 1 1 50 dra totemic mask 5 5000 dgry dgry ac% 170 200 allskills 1 1 skill-rand 3 221 250 skill-rand 3 221 250 skill 237 3 5 skill 227 3 5 move2 20 20 balance2 20 20 hp% 10 10 0 +Jalal's Mane 100 1 3 1 42 dra Totemic Mask 5 5000 dru 2 2 skilltab 16 2 2 ac% 150 200 balance2 30 30 att% 20 20 str 20 20 enr 20 20 mana-kill 5 5 res-all 30 30 0 +Call of the Wild 100 1 1 1 60 drb blood spirit 5 5000 ac 150 200 skill 227 7 7 skill 237 7 7 skill 247 7 7 allskills 2 2 dmg% 25 50 swing2 25 25 dex 25 25 lifesteal 6 6 0 +Cerebus 100 1 3 1 53 drb blood spirit 5 5000 whit whit ac% 130 140 skilltab 16 2 4 lifesteal 7 10 att% 60 120 openwounds 33 33 skill feral rage 1 2 0 +Eagle Eyes 100 1 1 1 65 drc sun spirit 5 5000 ac 160 210 skilltab 15 3 3 dru 1 1 red-dmg 15 15 charged 17 20 20 res-pois-len 60 60 att% 35 35 sock 1 1 0 +Sanctuary's Scion 100 1 3 1 59 drc Sun Spirit 5 5000 dyel dyel ac% 150 200 dru 2 2 red-dmg% 20 20 red-mag 20 20 mag% 25 50 att% 35 50 skill-rand 7 222 251 0 +Spirit of the Land 100 1 1 1 72 drd earth spirit 5 5000 ac 175 240 red-dmg% 25 25 res-mag 25 25 all-stats 25 25 balance2 25 25 move2 25 25 hp% 25 25 dru 1 1 0 +Spiritkeeper 100 1 3 1 67 drd earth spirit 5 5000 dpur dpur ac% 170 190 balance2 20 20 abs-ltng 9 14 res-fire 30 40 abs-cold% 15 25 res-pois-max 10 10 dru 1 2 mag% 25 25 0 +Hecuba's Tresses 100 1 1 1 79 dre sky spirit 5 5000 ac 240 300 dru 4 4 sock 3 3 0 +Ravenlore 100 1 3 1 74 dre sky spirit 3 5000 dgld dgld ac% 120 150 res-all 15 25 skilltab 17 3 3 enr 20 30 pierce-fire 10 20 skill Raven 7 7 dru 2 2 addxp 5 10 0 +Call of the Kindred 100 1 1 1 86 drf dream spirit 5 5000 invhelm37 ac 200 240 skill 223 3 7 skill 224 3 7 skill 228 3 7 swing1 10 10 hp 50 50 lifesteal 3 6 manasteal 3 6 dru 1 1 0 +Stone Feather 100 1 3 1 80 drf Dream Spirit 5 5000 oran oran ac% 200 300 red-dmg% 25 50 ease -65 -65 sock 3 3 allskills 2 2 0 +b37 0 +Assassin Claws 0 +Killerwatch 100 1 1 15 15 ktr katar 5 5000 dmg-min 10 20 dmg-max 30 40 skilltab 18 1 3 swing1 15 15 charged 90 1 6 0 +Hidden Death 100 1 3 1 7 ktr katar 5 5000 blac blac dmg% 100 150 dmg-max 6 10 deadly 25 35 skill 252 1 2 0 +Simpleton's Shadow 100 1 5 1 3 ktr Katar 5 5000 dgry dgry dmg% 60 75 move2 25 25 lifesteal 3 5 ass 1 1 0 +Razorspine 100 1 1 19 19 wrb wrist blade 5 5000 invclaw1 dmg-min 12 23 dmg-max 32 42 skilltab 19 1 3 thorns 15 15 noheal 1 1 res-pois 15 20 0 +Storms of Spring 100 1 3 1 13 wrb wrist blade 5 5000 whit whit dmg% 105 155 dmg-ltng 10 15 res-ltng 15 25 gethit-skill 38 8 5 block 15 15 0 +Murdering Bloom 100 1 5 1 8 wrb Wrist Blade 5 5000 cred cred dmg% 60 80 ass 1 1 kill-skill charged bolt 25 5 swing1 15 15 sock 1 1 0 +Spirit Hawk 100 1 1 25 25 axf hatchet hands 5 5000 dmg-min 14 25 dmg-max 34 44 skilltab 20 1 3 oskill oak sage 3 5 res-mag 10 15 dmg-mag 25 40 0 +Cold of Winter 100 1 3 1 16 axf hatchet hands 5 5000 whit whit dmg% 110 160 dmg-cold 200 10 15 gethit-skill 44 5 2 swing2 20 20 rep-dur 20 skill 257 1 3 skill 256 1 3 0 +Crimson Cry 100 1 5 1 11 axf Hatchet Hands 5 5000 dred dred dmg% 65 85 bloody 1 1 noheal 1 1 howl 66 66 swing2 25 25 oskill shout 2 2 0 +Samantha's Fist 100 1 1 28 28 ces cestus 5 5000 dmg-min 16 27 dmg-max 36 46 skilltab 19 1 3 swing2 25 25 sock 2 2 0 +Androsphinx 100 1 3 1 20 ces cestus 5 5000 dred dred dmg% 115 165 swing1 15 15 move2 20 20 lifesteal 6 6 ac-hth 200 200 crush 15 15 ass 1 1 0 +Liege Reaver 100 1 5 1 15 ces Cestus 5 5000 cgrn cgrn dmg% 65 90 dmg-norm 5 10 skilltab 20 1 3 rep-dur 25 dur 75 75 att 150 150 openwounds 33 33 0 +Frostshiver 100 1 1 30 30 clw claws 5 5000 lblu lblu dmg-min 22 35 dmg-max 45 70 skilltab 18 1 3 dmg-cold 25 35 freeze 3 3 gethit-skill 40 4 6 charged 55 81 6 res-cold 25 35 0 +Lynx Talon 100 1 3 1 24 clw claws 5 5000 dyel dyel dmg% 120 170 swing3 25 25 mana-kill 3 5 aura 104 1 3 balance2 25 25 dex 20 20 res-fire 25 25 0 +Winterquick 100 1 5 1 19 clw Claws 5 5000 whit whit dmg% 65 100 dmg-cold 300 15 25 swing2 30 30 skill 252 2 2 pierce-cold 15 15 oskill frost nova 5 5 dmg-max 10 15 0 +Nightmare Glove 100 1 1 31 32 btl blade talons 5 5000 blac blac dmg-min 25 35 dmg-max 60 80 skilltab 19 1 3 extra-fire 10 10 extra-ltng 10 10 res-all 15 15 lifesteal 4 7 att% 20 20 0 +Key to the Madhouse 100 1 3 1 28 btl blade talons 5 5000 oran oran dmg% 125 175 dmg-norm 15 30 enr 15 15 mag% 25 35 allskills 1 1 skilltab 18 1 3 sock 2 2 0 +Bladespan 100 1 5 1 22 btl Blade Talons 5 5000 dmg% 70 100 ignore-ac 1 1 ass 1 1 skilltab 19 1 2 slow 20 20 skill-rand 3 251 268 dmg-norm 11 22 0 +Dark Demense 100 1 1 32 38 skr scissors katar 5 5000 dgry dgry dmg-min 35 45 dmg-max 80 100 skilltab 20 1 3 ass 1 1 allskills 1 1 stupidity 2 2 gethit-skill 71 11 4 rep-dur 17 0 +Martial Law 100 1 3 1 33 skr scissors katar 5 5000 dgld dgld dmg% 130 180 skilltab 20 3 5 str 15 15 dex 20 20 vit 25 25 red-dmg% 10 15 crush 20 20 deadly/lvl 6 0 +Troll's Touch 100 1 5 1 27 skr Scissors Katar 5 5000 lgrn lgrn dmg% 80 120 regen 25 25 lifesteal 6 8 manasteal 6 8 regen-mana 75 75 ass 2 2 dmg-norm 15 30 0 +Keys to Hell 100 1 1 55 52 9ar quhab 5 5000 cred cred dmg% 100 100 dmg-min 20 40 dmg-max 50 90 allskills 2 2 block1 25 25 dmg-demon 200 200 charged 88 45 15 skilltab 18 2 2 0 +Storm Demon's Glair 100 1 3 1 37 9ar quhab 5 5000 dyel dyel dmg% 180 230 skilltab 19 1 3 hit-skill 48 6 9 ignore-ac 1 1 oskill vengeance 2 4 regen-mana 25 25 cast1 15 15 0 +Corpseflayer 100 1 5 1 29 9ar Quhab 5 5000 invclaw3 dmg% 135 190 ass 1 1 dmg-undead 250 250 deadly 15 25 skilltab 19 1 3 att 100 250 reanimate 7 10 10 0 +Sabertooth 100 1 1 55 56 9wb wrist spike 5 5000 whit whit dmg% 100 100 dmg-min 20 40 dmg-max 50 90 skilltab 19 1 3 ass 1 1 res-cold 25 35 regen 15 15 str 15 15 red-dmg 10 15 0 +Pleasures of the Perverse 100 1 3 1 41 9wb wrist spike 5 5000 dmg% 190 240 swing3 30 30 ass 2 2 skill 260 1 3 skill 263 1 3 charged 68 25 20 kick/lvl 8 sock 1 1 0 +Dusk Ray 100 1 5 1 32 9wb Wrist Spike 5 5000 oran oran dmg% 140 180 dmg-norm 20 40 allskills 1 1 skilltab 20 1 1 dmg-fire 40 90 aura 109 4 9 swing2 30 30 mana 35 35 0 +Razor Knuckle 100 1 1 55 60 9xf fascia 5 5000 dmg% 100 100 dmg-min 22 40 dmg-max 53 90 skilltab 18 3 3 swing2 30 30 cast2 25 25 thorns/lvl 8 stam 50 50 0 +Heat of Summer 100 1 3 1 43 9xf fascia 5 5000 lred lred dmg% 200 250 dmg-fire 75 150 res-cold 25 50 abs-fire% 10 10 fireskill 5 5 dex 15 15 skill-rand 3 251 280 dmg-norm 15 30 0 +Scarab Cleaver 100 1 5 1 36 9xf Fascia 5 5000 cblu cblu dmg% 150 200 swing2 20 20 ass 2 2 sock 1 2 res-ltng 75 75 abs-ltng 20 25 dmg-demon 150 250 dmg-max 10 10 0 +Fullsuffering 100 1 1 55 63 9cs hand scythe 5 5000 invclaw2 dmg% 100 120 dmg-min 24 40 dmg-max 55 90 skilltab 20 2 2 allskills 2 2 hit-skill 66 20 5 res-fire 25 35 cheap 5 10 mana-kill 6 6 0 +Chi Strike 100 1 3 1 46 9cs hand scythe 5 5000 lpur lpur dmg% 205 255 skill 253 5 7 ass 1 1 deadly 33 33 res-all 20 40 enr 15 25 red-dmg 10 15 heal-kill 8 8 0 +Slithertongue 100 1 5 1 38 9cs Hand Scythe 5 5000 lgrn lgrn dmg% 175 220 res-pois 45 45 res-pois-max 10 20 res-pois-len 75 75 dmg-pois 125 1000 1000 skilltab 21 1 3 dex 15 15 enr 20 20 dmg-max 15 20 0 +Wight Claw 100 1 1 55 65 9lw greater claws 5 5000 blac blac dmg% 110 130 dmg-min 27 40 dmg-max 60 90 skilltab 19 2 2 ass 1 1 lifesteal 10 10 manasteal 10 10 regen 10 10 regen-mana 65 65 0 +Shadowy Places 100 1 3 1 48 9lw greater claws 5 5000 dgry dgry dmg% 215 265 skill-rand 4 251 280 skill-rand 3 251 280 skill-rand 3 251 280 skill-rand 2 251 280 skill-rand 2 251 280 ass 1 1 swing2 20 20 0 +Werewolf Talons 100 1 5 1 40 9lw Greater Claws 5 5000 dmg% 175 240 swing3 30 30 allskills 2 2 oskill shape shifting 5 8 oskill wearwolf 5 8 oskill fury 1 3 att% 25 25 sock 1 2 0 +Dark Nemesis 100 1 1 55 68 9tw greater talons 5 5000 dpur dpur dmg% 120 140 dmg-min 30 40 dmg-max 70 90 skilltab 18 1 3 swing2 25 25 sock 3 3 0 +Adamantine Razors 100 1 3 1 50 9tw greater talons 5 5000 whit whit dmg% 220 270 ass 1 1 dmg-norm 30 60 indestruct 1 1 deadly 44 44 crush 27 27 skilltab 18 1 3 manasteal 6 6 0 +Cutthroat1 100 1 5 1 42 9tw Greater Talons 5 5000 ass 2 2 skilltab 20 1 1 dmg% 150 200 balance2 30 30 att% 20 20 str 20 20 dex 20 20 lifesteal 5 9 dmg-norm 25 50 0 +Path of the Nightwalker 100 1 1 55 70 9qr scissors quhab 5 5000 blac blac dmg% 135 160 dmg-min 30 40 dmg-max 70 90 dmg/lvl 5 att% 35 35 balance2 25 25 all-stats 10 25 rep-dur 20 skill 267 7 7 0 +Fearsome Rumors 100 1 3 1 54 9qr scissors quhab 5 5000 dgrn dgrn dmg% 225 275 dmg-max 30 50 swing3 40 40 skill 260 3 5 skill 263 3 5 sock 1 1 ease -100 -100 hp 75 75 res-pois 30 40 0 +Glitterkill 100 1 5 1 49 9qr Scissors Quhab 5 5000 dmg% 170 225 ass 2 2 res-mag 20 20 res-all 25 25 move2 25 25 skill 258 1 3 skill 264 1 3 rep-dur 5 0 +Wakazashi 100 1 1 1 64 7ar suwayyah 5 5000 whit whit dmg% 250 300 ass 2 2 skilltab 20 1 3 swing3 30 30 ignore-ac 1 1 sock 2 2 0 +Iniquity Razor 100 1 3 1 55 7ar Suwayyah 5 5000 lpur dmg% 200 275 allskills 1 2 mag%/lvl 5 mag% 25 50 deadly 50 75 block 15 25 block3 40 40 skilltab 20 1 3 0 +Biteblade 100 1 1 1 71 7wb wrist sword 5 5000 dred dred dmg% 250 300 dmg-norm 20 40 reduce-ac 15 15 noheal 1 1 skilltab 18 1 2 demon-heal 25 25 skill 260 1 3 0 +Jadetalon 100 1 3 1 66 7wb wrist sword 5 5000 cgrn cgrn dmg% 190 240 manasteal 10 15 res-all 40 50 balance2 30 30 skilltab 19 1 2 skilltab 20 1 2 dmg-pois 150 1500 1500 0 +Nightwrath 100 1 1 1 76 7xf war fist 5 5000 dmg% 250 300 dmg/lvl 6 block 15 25 skill 263 1 3 ass 1 1 fade 1 1 stupidity 1 1 0 +Silent Shank 100 1 3 1 70 7xf War Fist 5 5000 blac blac dmg% 200 260 swing3 35 35 cast2 25 25 regen 10 15 nofreeze 1 1 cheap 20 20 fade 1 3 dmg/lvl 7 0 +Flaming Fist 100 1 1 1 79 7cs battle cestus 5 5000 cred cred dmg% 250 300 allskills 1 1 dmg-fire 200 300 pierce-fire 15 15 abs-fire% 10 10 res-fire-max 15 15 res-fire 60 60 res-cold 35 35 0 +Shadowkiller 100 1 3 1 72 7cs battle cestus 5 5000 lgry lgry invaxfu dmg% 170 220 reduce-ac 25 25 freeze 2 2 mana-kill 10 15 hit-skill Frost Nova 33 8 ethereal 1 1 indestruct 1 1 0 +Death Slaad1 100 1 1 1 82 7lw feral claws 5 5000 dgry dgry dmg% 250 300 dmg-norm 25 50 ass 2 2 dmg-max 25 50 rip 1 1 addxp 5 5 rep-dur 15 0 +Firelizard's Talons 100 1 3 1 75 7lw feral claws 5 5000 dmg% 200 270 swing2 15 15 skilltab 20 1 3 dmg-fire 236 480 res-fire 40 70 skill Wake of Fire Sentry 1 2 skill Inferno Sentry 1 2 ass 1 2 0 +Spellfist 100 1 1 1 85 7tw runic talons 5 5000 oran oran dmg% 250 320 res-mag 25 25 red-mag 25 25 dmg-mag 75 200 oskill battle command 3 3 skilltab 19 1 2 gethit-skill 49 6 12 res-all 25 25 0 +Sin Sister 100 1 3 1 78 7tw Runic Talons 5 5000 blac blac dmg% 200 300 swing2 30 30 sock 3 3 dmg/lvl 7 0 +Avalanche Strike 100 1 1 1 89 7qr scissors suwayyah 5 5000 cblu cblu dmg% 300 400 dmg-cold 300 200 250 hit-skill 55 50 4 res-cold 45 45 rep-dur 15 lifesteal 6 8 manasteal 6 8 0 +Torturer's Trust 100 1 3 1 83 7qr Scissors Suwayyah 5 5000 dmg% 225 300 allskills 2 2 skilltab 20 1 3 res-all 30 50 skill-rand 7 252 281 regen 15 20 gold% 100 100 mag% 25 50 att% 100 100 0 +b38 0 +Rings 0 +Ring of Engagement 100 1 9 3 3 rin ring 5 5000 invring1 dmg-max 2 5 hp 10 20 str 5 5 aura 98 1 1 0 +Nagelring 0 1 9 7 7 rin Ring 5 5000 invring2 red-mag 3 3 thorns 3 7 att 30 50 mag% 10 15 0 +Nameless Fear 100 1 9 11 11 rin ring 5 5000 invring3 dmg% 10 15 swing1 10 10 howl 45 45 noheal 1 1 0 +Manald Heal 0 1 9 15 15 rin Ring 5 5000 invring4 manasteal 4 7 regen 5 8 hp 15 25 regen-mana 25 50 0 +Elven Heartband 100 1 8 18 18 rin ring 5 5000 invring5 addxp 3 5 hp 20 30 red-dmg% 5 5 mag% 15 25 lifesteal 4 6 0 +Knell of Discord 100 1 8 20 20 rin ring 5 5000 invring6 lifesteal 5 7 slow 15 15 knock 3 3 enr 10 10 0 +The Stone of Jordan 0 1 3 25 25 rin Ring 5 5000 invring7 mana 20 20 mana% 25 25 ltng-min 1 1 allskills 1 1 ltng-max 20 20 0 +Vampiric Regeneration 100 1 7 28 28 rin ring 5 5000 invring8 hp 35 35 regen 8 12 regen-mana 35 35 regen-stam 20 20 manasteal 3 5 lifesteal 5 7 0 +Dwarf Star 100 1 7 32 32 rin Ring 5 5000 invring9 gold% 100 100 stam 40 40 regen-stam 15 15 hp 40 40 red-mag 12 15 abs-fire% 15 15 0 +Golem's Might 100 1 6 38 38 rin ring 5 5000 invring10 str 25 25 dmg-norm 10 15 res-all 8 15 heal-kill 5 8 swing1 10 10 0 +Raven Frost 100 1 6 42 42 rin Ring 5 5000 invring11 nofreeze 1 1 dmg-cold 100 15 45 abs-cold% 20 20 mana 40 40 dex 15 20 att 150 250 0 +Planatar Enlightenment 100 1 2 45 45 rin ring 5 5000 invring12 allskills 1 1 res-all 8 15 hp 35 50 gold% 65 65 enr 20 20 addxp 3 5 cheap 8 10 0 +Thief of Dreams 100 1 5 50 50 rin ring 5 5000 invring13 mag% 33 50 res-fire 25 35 mana 65 80 cast1 15 15 addxp 3 3 0 +Bul Katho's Wedding Band 100 1 1 58 58 rin Ring 5 5000 invring14 hp/lvl 4 allskills 1 2 lifesteal 3 5 stam 50 50 0 +Carrion Wind 100 1 4 60 60 rin ring 3 5000 invring15 ac-miss 100 160 lifesteal 6 9 res-pois 55 55 gethit-skill Poison Nova 10 10 charged Plague Poppy 15 21 hit-skill Twister 8 13 dmg-to-mana 10 10 0 +Jackal's Laughter 100 1 3 64 64 rin ring 5 5000 invring16 allskills 1 1 lifesteal 8 11 manasteal 7 8 hp 55 80 oskill terror 6 6 deadly/lvl 3 0 +Nature's Peace 100 1 3 69 69 rin ring 5 5000 invring17 noheal 1 1 rip 1 1 red-dmg 7 11 res-pois 20 30 charged Oak Sage 27 5 0 +Fellowship's Hope 100 1 2 73 73 rin ring 5 5000 invring18 allskills 1 1 mag% 25 35 res-all 15 15 red-dmg 5 8 red-mag 5 8 move1 10 10 cast1 10 10 block1 10 10 0 +Wisp 100 1 1 76 76 rin ring 5 5000 invring19 abs-ltng% 10 20 hit-skill Lightning 10 16 mag% 10 20 charged Oak Sage 15 2 charged Heart of Wolverine 13 5 charged Spirit of Barbs 11 7 0 +Faerie Ring 100 1 1 82 82 rin ring 5 5000 invring20 allskills 1 1 hp% 15 15 mana% 15 15 light 7 7 fade 1 1 block 15 20 mag% 25 50 balance2 20 20 0 +Stone Of Jordan* 100 1 1 85 85 rin ring 5 5000 invring21 allskills 2 2 mana% 40 40 cast1 15 15 dmg-ltng 10 100 0 +Constricting Ring 100 1 1 85 90 rin ring 5 5000 invring22 res-all 100 100 mag% 100 100 regen -30 -30 res-all-max 15 15 0 +b39 0 +Amulets 0 +Eye of Kahn 100 1 9 3 3 amu amulet 5 5000 invammy7 hp 25 35 mana 25 35 dex 5 10 str 5 10 dmg-max 3 6 0 +Psyche Shroud 100 1 9 6 6 amu amulet 5 5000 invammy5 move2 15 15 res-ltng 20 20 hp 20 30 enr 10 10 0 +Nokozan Relic 0 1 8 10 10 amu Amulet 5 5000 lgld invammy21 dmg-fire 3 6 res-fire-max 10 10 res-fire 50 50 light 3 3 balance2 20 20 0 +Amulet of Warding 100 1 7 12 12 amu amulet 5 5000 invammy2 red-dmg 5 8 res-all 10 15 red-mag 4 6 block 5 12 ac 35 50 0 +The Eye of Etlich 0 1 4 15 15 amu Amulet 5 5000 invammy14 ac-miss 10 40 light 1 5 allskills 1 1 lifesteal 3 7 cold-min 1 2 cold-max 3 5 cold-len 50 250 0 +The Mahim-Oak Curio 0 1 7 20 20 amu Amulet 5 5000 lpur invammy20 dex 10 10 str 10 10 enr 10 10 vit 10 10 ac 10 10 att% 10 10 res-all 10 10 ac% 10 10 0 +Cryptking 100 1 6 27 27 amu amulet 5 5000 invammy8 oskill skeleton mastery 5 10 oskill raise skeletal mage 3 5 dmg-undead 200 200 allskills 1 1 regen 6 8 regen-mana 25 50 0 +Sequence of Seasons 100 1 6 30 30 amu amulet 5 5000 invammy4 allskills 1 1 dmg-cold 100 15 25 dmg-ltng 1 70 dmg-fire 30 50 aura 103 1 1 regen 6 8 0 +Saracen's Chance 100 1 6 33 33 amu Amulet 5 5000 dpur dpur invammy22 res-all 15 25 gethit-skill 76 10 2 str 12 12 dex 12 12 enr 12 12 vit 12 12 0 +Rat Lord�s Gate 100 1 6 38 38 amu amulet 5 5000 invammy1 dmg-demon 100 100 hp 35 50 str 15 25 dex 15 25 red-dmg% 10 10 red-mag 6 9 0 +The Cat's Eye 100 1 6 42 42 amu Amulet 5 5000 oran oran invammy19 move2 30 30 swing2 20 20 ac 100 100 ac-miss 100 100 dex 25 25 0 +Luck Sigil 100 1 5 45 45 amu amulet 5 5000 invammy3 allskills 1 1 mag%/lvl 12 gold%/lvl 16 att% 10 10 fade 1 1 0 +Crescent Moon 100 1 5 50 50 amu Amulet 5 5000 invammy12 manasteal 11 15 red-mag 10 10 dmg-to-mana 10 10 light -2 -2 mana 45 45 lifesteal 3 6 0 +Atma's Scarab 100 1 5 55 55 amu Amulet 5 5000 cgrn cgrn invammy17 dmg-pois 100 102 102 res-pois 75 75 light 3 3 thorns 5 5 hit-skill 66 5 2 att% 20 20 0 +The Rising Sun 100 1 5 60 60 amu Amulet 5 5000 lgld lgld invammy18 abs-fire/lvl 6 light 4 4 gethit-skill 56 2 0 dmg-fire 24 48 fireskill 2 2 regen 10 10 0 +Highlord's Wrath 100 1 5 65 65 amu Amulet 5 5000 bwht invammy16 res-ltng 35 35 dmg-ltng 1 30 swing2 20 20 allskills 1 1 deadly/lvl 3 light-thorns 15 15 0 +Seraph's Hymn 100 1 3 67 67 amu amulet 5 5000 invammy15 allskills 2 2 skilltab 11 1 2 dmg-demon 25 50 dmg-undead 25 50 att-demon 150 250 att-undead 150 250 light 2 2 0 +Mara's Kaleidoscope 100 1 4 70 70 amu Amulet 5 5000 invammy11 allskills 2 2 res-all 20 30 str 5 5 dex 5 5 vit 5 5 enr 5 5 0 +The Vanquisher 100 1 3 75 75 amu amulet 5 5000 invammy9 dmg% 50 50 dmg-max 10 20 cast2 20 20 lifesteal 8 8 manasteal 5 8 swing2 25 25 0 +Foulsigil 100 1 3 78 78 amu amulet 5 5000 invammy13 allskills 1 2 all-stats 15 25 regen -3 -3 res-pois 65 65 swing1 15 15 block 15 15 nofreeze 1 1 0 +Metalgrid 100 1 2 81 81 amu amulet 5 5000 invammy10 ac 300 350 res-all 25 35 att 400 450 charged IronGolem 11 22 charged Iron Maiden 20 12 red-dmg% 15 20 0 +Veil of Despair 100 1 1 85 85 amu amulet 5 5000 invammy6 allskills 2 2 block 10 15 kill-skill 5 15 mag% 35 50 gold%/lvl 16 att% 25 25 res-all-max 5 20 res-all 20 50 0 +b40 0 +Charms 0 +Autumn's Avatar 100 1 5 12 12 cm1 charm 1 5 5000 invscm3 hp 12 15 mana 10 12 dmg-max 3 3 str 1 3 0 +Remembrance of Glory 100 1 4 29 29 cm1 charm 1 5 5000 invscm1 hp 20 20 mana 17 17 res-all 5 5 dex 5 5 0 +Argo's Anchor 100 1 3 48 48 cm1 charm 1 5 5000 invscm4 dmg-max 5 5 mag% 7 7 block 5 10 att 50 75 0 +Ogre King's Bowl 100 1 2 60 60 cm1 charm 1 5 5000 invscm2 lifesteal 3 5 manasteal 3 5 regen 3 8 regen-mana 25 50 0 +Annihilus 100 1 1 85 70 cm1 charm 1 3 5000 flpmss invmss item_gem 12 item_gem allskills 1 1 all-stats 10 20 res-all 10 20 addxp 5 10 0 +Peacemaker 100 1 5 15 15 cm2 charm 1 5 5000 hp 20 25 mana 15 20 str 5 5 dex 5 5 0 +Lifesever 100 1 3 55 55 cm2 charm 1 5 5000 allskills 1 1 mag% 15 20 thorns 6 15 0 +Thronegranter 100 1 1 81 81 cm2 charm 1 5 5000 dred dred invlcm1 hp 30 35 mana 25 30 dmg-max 6 8 cheap 3 5 addxp 3 5 0 +Nightdrifter 100 1 5 6 6 cm3 charm 1 5 5000 hp 15 25 dmg-cold 200 5 8 freeze 1 2 move1 15 15 0 +Gheed's Fortune 100 1 3 45 45 cm3 charm 1 3 5000 lgld mag% 20 40 gold% 80 160 cheap 10 15 0 +Queenscall 100 1 1 71 75 cm3 charm 1 5 5000 allskills 1 1 hp 40 44 res-all 11 15 block1 10 10 move1 10 10 swing1 10 10 0 +b41 0 +Jewels 0 +Tiger Eye 100 1 5 5 5 jew jewel 3 5000 dmg% 15 20 dmg-max 5 8 0 +Jade Facet 100 1 5 18 18 jew jewel 3 5000 lgrn dmg% 20 29 dmg-max 8 10 0 +Topaz Facet 100 1 5 27 27 jew jewel 3 5000 lyel gold% 25 25 mag% 10 15 ac 25 50 0 +Emerald Facet 100 1 5 38 38 jew jewel 3 5000 cgrn dmg% 27 34 swing1 15 15 0 +Quartz Facet 100 1 5 43 43 jew jewel 3 5000 hp 30 40 mana 25 35 cast1 10 10 0 +Rainbow Facet 100 1 1 49 49 jew jewel 3 5000 dmg-ltng 1 74 pierce-ltng 3 5 extra-ltng 3 5 death-skill Chain Lightning 100 47 0 +Rainbow Facet1 100 1 1 49 49 jew jewel 3 5000 dmg-cold 3 24 38 pierce-cold 3 5 extra-cold 3 5 death-skill Blizzard 100 37 0 +Rainbow Facet2 100 1 1 49 49 jew jewel 3 5000 dmg-fire 17 45 pierce-fire 3 5 extra-fire 3 5 death-skill Meteor 100 31 0 +Rainbow Facet3 100 1 1 49 49 jew jewel 3 5000 dmg-pois 50 187 187 pierce-pois 3 5 extra-pois 3 5 death-skill Poison Nova 100 51 0 +Rainbow Facet4 100 1 1 49 49 jew jewel 3 5000 dmg-ltng 1 74 pierce-ltng 3 5 extra-ltng 3 5 levelup-skill Nova 100 41 0 +Rainbow Facet5 100 1 1 49 49 jew jewel 3 5000 dmg-cold 3 24 38 pierce-cold 3 5 extra-cold 3 5 levelup-skill Frost Nova 100 43 0 +Rainbow Facet6 100 1 1 49 49 jew jewel 3 5000 dmg-fire 17 45 pierce-fire 3 5 extra-fire 3 5 levelup-skill Blaze 100 29 0 +Rainbow Facet7 100 1 1 49 49 jew jewel 3 5000 dmg-pois 50 187 187 pierce-pois 3 5 extra-pois 3 5 levelup-skill Venom 100 23 0 +Saphire Facet 100 1 1 58 58 jew jewel 3 5000 cblu dmg% 37 40 swing1 15 15 0 +Saphire Facet1 100 1 1 58 58 jew jewel 3 5000 cblu dmg% 37 40 dmg-max 12 15 0 +Diamond Facet 100 1 1 67 67 jew jewel 3 5000 whit res-all 12 15 cast1 10 10 red-dmg% 3 5 block 6 8 0 +Adamantine Facet 100 1 1 71 71 jew jewel 3 5000 red-dmg% 5 7 red-dmg 8 12 red-mag 8 10 ac% 25 35 0 +Star Facet 100 1 1 76 76 jew jewel 3 5000 dmg% 40 40 swing1 15 15 dmg-max 15 15 0 +Heaven Facet 100 1 1 85 85 jew jewel 3 5000 allskills 1 1 hp% 5 5 mana% 5 5 0 +b42 0 +Quest Items 0 +Amulet of the Viper 0 1 1 1 0 vip Amulet 5 5000 lgry mana 10 10 res-pois 25 25 hp 10 10 0 0 0 0 0 0 0 0 0 +Staff of Kings 0 1 1 1 0 msf Staff 5 5000 dgry res-all 10 10 swing3 50 50 0 0 0 0 0 0 0 0 0 0 0 +Horadric Staff 0 1 1 1 0 hst Staff 5 5000 blac mana 10 10 res-pois 25 25 hp 10 10 res-all 10 10 swing3 50 50 0 0 0 0 0 +Hell Forge Hammer 0 1 1 1 0 hfh Hammer 5 5000 cred fire-min 5 5 fire-max 20 20 res-fire 40 40 ac 35 35 0 0 0 0 0 0 0 +KhalimFlail 0 1 1 1 0 qf1 Flail 5 5000 dblu ltng-min 1 1 ltng-max 20 20 swing3 50 50 att 40 40 0 0 0 0 0 0 0 +SuperKhalimFlail 0 1 1 1 0 qf2 Flail 5 5000 dblu ltng-min 1 1 ltng-max 40 40 swing3 50 50 att 40 40 manasteal 6 6 lifesteal 6 6 0 0 0 diff --git a/Weapons.txt b/Weapons.txt new file mode 100644 index 0000000..c7f1e04 --- /dev/null +++ b/Weapons.txt @@ -0,0 +1,308 @@ +name type type2 code alternateGfx namestr version compactsave rarity spawnable mindam maxdam 1or2handed 2handed 2handmindam 2handmaxdam minmisdam maxmisdam rangeadder speed StrBonus DexBonus reqstr reqdex durability nodurability level levelreq cost gamble cost magic lvl auto prefix OpenBetaGfx normcode ubercode ultracode wclass 2handedwclass component hit class invwidth invheight stackable minstack maxstack spawnstack flippyfile invfile uniqueinvfile setinvfile hasinv gemsockets gemapplytype special useable dropsound dropsfxframe usesound unique transparent transtbl quivered lightradius belt quest questdiffcheck missiletype durwarning qntwarning gemoffset bitfield1 CharsiMin CharsiMax CharsiMagicMin CharsiMagicMax CharsiMagicLvl GheedMin GheedMax GheedMagicMin GheedMagicMax GheedMagicLvl AkaraMin AkaraMax AkaraMagicMin AkaraMagicMax AkaraMagicLvl FaraMin FaraMax FaraMagicMin FaraMagicMax FaraMagicLvl LysanderMin LysanderMax LysanderMagicMin LysanderMagicMax LysanderMagicLvl DrognanMin DrognanMax DrognanMagicMin DrognanMagicMax DrognanMagicLvl HraltiMin HraltiMax HraltiMagicMin HraltiMagicMax HraltiMagicLvl AlkorMin AlkorMax AlkorMagicMin AlkorMagicMax AlkorMagicLvl OrmusMin OrmusMax OrmusMagicMin OrmusMagicMax OrmusMagicLvl ElzixMin ElzixMax ElzixMagicMin ElzixMagicMax ElzixMagicLvl AshearaMin AshearaMax AshearaMagicMin AshearaMagicMax AshearaMagicLvl CainMin CainMax CainMagicMin CainMagicMax CainMagicLvl HalbuMin HalbuMax HalbuMagicMin HalbuMagicMax HalbuMagicLvl JamellaMin JamellaMax JamellaMagicMin JamellaMagicMax JamellaMagicLvl LarzukMin LarzukMax LarzukMagicMin LarzukMagicMax LarzukMagicLvl DrehyaMin DrehyaMax DrehyaMagicMin DrehyaMagicMax DrehyaMagicLvl MalahMin MalahMax MalahMagicMin MalahMagicMax MalahMagicLvl Source Art Game Art Transform InvTrans SkipName NightmareUpgrade HellUpgrade Nameable PermStoreItem +Hand Axe axe hax hax hax 0 3 1 3 6 100 14 3 0 0 0 hax hax 9ha 7ha 1hs 1hs 5 1hsl 1 3 flphax invhax invhaxu invhaxu 1 4 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 0 0 3 2 2 0 axe 2ax 1 0 +Axe axe axe axe axe 0 4 1 4 11 1 10 100 32 12 7 0 0 0 axe axe 9ax 7ax 1hs 1hs 5 1hsl 2 3 flpaxe invaxe invaxeu invaxeu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 2ax mpi 1 0 +Double Axe axe 2ax axe 2ax 0 4 1 5 13 1 10 100 43 12 13 0 0 0 axe 2ax 92a 72a 1hs 1hs 5 1hsl 2 3 flp2ax inv2ax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 mpi wax 1 0 +Military Pick axe mpi axe mpi 0 4 1 7 11 1 -10 100 49 33 13 19 0 0 0 axe mpi 9mp 7mp 1hs 1hs 5 1hsl 2 3 flpmpi invmpi invmpiu invmpiu 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 wax xxx 1 0 +War Axe axe wax hax wax 0 4 1 10 18 2 100 67 13 25 0 0 0 hax wax 9wa 7wa 1hs 1hs 5 1hsl 2 3 flpwax invwax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Large Axe axe lax lax lax 0 4 1 1 6 13 1 -10 100 35 15 6 0 0 0 lax lax 9la 7la stf stf 5 2hsl 2 3 flplax invlax 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 btx gax 1 0 +Broad Axe axe bax lax bax 0 4 1 1 10 18 1 100 48 18 12 0 0 0 lax bax 9ba 7ba stf stf 5 2hsl 2 3 flpbrx invbrx 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 gax gix 1 0 +Battle Axe axe btx btx btx 0 4 1 1 12 32 1 10 100 54 20 17 0 0 0 lax btx 9bt 7bt stf stf 5 2hsl 2 3 flpbtx invbtx invbtxu invbtxu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 5 2 0 gix xxx 1 0 +Great Axe axe gax btx gax 0 4 1 1 9 30 2 -10 100 63 39 25 23 0 0 0 lax gax 9ga 7ga stf stf 5 2hsl 2 4 flpgax invgax invgaxu invgaxu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Giant Axe axe gix gix gix 0 4 1 1 22 45 3 10 100 70 25 27 0 0 0 lax gix 9gi 7gi stf stf 5 2hsl 2 3 flpgix invgix 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Wand wand wnd wnd wnd 0 1 1 2 4 100 8 2 0 0 0 1 wnd wnd 9wn 7wn 1hs 1hs 5 1hss 1 2 flpwnd invwnd invwndu invwndu 1 3 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 ywn bwn 1 0 +Yew Wand wand ywn ywn ywn 0 1 1 2 8 10 100 8 12 0 0 0 1 wnd ywn 9yw 7yw 1hs 1hs 5 1hss 1 2 flpywn invywn 1 3 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 bwn gwn 1 0 +Bone Wand wand bwn bwn bwn 0 1 1 3 7 -20 100 8 18 0 0 0 1 wnd bwn 9bw 7bw 1hs 1hs 5 1hss 1 2 flpbwn invbwn invbwnu invbwnu 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Grim Wand wand gwn bwn gwn 0 1 1 5 11 100 8 26 0 0 0 1 wnd gwn 9gw 7gw 1hs 1hs 5 1hss 1 2 flpgwn invgwn 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Club club clb clb clb 0 1 1 1 6 -10 100 12 1 0 0 0 clb clb 9cl 7cl 1hs 1hs 5 club 1 3 flpclb invclb invclbu invclbu 1 4 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 5 2 0 mac mst 1 0 +Scepter scep scp mac scp 0 2 1 6 11 100 25 25 3 0 0 0 mac scp 9sc 7sc 1hs 1hs 5 club 1 3 flpscp invscp 1 4 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 gsc wsp 1 0 +Grand Scepter scep gsc mac gsc 0 4 1 8 18 1 10 100 37 30 15 0 0 0 mac gsc 9qs 7qs 1hs 1hs 5 club 1 3 flpgsc invgsc 1 5 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx wsp 1 0 +War Scepter scep wsp whm wsp 0 4 1 10 17 1 -10 100 55 35 21 0 0 0 mac wsp 9ws 7ws 1hs 1hs 5 club 2 3 flpwsp invwsp 1 7 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Spiked Club club spc clb spc 0 3 1 5 8 1 100 18 4 0 0 0 clb spc 9sp 7sp 1hs 1hs 5 1hsl 1 3 flpspc invspc invspcu invspcu 1 4 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 2 2 0 mac mst 1 0 +Mace mace mac mac mac 0 4 1 3 10 100 27 30 8 0 0 0 mac mac 9ma 7ma 1hs 1hs 5 1hsl 1 3 flpmac invmac 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 mst mau 1 0 +Morning Star mace mst mac mst 0 4 1 7 16 1 10 100 36 36 13 0 0 0 mac mst 9mt 7mt 1hs 1hs 5 1hsl 1 3 flpmst invmst invmstu invmstu 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx fla 1 0 +Flail mace fla fla fla 0 4 1 1 24 2 -10 100 41 35 15 19 0 0 0 mac fla 9fl 7fl 1hs 1hs 5 1hsl 2 3 flpfla invfla 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx gma 1 0 +War Hammer hamm whm whm whm 0 4 1 19 29 20 110 53 28 25 0 0 0 mac whm 9wh 7wh 1hs 1hs 5 1hsl 2 3 flpwhm invwhm 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 178 7 0 0 3 2 2 0 mau gma 1 0 +Maul hamm mau mau mau 0 4 1 1 30 43 1 10 110 69 30 21 0 0 0 mau mau 9m9 7m7 stf stf 5 2hsl 2 4 flpmau invmau invmauu invmauu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx mau 1 0 +Great Maul hamm gma mau gma 0 3 1 1 38 58 2 20 110 99 30 32 0 0 0 mau gma 9gm 7gm stf stf 5 2hsl 2 3 flpgma invgma invgma invgma 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Short Sword swor ssd ssd ssd 0 3 1 2 7 100 12 1 0 0 0 ssd ssd 9ss 7ss 1hs 1hs 5 1hsl 1 3 flpssd invssd 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 scm wsd 1 0 +Scimitar swor scm scm scm 0 3 1 2 6 -20 100 21 11 5 0 0 0 scm scm 9sm 7sm 1hs 1hs 5 1hsl 1 3 flpscm invscm invscmu invscmu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 sbr lsd 1 0 +Saber swor sbr scm sbr 0 3 1 3 8 -10 100 25 25 16 8 0 0 0 scm sbr 9sb 7sb 1hs 1hs 5 1hsl 1 3 flpsbr invsbr inv9sbu inv9sbu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 flc bsd 1 0 +Falchion swor flc flc flc 0 4 1 9 17 20 100 33 16 11 0 0 0 scm flc 9fc 7fc 1hs 1hs 5 1hsl 1 3 flpflc invflc invflcu invflcu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 bsd wsd 1 0 +Crystal Sword swor crs crs crs 0 2 1 5 15 1 100 43 10 11 0 0 0 ssd crs 9cr 7cr 1hs 1hs 5 1hsl 2 3 flpcrs invcrs invcrsu invcrsu 1 7 0 0 item_sword 12 item_sword 0 0 5 0 5 0 0 7 0 0 1 1 8 0 xxx xxx 1 0 +Broad Sword swor bsd bsd bsd 0 4 1 7 14 100 48 16 15 0 0 0 ssd bsd 9bs 7bs 1hs 1hs 5 1hsl 2 3 flpbsd invbsd invbsdu invbsdu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 lsd wsd 1 0 +Long Sword swor lsd lsd lsd 0 4 1 3 19 1 -10 100 55 39 22 20 0 0 0 ssd lsd 9ls 7ls 1hs 1hs 5 1hsl 2 3 flplsd invlsd invlsdu invlsdu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 wsd wsd 1 0 +War Sword swor wsd flc wsd 0 4 1 8 20 1 100 71 45 22 27 0 0 0 ssd wsd 9wd 7wd 1hs 1hs 5 1hsl 1 3 flpwsd invwsd 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Two-Handed Sword swor 2hs clm 2hs 0 4 1 2 9 1 1 8 17 2 100 35 27 22 10 0 0 0 clm 2hs 92h 72h 1hs 2hs 5 2hss 1 4 flp2hs inv2hs inv2hsu inv2hsu 1 5 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 clm gis 1 0 +Claymore swor clm clm clm 0 4 1 5 12 1 1 13 30 2 10 100 47 25 17 0 0 0 clm clm 9cm 7cm 1hs 2hs 5 2hss 1 4 flpclm invclm 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 gis bsw 1 0 +Giant Sword swor gis gsd gis 0 4 1 3 16 1 1 9 28 2 100 56 34 25 21 0 0 0 clm gis 9gs 7gs 1hs 2hs 5 2hss 1 4 flpgis invgis invgisu invgisu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 bsw flb 1 0 +Bastard Sword swor bsw clm bsw 0 4 1 7 19 1 1 20 28 1 10 100 62 20 24 0 0 0 clm bsw 9b9 7b7 1hs 2hs 5 2hss 1 4 flpbsw invbsw invbswu invbswu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Flamberge swor flb clm flb 0 4 1 9 15 1 1 13 26 2 -10 100 70 49 25 27 0 0 0 clm flb 9fb 7fb 1hs 2hs 5 2hss 2 4 flpflb invflb 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Great Sword swor gsd gsd gsd 0 4 1 12 20 1 1 25 42 2 10 100 100 60 25 33 0 0 0 clm gsd 9gd 7gd 1hs 2hs 5 2hss 2 4 flpgsd invgsd invgsdu invgsdu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Dagger knif dgr dgr dgr 0 2 1 1 4 -20 75 75 8 3 0 0 0 dgr dgr 9dg 7dg 1ht 1ht 5 1ht 1 2 flpdgr invdgr 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 kri bld 1 0 +Dirk knif dir dir dir 0 2 1 3 9 75 75 25 10 9 0 0 0 dgr dir 9di 7di 1ht 1ht 5 1ht 1 2 flpdir invdir 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 kri bld 1 0 +Kriss knif kri dir kri 0 2 1 2 11 -20 75 75 45 12 17 0 0 0 dgr kri 9kr 7kr 1ht 1ht 5 1ht 1 3 flpkrs invkrs invkrsu invkrsu 1 5 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Blade knif bld dgr bld 0 3 1 4 15 -10 75 75 35 51 12 23 0 0 0 dgr bld 9bl 7bl 1ht 1ht 5 1ht 1 3 flpbld invbld 1 4 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Throwing Knife tkni tkf dgr tkf 0 2 1 2 3 4 9 75 75 21 9 2 0 0 0 dgr tkf 9tk 7tk 1ht 1ht 5 1ht 1 2 1 40 160 75 flptkn invtkn 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 bkf bkf 1 0 +Throwing Axe taxe tax hax tax 0 2 1 4 7 8 12 10 75 75 40 12 7 0 0 0 hax tax 9ta 7ta 1hs 1hs 5 1hsl 1 2 1 16 130 32 flptax invtax 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 5 0 3 2 2 0 bal bal 1 0 +Balanced Knife tkni bkf dgr bkf 0 2 1 1 8 6 11 -20 75 75 51 12 13 0 0 0 dgr bkf 9bk 7bk 1ht 1ht 5 1ht 1 2 1 30 160 60 flpbkf invbkf 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 xxx xxx 1 0 +Balanced Axe taxe bal hax bal 0 2 1 5 10 12 15 -10 75 75 57 15 16 0 0 0 hax bal 9b8 7b8 1hs 1hs 5 1hsl 2 3 1 16 130 24 flpbal invbal 1 4 0 primarily thrown 0 item_sword 12 item_sword 0 0 5 0 0 0 35 7 5 0 3 2 2 0 xxx xxx 1 0 +Javelin jave jav jav jav 0 4 1 1 5 6 14 15 2 -10 75 75 12 1 0 0 0 jav jav 9ja 7ja 1ht 1ht 5 1ht 1 3 1 30 60 60 flpjav invjav 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 1 7 2 0 3 5 8 0 glv glv 1 0 +Pilum jave pil pil pil 0 4 1 4 9 7 20 15 2 75 75 45 9 10 0 0 0 jav pil 9pi 7pi 1ht 1ht 5 1ht 1 3 1 25 50 50 flppil invpil 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 371 7 2 0 3 5 8 0 tsp tsp 1 0 +Short Spear jave ssp jav ssp 0 4 1 2 13 10 22 15 2 10 75 75 40 40 9 15 0 0 0 jav ssp 9s9 7s7 1ht 1ht 5 1ht 1 3 1 20 40 40 flpssp invssp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 1 7 2 0 3 5 8 0 tsp tsp 1 0 +Glaive jave glv glv glv 0 4 1 5 17 16 22 15 2 20 75 75 52 35 12 23 0 0 0 jav glv 9gl 7gl 1ht 1ht 5 1ht 1 4 1 15 40 40 flpglv invglv 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 1 2 0 xxx xxx 1 0 +Throwing Spear jave tsp pil tsp 0 4 1 5 15 12 30 15 2 -10 75 75 65 12 29 0 0 0 jav tsp 9ts 7ts 1ht 1ht 5 1ht 1 4 1 32 80 80 flptsp invtsp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 371 7 2 0 3 1 2 0 xxx xxx 1 0 +Spear spea spr spr spr 0 4 1 1 3 15 15 3 -10 100 20 15 5 0 0 0 spr spr 9sr 7sr 2ht 2ht 5 2ht 2 4 flpspr invspr 1 5 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 spt pik 1 0 +Trident spea tri tri tri 0 4 1 1 9 15 15 3 100 38 24 18 9 0 0 0 spr tri 9tr 7tr 2ht 2ht 5 2ht 2 4 flptri invtri invtriu invtriu 1 6 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 spt pik 1 0 +Brandistock spea brn brn brn 0 4 1 1 7 17 15 4 -20 100 40 50 14 16 0 0 0 spr brn 9br 7br 2ht 2ht 5 2ht 2 4 flpbrn invbrn 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx pik 1 0 +Spetum spea spt tri spt 0 4 1 1 15 23 15 4 100 54 35 14 20 0 0 0 spr spt 9st 7st 2ht 2ht 5 2ht 2 4 flpspt invspt 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Pike spea pik pik pik 0 4 1 1 14 63 15 4 20 100 60 45 13 24 0 0 0 spr pik 9p9 7p7 2ht 2ht 5 2ht 2 4 flppik invpik 1 7 0 3 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Bardiche pole bar hal bar 0 4 1 1 1 27 2 10 100 40 25 5 0 0 0 hal bar 9b7 7o7 stf stf 5 2hsl 2 4 flpbar invbar 1 5 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 hal wsc 1 0 +Voulge pole vou hal vou 0 4 1 1 6 21 2 100 50 25 11 0 0 0 hal vou 9vo 7vo stf stf 5 2hsl 2 4 flpvou invvou 1 6 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 hal wsc 1 0 +Scythe pole scy scy scy 0 4 1 1 8 20 1 -10 100 41 41 33 15 0 0 0 hal scy 9s8 7s8 stf stf 5 2hsl 2 4 flpscy invscy invscyu invscyu 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 hal wsc 1 0 +Poleaxe pole pax hal pax 0 4 1 1 18 39 3 10 100 62 33 21 0 0 0 hal pax 9pa 7pa stf stf 5 2hsl 2 4 flppax invpax 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Halberd pole hal pax hal 0 4 1 1 12 45 4 100 75 47 28 29 0 0 0 hal hal 9h9 7h7 stf stf 5 2hsl 2 4 flphal invhal 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +War Scythe pole wsc scy wsc 0 3 1 1 15 36 4 -10 100 80 80 28 34 0 0 0 hal wsc 9wc 7wc stf stf 5 2hsl 2 4 flpwsc invwsc 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Short Staff staf sst bst sst 0 2 1 1 1 5 1 -10 100 10 1 0 0 0 1 bst sst 8ss 6ss stf stf 5 club 1 3 flpsst invsst 1 4 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 cst bst 1 0 +Long Staff staf lst sst lst 0 2 1 1 2 8 1 100 15 8 0 0 0 1 bst lst 8ls 6ls stf stf 5 staf 1 4 flplst invlst 1 5 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 cst wst 1 0 +Gnarled Staff staf cst cst cst 0 2 1 1 4 12 1 10 100 18 12 0 0 0 1 bst cst 8cs 6cs stf stf 5 staf 1 4 flpcst invcst invcstu invcstu 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx wst 1 0 +Battle Staff staf bst sst bst 0 2 1 1 6 13 1 100 20 17 0 0 0 1 bst bst 8bs 6bs stf stf 5 staf 1 4 flpbst invbst 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +War Staff staf wst lst wst 0 2 1 1 12 28 1 20 100 25 24 0 0 0 1 bst wst 8ws 6ws stf stf 5 staf 2 4 flpwst invwst 1 7 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Short Bow bow sbw sbw sbw 0 2 1 1 1 4 13 5 100 15 10 1 1 0 0 0 sbw sbw 8sb 6sb bow bow 6 bow 2 3 flpsbw invsbw 1 5 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 hbw cbw 1 0 +Hunter's Bow bow hbw sbw hbw 0 2 1 1 2 6 13 -10 100 28 16 1 5 0 0 0 sbw hbw 8hb 6hb bow bow 6 bow 2 3 flphbw invhbw 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 lbw sbb 1 0 +Long Bow bow lbw lbw lbw 0 2 1 1 3 10 13 100 22 19 14 1 8 0 0 0 sbw lbw 8lb 6lb bow bow 6 bow 2 4 flplbw invlbw 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 cbw lbb 1 0 +Composite Bow bow cbw lbw cbw 0 2 1 1 4 8 13 -10 100 25 35 18 1 12 0 0 0 sbw cbw 8cb 6cb bow bow 6 bow 2 3 flpcbw invcbw invcbwu invcbwu 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 sbb swb 1 0 +Short Battle Bow bow sbb sbb sbb 0 2 1 1 5 11 13 100 30 40 20 1 18 0 0 0 sbw sbb 8s8 6s7 bow bow 6 bow 2 3 flpsbb invsbb invsbbu invsbbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 lbb lwb 1 0 +Long Battle Bow bow lbb lbb lbb 0 2 1 1 3 18 13 10 100 40 50 22 1 23 0 0 0 sbw lbb 8l8 6l7 bow bow 6 bow 2 4 flplbb invlbb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 swb lwb 1 0 +Short War Bow bow swb sbb swb 0 2 1 1 6 14 13 100 35 55 24 1 27 0 0 0 sbw swb 8sw 6sw bow bow 6 bow 2 3 flpswb invswb invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 lwb xxx 1 0 +Long War Bow bow lwb lbb lwb 0 2 1 1 3 23 13 10 100 50 65 28 1 31 0 0 0 sbw lwb 8lw 6lw bow bow 6 bow 2 4 flplwb invlwb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Light Crossbow xbow lxb lxb lxb 0 2 1 1 6 9 -10 100 21 27 15 1 6 0 0 0 lxb lxb 8lx 6lx xbw xbw 5 xbow 2 3 flplxb invlxb invlxbu invlxbu 1 5 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 mxb rxb 1 0 +Crossbow xbow mxb lxb mxb 0 2 1 1 9 16 100 40 33 20 1 15 0 0 0 lxb mxb 8mx 6mx xbw xbw 5 xbow 2 3 flpmxb invmxb invmxbu invmxbu 1 6 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 hxb hxb 1 0 +Heavy Crossbow xbow hxb hxb hxb 0 2 1 1 14 26 10 100 60 40 25 1 24 0 0 0 lxb hxb 8hx 6hx xbw xbw 5 xbow 2 4 flphxb invhxb invhxbu invhxbu 1 7 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Repeating Crossbow xbow rxb hxb rxb 0 2 1 1 6 12 -40 100 40 50 20 1 33 0 0 0 lxb rxb 8rx 6rx xbw xbw 5 xbow 2 3 flprxb invrxb invrxbu invrxbu 1 7 0 fires 5 shots before reload 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Rancid Gas Potion tpot gps gpl gps 0 1 0 1 1 1 1 0 0 0 gpl gps 1ht 1ht 5 hth 1 1 1 1 6 4 flpgpl invgpl 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 49 7 3 0 0 0 0 0 xxx xxx 0 1 +Oil Potion tpot ops opl ops 0 1 0 1 1 1 1 0 0 0 opl ops 1ht 1ht 5 hth 1 1 1 1 6 4 flpopl invopl 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 46 7 3 0 0 0 0 0 xxx xxx 0 1 +Choking Gas Potion tpot gpm gps gpm 0 1 0 1 1 1 1 0 0 0 gps gpm 1ht 1ht 5 hth 1 1 1 1 6 4 flpgps invgpm 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 48 7 3 0 0 0 0 0 xxx xxx 0 1 +Exploding Potion tpot opm opl opm 0 1 0 1 1 1 1 0 0 0 opl opm 1ht 1ht 5 hth 1 1 1 1 6 4 flpops invopm 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 45 7 3 0 0 0 0 0 xxx xxx 0 1 +Strangling Gas Potion tpot gpl gps gpl 0 1 0 1 1 1 1 0 0 0 gps gpl 1ht 1ht 5 hth 1 1 1 1 6 4 flpgps invgps 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 47 7 3 0 0 0 0 0 xxx xxx 0 1 +Fulminating Potion tpot opl ops opl 0 1 0 1 1 1 1 0 0 0 ops opl 1ht 1ht 5 hth 1 1 1 1 6 4 flpops invops 1 0 0 item_potion 14 item_potion 0 0 5 0 1 0 44 7 3 0 0 0 0 0 xxx xxx 0 1 +decoy dagger knif d33 dgr d33 0 1 2 -20 100 15 20 5 0 0 0 0 dgr d33 1ht 1ht 5 1ht 1 2 flpd33 invd33 1 0 0 item_smallmetalweapon 12 item_smallmetalweapon 1 0 5 0 0 0 18 1 0 7 0 0 3 1 2 0 xxx xxx 0 0 +Gidbinn knif g33 dgr g33 0 3 7 -20 100 15 25 15 0 0 0 0 dgr g33 1ht 1ht 5 1ht 1 2 flpg33 invg33 1 0 0 item_smallmetalweapon 12 item_smallmetalweapon 1 0 5 0 0 0 18 1 0 7 0 0 3 1 2 0 xxx xxx 0 0 +Wirt's Leg club leg clb leg 0 2 8 -10 100 33 0 0 0 0 clb leg 1hs 1hs 5 1hsl 1 3 flpleg invleg 1 5 0 0 item_staff 12 item_staff 0 0 5 0 0 0 1 1 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Horadric Malus hamm hdm whm hdm 0 6 15 20 100 15 15 28 0 0 0 0 whm hdm 1hs 1hs 5 1hsl 1 2 flphmr invhmr 1 0 0 item_sword 12 item_sword 1 0 5 0 0 0 4 1 0 7 0 0 0 2 2 0 xxx xxx 0 0 +Hellforge Hammer hamm hfh whm hfh 0 6 15 100 28 0 0 0 0 whm hfh 1hs 1hs 5 1hsl 2 3 flphmr invhfh 1 7 0 0 item_sword 12 item_sword 1 0 5 0 0 0 25 1 0 7 0 0 1 2 2 1 xxx xxx 0 0 +Horadric Staff staf hst bst hst 0 1 12 20 2 100 30 25 0 0 0 0 bst hst stf stf 5 staf 1 4 flphst invhst 1 0 magically charged 0 item_staff 12 item_staff 1 0 5 0 0 0 10 1 0 7 0 0 5 5 8 1 xxx xxx 0 0 +Staff of the Kings staf msf bst msf 0 1 10 15 2 100 25 23 0 0 0 0 bst msf stf stf 5 staf 1 3 flpmsf invmsf 1 0 magically charged 0 item_staff 12 item_staff 1 0 5 0 0 0 10 1 0 7 0 0 5 5 8 1 xxx xxx 0 0 +Hatchet axe 9ha hax 9ha 0 3 1 10 21 100 25 25 14 31 19 0 0 hax hax 9ha 7ha 1hs 1hs 5 1hsl 1 3 flphax invhax invhaxu invhaxu 1 4 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 0 0 3 5 2 0 xxx xxx 1 0 +Cleaver axe 9ax axe 9ax 0 4 1 10 33 1 10 100 68 12 34 22 0 0 axe axe 9ax 7ax 1hs 1hs 5 1hsl 2 3 flpaxe invaxe invaxeu invaxeu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +Twin Axe axe 92a axe 92a 0 4 1 13 38 1 10 100 85 12 39 25 0 0 axe 2ax 92a 72a 1hs 1hs 5 1hsl 2 3 flp2ax inv2ax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +Crowbill axe 9mp axe 9mp 0 4 1 14 34 1 -10 100 94 70 13 43 25 0 0 axe mpi 9mp 7mp 1hs 1hs 5 1hsl 2 3 flpmpi invmpi invmpiu invmpiu 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Naga axe 9wa hax 9wa 0 4 1 16 45 2 100 121 13 48 25 0 0 hax wax 9wa 7wa 1hs 1hs 5 1hsl 2 3 flpwax invwax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Military Axe axe 9la lax 9la 0 4 1 1 14 34 1 -10 100 73 15 34 22 0 0 lax lax 9la 7la stf stf 5 2hsl 2 3 flplax invlax 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Bearded Axe axe 9ba lax 9ba 0 4 1 1 21 49 1 100 92 18 38 25 0 0 lax bax 9ba 7ba stf stf 5 2hsl 2 3 flpbrx invbrx 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Tabar axe 9bt btx 9bt 0 4 1 1 24 77 1 10 100 101 20 42 25 0 0 btx btx 9bt 7bt stf stf 5 2hsl 2 3 flpbtx invbtx inv9btu inv9btu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +Gothic Axe axe 9ga btx 9ga 0 4 1 1 18 70 2 -10 100 115 79 25 46 25 0 0 btx gax 9ga 7ga stf stf 5 2hsl 2 4 flpgax invgax invgaxu invgaxu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Ancient Axe axe 9gi gix 9gi 0 4 1 1 43 85 3 10 100 125 25 51 25 0 0 gix gix 9gi 7gi stf stf 5 2hsl 2 3 flpgix invgix inv9giu inv9giu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Burnt Wand wand 9wn wnd 9wn 0 1 1 8 18 100 25 8 31 19 0 0 1 wnd wnd 9wn 7wn 1hs 1hs 5 1hss 1 2 flpwnd invwnd 1 3 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Petrified Wand wand 9yw ywn 9yw 0 1 1 8 24 10 100 25 8 38 25 0 0 1 ywn ywn 9yw 7yw 1hs 1hs 5 1hss 1 2 flpywn invywn 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Tomb Wand wand 9bw bwn 9bw 0 1 1 10 22 -20 100 25 8 43 25 0 0 1 bwn bwn 9bw 7bw 1hs 1hs 5 1hss 1 2 flpbwn invbwn invbwnu invbwnu 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Grave Wand wand 9gw bwn 9gw 0 1 1 13 29 100 25 8 49 25 0 0 1 bwn gwn 9gw 7gw 1hs 1hs 5 1hss 1 2 flpgwn invgwn inv9gwu inv9gwu 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Cudgel club 9cl clb 9cl 0 1 1 6 21 -10 100 25 12 30 18 0 0 clb clb 9cl 7cl 1hs 1hs 5 club 1 3 flpclb invclb invclbu invclbu 1 4 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 5 2 0 xxx xxx 1 0 +Rune Scepter scep 9sc mac 9sc 0 2 1 13 24 100 58 25 31 19 0 0 mac scp 9sc 7sc 1hs 1hs 5 club 1 3 flpscp invscp 1 4 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Holy Water Sprinkler scep 9qs mac 9qs 0 4 1 14 36 1 10 100 76 30 40 25 0 0 mac gsc 9qs 7qs 1hs 1hs 5 club 1 3 flpgsc invgsc 1 5 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Divine Scepter scep 9ws whm 9ws 0 4 1 16 38 1 -10 100 103 35 45 25 0 0 whm wsp 9ws 7ws 1hs 1hs 5 club 2 3 flpwsp invwsp 1 7 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Barbed Club club 9sp clb 9sp 0 3 1 13 25 1 100 30 18 32 20 0 0 clb spc 9sp 7sp 1hs 1hs 5 1hsl 1 3 flpspc invspc invspcu invspcu 1 5 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 2 2 0 xxx xxx 1 0 +Flanged Mace mace 9ma mac 9ma 0 4 1 15 23 100 61 30 35 23 0 0 mac mac 9ma 7ma 1hs 1hs 5 1hsl 1 3 flpmac invmac 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Jagged Star mace 9mt mac 9mt 0 4 1 20 31 1 10 100 74 36 39 25 0 0 mac mst 9mt 7mt 1hs 1hs 5 1hsl 1 3 flpmst invmst invmstu invmstu 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Knout mace 9fl fla 9fl 0 4 1 13 35 2 -10 100 82 73 15 43 25 0 0 fla fla 9fl 7fl 1hs 1hs 5 1hsl 2 3 flpfla invfla 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Battle Hammer hamm 9wh whm 9wh 0 4 1 35 58 20 110 100 28 48 25 0 0 whm whm 9wh 7wh 1hs 1hs 5 1hsl 2 3 flpwhm invwhm 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 178 7 0 0 3 2 2 0 xxx xxx 1 0 +War Club hamm 9m9 mau 9m9 0 4 1 1 53 78 1 10 110 124 30 45 25 0 0 mau mau 9m9 7m7 stf stf 5 2hsl 2 4 flpmau invmau 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Martel de Fer hamm 9gm mau 9gm 0 3 1 1 61 99 2 20 110 169 30 53 25 0 0 mau gma 9gm 7gm stf stf 5 2hsl 2 3 flpgma invgma inv9gmu inv9gmu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Gladius swor 9ss ssd 9ss 0 3 1 8 22 100 25 12 30 18 0 0 ssd ssd 9ss 7ss 1hs 1hs 5 1hsl 1 3 flpssd invssd 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Cutlass swor 9sm scm 9sm 0 3 1 8 21 -30 100 25 52 11 43 25 0 0 scm scm 9sm 7sm 1hs 1hs 5 1hsl 1 3 flpscm invscm invscmu invscmu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Shamshir swor 9sb scm 9sb 0 3 1 10 24 -10 100 58 58 16 35 23 0 0 scm sbr 9sb 7sb 1hs 1hs 5 1hsl 1 3 flpsbr invsbr invsbru invsbru 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Tulwar swor 9fc flc 9fc 0 4 1 16 35 20 100 70 42 16 37 25 0 0 flc flc 9fc 7fc 1hs 1hs 5 1hsl 1 3 flpflc invflc 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Dimensional Blade swor 9cr crs 9cr 0 2 1 13 35 1 100 85 60 10 37 25 0 0 crs crs 9cr 7cr 1hs 1hs 5 1hsl 2 3 flpcrs invcrs inv9cru inv9cru 1 7 0 0 item_sword 12 item_sword 0 0 5 0 5 0 0 7 0 0 1 1 8 0 xxx xxx 1 0 +Battle Sword swor 9bs bsd 9bs 0 4 1 16 34 100 92 43 16 40 25 0 0 bsd bsd 9bs 7bs 1hs 1hs 5 1hsl 2 3 flpbsd invbsd 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Rune Sword swor 9ls lsd 9ls 0 4 1 10 42 1 -10 100 103 79 22 44 25 0 0 lsd lsd 9ls 7ls 1hs 1hs 5 1hsl 2 3 flplsd invlsd inv9lsu inv9lsu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Ancient Sword swor 9wd flc 9wd 0 4 1 18 43 1 100 127 88 22 49 25 0 0 flc wsd 9wd 7wd 1hs 1hs 5 1hsl 1 3 flpwsd invwsd 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Espadon swor 92h clm 92h 0 4 1 8 26 1 1 18 40 2 100 73 61 22 37 25 0 0 clm 2hs 92h 72h 1hs 2hs 5 2hss 1 4 flp2hs inv2hs inv2hsu inv2hsu 1 5 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Dacian Falx swor 9cm clm 9cm 0 4 1 13 30 1 1 26 61 2 10 100 91 20 25 42 25 0 0 clm clm 9cm 7cm 1hs 2hs 5 2hss 1 4 flpclm invclm 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Tusk Sword swor 9gs gsd 9gs 0 4 1 10 37 1 1 19 58 2 100 104 71 25 45 25 0 0 gsd gis 9gs 7gs 1hs 2hs 5 2hss 1 4 flpgis invgis invgisu invgisu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Gothic Sword swor 9b9 clm 9b9 0 4 1 14 40 1 1 39 60 1 10 100 113 20 20 48 25 0 0 clm bsw 9b9 7b7 1hs 2hs 5 2hss 1 4 flpbsw invbsw invbswu invbswu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Zweihander swor 9fb clm 9fb 0 4 1 19 35 1 1 29 54 2 -10 100 125 94 25 49 25 0 0 clm flb 9fb 7fb 1hs 2hs 5 2hss 2 4 flpflb invflb inv9fbu inv9fbu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Executioner Sword swor 9gd gsd 9gd 0 4 1 24 40 1 1 47 80 2 10 100 170 110 25 54 25 0 0 gsd gsd 9gd 7gd 1hs 2hs 5 2hss 2 4 flpgsd invgsd invgsdu invgsdu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Poignard knif 9dg dgr 9dg 0 2 1 6 18 -20 75 75 25 8 31 19 0 0 dgr dgr 9dg 7dg 1ht 1ht 5 1ht 1 2 flpdgr invdgr 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Rondel knif 9di dir 9di 0 2 1 10 26 75 75 25 58 10 36 24 0 0 dir dir 9di 7di 1ht 1ht 5 1ht 1 2 flpdir invdir 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Cinquedeas knif 9kr dir 9kr 0 2 1 15 31 -20 75 75 25 88 12 42 25 0 0 dir kri 9kr 7kr 1ht 1ht 5 1ht 1 3 flpkrs invkrs invkrsu invkrsu 1 5 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Stilleto knif 9bl dgr 9bl 0 3 1 19 36 -10 75 75 47 97 12 46 25 0 0 dgr bld 9bl 7bl 1ht 1ht 5 1ht 1 3 flpbld invbld inv9blu inv9blu 1 4 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Battle Dart tkni 9tk dgr 9tk 0 2 1 8 16 11 24 15 75 75 25 52 5 31 19 0 0 dgr tkf 9tk 7tk 1ht 1ht 5 1ht 1 2 1 40 160 75 flptkn invtkn 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 xxx xxx 1 0 +Francisca taxe 9ta hax 9ta 0 2 1 11 22 18 33 15 10 75 75 25 80 9 34 22 0 0 hax tax 9ta 7ta 1hs 1hs 5 1hsl 1 2 1 16 130 32 flptax invtax 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 5 0 3 5 2 0 xxx xxx 1 0 +War Dart tkni 9bk dgr 9bk 0 2 1 6 24 14 27 15 -20 75 75 25 97 12 39 25 0 0 dgr bkf 9bk 7bk 1ht 1ht 5 1ht 1 2 1 30 160 60 flpbkf invbkf 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 xxx xxx 1 0 +Hurlbat taxe 9b8 hax 9b8 0 2 1 13 27 24 34 15 -10 75 75 25 106 9 41 25 0 0 hax bal 9b8 7b8 1hs 1hs 5 1hsl 2 3 1 16 130 24 flpbal invbal 1 4 0 primarily thrown 0 item_sword 12 item_sword 0 0 5 0 0 0 35 7 5 0 3 2 2 0 xxx xxx 1 0 +War Javelin jave 9ja jav 9ja 0 4 1 6 19 14 32 15 2 -10 75 75 25 25 10 30 18 0 0 jav jav 9ja 7ja 1ht 1ht 5 1ht 1 3 1 30 60 60 flpjav invjav 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 1 7 2 0 3 5 8 0 xxx xxx 1 0 +Great Pilum jave 9pi pil 9pi 0 4 1 11 26 16 42 15 2 75 75 25 88 11 37 25 0 0 pil pil 9pi 7pi 1ht 1ht 5 1ht 1 3 1 25 50 50 flppil invpil 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 5 8 0 xxx xxx 1 0 +Simbilan jave 9s9 jav 9s9 0 4 1 8 32 27 50 15 2 10 75 75 80 80 11 40 25 0 0 jav ssp 9s9 7s7 1ht 1ht 5 1ht 1 3 1 20 40 40 flpssp invssp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 5 8 0 xxx xxx 1 0 +Spiculum jave 9gl glv 9gl 0 4 1 13 38 32 60 15 2 20 75 75 98 73 12 46 25 0 0 glv glv 9gl 7gl 1ht 1ht 5 1ht 1 4 1 8 20 20 flpglv invglv 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 1 2 0 xxx xxx 1 0 +Harpoon jave 9ts pil 9ts 0 4 1 13 35 18 54 15 2 -10 75 75 25 118 14 51 25 0 0 pil tsp 9ts 7ts 1ht 1ht 5 1ht 1 4 1 32 80 80 flptsp invtsp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 1 2 0 xxx xxx 1 0 +War Spear spea 9sr spr 9sr 0 4 1 1 10 37 3 -10 100 25 25 15 33 21 0 0 spr spr 9sr 7sr 2ht 2ht 5 2ht 2 4 flpspr invspr 1 5 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Fuscina spea 9tr tri 9tr 0 4 1 1 19 37 3 100 77 25 18 36 24 0 0 tri tri 9tr 7tr 2ht 2ht 5 2ht 2 4 flptri invtri invtriu invtriu 1 6 0 2 square reach 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +War Fork spea 9br brn 9br 0 4 1 1 16 40 4 -20 100 80 95 14 41 25 0 0 brn brn 9br 7br 2ht 2ht 5 2ht 2 4 flpbrn invbrn inv9bru inv9bru 1 7 0 2 square reach 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Yari spea 9st tri 9st 0 4 1 1 29 59 4 100 101 14 44 25 0 0 tri spt 9st 7st 2ht 2ht 5 2ht 2 4 flpspt invspt 1 7 0 2 square reach 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Lance spea 9p9 pik 9p9 0 4 1 1 27 114 4 20 100 110 88 13 47 25 0 0 pik pik 9p9 7p7 2ht 2ht 5 2ht 2 4 flppik invpik 1 7 0 3 square reach 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Lochaber Axe pole 9b7 hal 9b7 0 4 1 1 6 58 2 10 100 80 25 33 21 0 0 hal bar 9b7 7o7 stf stf 5 2hsl 2 4 flpbar invbar 1 5 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Bill pole 9vo hal 9vo 0 4 1 1 14 53 2 100 95 25 37 25 0 0 pax vou 9vo 7vo stf stf 5 2hsl 2 4 flpvou invvou 1 6 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Battle Scythe pole 9s8 scy 9s8 0 4 1 1 18 45 1 -10 100 82 82 33 40 25 0 0 scy scy 9s8 7s8 stf stf 5 2hsl 2 4 flpscy invscy inv9s8u inv9s8u 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Partizan pole 9pa hal 9pa 0 4 1 1 34 75 3 10 100 113 67 33 35 23 0 0 pax pax 9pa 7pa stf stf 5 2hsl 2 4 flppax invpax 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Bec-de-Corbin pole 9h9 pax 9h9 0 4 1 1 13 85 4 100 133 91 28 51 25 0 0 hal hal 9h9 7h7 stf stf 5 2hsl 2 4 flphal invhal 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Grim Scythe pole 9wc scy 9wc 0 3 1 1 30 70 4 -10 100 140 140 28 55 25 0 0 scy wsc 9wc 7wc stf stf 5 2hsl 2 4 flpwsc invwsc 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Jo Staff staf 8ss bst 8ss 0 2 1 1 6 21 1 -10 100 25 10 30 18 0 0 1 bst sst 8ss 6ss stf stf 5 club 1 3 flpsst invsst 1 4 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Quarterstaff staf 8ls sst 8ls 0 2 1 1 8 26 1 100 25 15 35 23 0 0 1 sst lst 8ls 6ls stf stf 5 staf 1 4 flplst invlst 1 5 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Cedar Staff staf 8cs cst 8cs 0 2 1 1 11 32 1 10 100 25 18 38 25 0 0 1 cst cst 8cs 6cs stf stf 5 staf 1 4 flpcst invcst invcstu invcstu 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Gothic Staff staf 8bs sst 8bs 0 2 1 1 14 34 1 100 25 20 42 25 0 0 1 sst bst 8bs 6bs stf stf 5 staf 1 4 flpbst invbst 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Rune Staff staf 8ws lst 8ws 0 2 1 1 24 58 1 20 100 25 25 47 25 0 0 1 lst wst 8ws 6ws stf stf 5 staf 2 4 flpwst invwst inv8wsu inv8wsu 1 7 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Edge Bow bow 8sb sbw 8sb 0 2 1 1 6 19 13 5 100 25 43 10 1 30 18 0 0 sbw sbw 8sb 6sb bow bow 6 bow 2 3 flpsbw invsbw 1 5 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Razor Bow bow 8hb sbw 8hb 0 2 1 1 8 22 13 -10 100 25 62 16 1 33 21 0 0 sbw hbw 8hb 6hb bow bow 6 bow 2 3 flphbw invhbw 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Cedar Bow bow 8lb lbw 8lb 0 2 1 1 10 29 13 100 53 49 14 1 35 23 0 0 lbw lbw 8lb 6lb bow bow 6 bow 2 4 flplbw invlbw inv8lbu inv8lbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Double Bow bow 8cb lbw 8cb 0 2 1 1 11 26 13 -10 100 58 73 18 1 39 25 0 0 lbw cbw 8cb 6cb bow bow 6 bow 2 3 flpcbw invcbw invcbwu invcbwu 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Short Siege Bow bow 8s8 sbb 8s8 0 2 1 1 13 30 13 100 65 80 20 1 43 25 0 0 sbb sbb 8s8 6s7 bow bow 6 bow 2 3 flpsbb invsbb inv8s8u inv8s8u 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Long Siege Bow bow 8l8 lbb 8l8 0 2 1 1 10 42 13 10 100 80 95 22 1 46 25 0 0 lbb lbb 8l8 6l7 bow bow 6 bow 2 4 flplbb invlbb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Rune Bow bow 8sw sbb 8sw 0 2 1 1 14 35 13 100 73 103 24 1 49 25 0 0 sbb swb 8sw 6sw bow bow 6 bow 2 3 flpswb invswb invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Gothic Bow bow 8lw lbb 8lw 0 2 1 1 10 50 13 10 100 95 118 28 1 52 25 0 0 lbb lwb 8lw 6lw bow bow 6 bow 2 4 flplwb invlwb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Arbalest xbow 8lx lxb 8lx 0 2 1 1 14 27 -10 100 52 61 15 1 34 22 0 0 lxb lxb 8lx 6lx xbw xbw 5 xbow 2 3 flplxb invlxb inv8lxu inv8lxu 1 5 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Siege Crossbow xbow 8mx lxb 8mx 0 2 1 1 20 42 100 80 70 20 1 40 25 0 0 lxb mxb 8mx 6mx xbw xbw 5 xbow 2 3 flpmxb invmxb inv8mxu inv8mxu 1 6 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Balista xbow 8hx hxb 8hx 0 2 1 1 33 55 10 100 110 80 25 1 47 25 0 0 hxb hxb 8hx 6hx xbw xbw 5 xbow 2 4 flphxb invhxb invhxbu invhxbu 1 7 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Chu-Ko-Nu xbow 8rx hxb 8rx 0 2 1 1 14 32 -60 100 80 95 20 1 54 25 0 0 hxb rxb 8rx 6rx xbw xbw 5 xbow 2 3 flprxb invrxb invrxbu invrxbu 1 7 0 fires 5 shots before reload 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +KhalimFlail mace qf1 fla qf1 0 1 15 -10 100 41 35 15 0 0 0 0 fla qf1 1hs 1hs 5 1hsl 2 3 flpfla invqf1 1 5 0 0 item_sword 12 item_sword 1 0 5 0 0 0 17 1 0 7 0 0 1 2 2 1 xxx xxx 0 0 +SuperKhalimFlail mace qf2 fla qf2 0 1 15 -10 100 15 0 0 0 0 fla qf2 1hs 1hs 5 1hsl 2 3 flpfla invqf2 1 5 0 0 item_sword 12 item_sword 1 0 5 0 0 0 17 1 0 7 0 0 1 2 2 1 xxx xxx 0 0 +Expansion +Katar h2h ktr ktr ktr 100 2 1 4 7 14 1 -10 75 75 20 20 24 1 0 0 0 ktr ktr 9ar 7ar ht1 ht1 5 1hsl 1 3 flpktr invktr 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 wrb btl 1 0 +Wrist Blade h2h wrb ktr wrb 100 2 1 5 9 14 1 75 75 33 33 26 9 0 0 0 ktr wrb 9wb 7wb ht1 ht1 5 1hsl 1 3 flpktr invktr 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 axf skr 1 0 +Hatchet Hands h2h axf axf axf 100 2 1 2 15 14 1 10 75 75 37 37 28 12 0 0 0 axf axf 9xf 7xf ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 ces clw 1 0 +Cestus h2h ces axf ces 100 2 1 7 15 14 1 75 75 42 42 36 15 0 0 0 axf ces 9cs 7cs ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 clw btl 1 0 +Claws h2h clw clw clw 100 2 1 8 15 14 1 -10 75 75 46 46 32 18 0 0 0 clw clw 9lw 7lw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 btl xxx 1 0 +Blade Talons h2h btl clw btl 100 2 1 10 14 14 1 -20 75 75 50 50 35 21 0 0 0 clw btl 9tw 7tw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Scissors Katar h2h skr skr skr 100 2 1 9 17 14 1 -10 75 75 55 55 34 24 0 0 0 skr skr 9qr 7qr ht1 ht1 5 1hsl 1 3 flpskr invskr invskru invskru 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Quhab h2h 9ar ktr 9ar 100 2 1 11 24 14 1 75 75 57 57 24 28 21 0 0 ktr ktr 9ar 7ar ht1 ht1 5 1hsl 1 3 flpktr invktr 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Wrist Spike h2h 9wb ktr 9wb 100 2 1 13 27 14 1 -10 75 75 66 66 28 32 24 0 0 ktr wrb 9wb 7wb ht1 ht1 5 1hsl 1 3 flpktr invktr 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Fascia h2h 9xf axf 9xf 100 2 1 8 37 14 1 10 75 75 69 69 32 36 27 0 0 axf axf 9xf 7xf ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Hand Scythe h2h2 9cs axf 9cs 100 2 1 16 37 14 1 -10 75 75 73 73 36 41 30 0 0 axf ces 9cs 7cs ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Greater Claws h2h2 9lw clw 9lw 100 2 1 18 37 14 1 -20 75 75 76 76 26 45 33 0 0 clw clw 9lw 7lw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Greater Talons h2h2 9tw clw 9tw 100 2 1 21 35 14 1 -30 75 75 79 79 35 50 37 0 0 clw btl 9tw 7tw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Scissors Quhab h2h2 9qr skr 9qr 100 2 1 19 40 14 1 75 75 82 82 34 54 40 0 0 skr skr 9qr 7qr ht1 ht1 5 1hsl 1 3 flpskr invskr invskru invskru 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Suwayyah h2h2 7ar ktr 7ar 100 2 1 39 52 14 1 75 75 99 99 24 59 44 0 0 ktr ktr 9ar 7ar ht1 ht1 5 1hsl 1 3 flpktr invktr 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Wrist Sword h2h2 7wb ktr 7wb 100 2 1 34 45 14 1 -10 75 75 105 105 28 62 46 0 0 ktr wrb 9wb 7wb ht1 ht1 5 1hsl 1 3 flpktr invktr 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +War Fist h2h2 7xf axf 7xf 100 2 1 44 53 14 1 10 75 75 108 108 32 68 51 0 0 axf axf 9xf 7xf ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Battle Cestus h2h2 7cs axf 7cs 100 2 1 36 42 14 1 -10 75 75 110 110 36 73 54 0 0 axf ces 9cs 7cs ht1 ht1 5 1hsl 1 3 flpaxf invaxf invaxfu invaxfu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Feral Claws h2h2 7lw clw 7lw 100 2 1 22 53 14 1 -20 75 75 113 113 26 78 58 0 0 clw clw 9lw 7lw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Runic Talons h2h2 7tw clw 7tw 100 2 1 24 44 14 1 -30 75 75 115 115 35 81 60 0 0 clw btl 9tw 7tw ht1 ht1 5 1hsl 1 3 flpclw invclw 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Scissors Suwayyah h2h2 7qr skr 7qr 100 2 1 40 51 14 1 75 75 118 118 34 85 64 0 0 skr skr 9qr 7qr ht1 ht1 5 1hsl 1 3 flpskr invskr invskru invskru 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Tomahawk axe 7ha hax 7ha 100 3 1 33 58 14 100 125 67 14 54 40 0 0 hax hax 9ha 7ha 1hs 1hs 5 1hsl 1 3 flphax invhax invhaxu invhaxu 1 4 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 0 0 3 5 2 0 xxx xxx 1 0 +Small Crescent axe 7ax axe 7ax 100 4 1 38 60 14 1 10 100 115 83 12 61 45 0 0 axe axe 9ax 7ax 1hs 1hs 5 1hsl 2 3 flpaxe invaxe invaxeu invaxeu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +Ettin Axe axe 72a axe 72a 100 4 1 33 66 14 1 10 100 145 45 12 70 52 0 0 axe 2ax 92a 72a 1hs 1hs 5 1hsl 2 3 flp2ax inv2ax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +War Spike axe 7mp axe 7mp 100 4 1 30 48 14 1 -10 100 133 54 13 79 59 0 0 axe mpi 9mp 7mp 1hs 1hs 5 1hsl 2 3 flpmpi invmpi invmpiu invmpiu 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Berserker Axe axe 7wa hax 7wa 100 4 1 24 71 14 2 100 138 59 13 85 64 0 0 hax wax 9wa 7wa 1hs 1hs 5 1hsl 2 3 flpwax invwax 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Feral Axe axe 7la lax 7la 100 4 1 1 25 123 17 2 -15 100 196 15 57 42 0 0 lax lax 9la 7la stf stf 5 2hsl 2 3 flplax invlax 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Silver Edged Axe axe 7ba lax 7ba 100 4 1 1 62 110 17 2 100 166 65 18 65 48 0 0 lax bax 9ba 7ba stf stf 5 2hsl 2 3 flpbrx invbrx 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Decapitator axe 7bt btx 7bt 100 4 1 1 49 137 17 2 10 100 189 33 20 73 54 0 0 btx btx 9bt 7bt stf stf 5 2hsl 2 3 flpbtx invbtx invbtxu invbtxu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 5 2 0 xxx xxx 1 0 +Champion Axe axe 7ga btx 7ga 100 4 1 1 59 94 17 2 -10 100 167 59 25 82 61 0 0 btx gax 9ga 7ga stf stf 5 2hsl 2 4 flpgax invgax invgaxu invgaxu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Glorious Axe axe 7gi gix 7gi 100 4 1 1 60 124 17 3 10 100 164 55 25 85 66 0 0 gix gix 9gi 7gi stf stf 5 2hsl 2 3 flpgix invgix 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Polished Wand wand 7wn wnd 7wn 100 1 1 18 33 14 100 25 11 55 41 0 0 wnd wnd 9wn 7wn 1hs 1hs 5 1hss 1 2 flpwnd invwnd 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Ghost Wand wand 7yw ywn 7yw 100 1 1 20 40 14 10 100 25 7 65 48 0 0 ywn ywn 9yw 7yw 1hs 1hs 5 1hss 1 2 flpywn invywn 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Lich Wand wand 7bw bwn 7bw 100 1 1 10 31 14 -20 100 25 9 75 56 0 0 bwn bwn 9bw 7bw 1hs 1hs 5 1hss 1 2 flpbwn invbwn invbwnu invbwnu 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Unearthed Wand wand 7gw bwn 7gw 100 1 1 22 28 14 100 25 9 86 64 0 0 bwn gwn 9gw 7gw 1hs 1hs 5 1hss 1 2 flpgwn invgwn 1 4 0 magically charged 0 item_wand 12 item_wand 0 0 5 0 0 0 0 7 0 0 5 1 2 0 xxx xxx 1 0 +Truncheon club 7cl clb 7cl 100 1 1 35 43 14 -10 100 88 43 28 52 39 0 0 clb clb 9cl 7cl 1hs 1hs 5 club 1 3 flpclb invclb invclbu invclbu 1 4 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 5 2 0 xxx xxx 1 0 +Mighty Scepter scep 7sc mac 7sc 100 2 1 40 52 14 100 125 65 25 62 46 0 0 mac scp 9sc 7sc 1hs 1hs 5 club 1 3 flpscp invscp 1 4 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Seraph Rod scep 7qs mac 7qs 100 4 1 45 54 14 1 10 100 108 69 30 76 57 0 0 mac gsc 9qs 7qs 1hs 1hs 5 club 1 3 flpgsc invgsc 1 5 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Caduceus scep 7ws whm 7ws 100 4 1 37 43 14 1 -10 100 97 70 35 85 66 0 0 whm wsp 9ws 7ws 1hs 1hs 5 club 2 3 flpwsp invwsp 1 7 0 magically charged 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Tyrant Club club 7sp clb 7sp 100 3 1 32 58 14 1 100 133 33 57 42 0 0 clb spc 9sp 7sp 1hs 1hs 5 1hsl 1 3 flpspc invspc invspcu invspcu 1 5 0 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 1 2 2 0 xxx xxx 1 0 +Reinforced Mace mace 7ma mac 7ma 100 4 1 41 49 14 100 145 46 30 63 47 0 0 mac mac 9ma 7ma 1hs 1hs 5 1hsl 1 3 flpmac invmac inv7mas 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Devil Star mace 7mt mac 7mt 100 4 1 43 53 14 1 10 100 153 44 36 70 52 0 0 mac mst 9mt 7mt 1hs 1hs 5 1hsl 1 3 flpmst invmst invmstu invmstu 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Scourge mace 7fl fla 7fl 100 4 1 3 80 14 2 -10 100 125 77 33 76 57 0 0 fla fla 9fl 7fl 1hs 1hs 5 1hsl 2 3 flpfla invfla 1 7 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Legendary Mallet hamm 7wh whm 7wh 100 4 1 50 61 14 1 20 110 189 33 82 61 0 0 whm whm 9wh 7wh 1hs 1hs 5 1hsl 2 3 flpwhm invwhm 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 178 7 0 0 3 2 2 0 xxx xxx 1 0 +Ogre Maul hamm 7m7 mau 7m7 100 4 1 1 77 106 17 1 10 110 225 30 69 51 0 0 mau mau 9m9 7m7 stf stf 5 2hsl 2 4 flpmau invmau 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Thunder Maul hamm 7gm mau 7gm 100 3 1 1 33 180 17 2 20 110 253 30 85 65 0 0 mau gma 9gm 7gm stf stf 5 2hsl 2 3 flpgma invgma 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Falcata swor 7ss ssd 7ss 100 3 1 31 59 14 100 150 88 12 56 42 0 0 ssd ssd 9ss 7ss 1hs 1hs 5 1hsl 1 3 flpssd invssd 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Ataghan swor 7sm scm 7sm 100 3 1 26 46 14 -20 100 138 95 11 61 45 0 0 scm scm 9sm 7sm 1hs 1hs 5 1hsl 1 3 flpscm invscm invscmu invscmu 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Elegant Blade swor 7sb scm 7sb 100 3 1 33 45 14 -10 100 109 122 16 63 47 0 0 scm sbr 9sb 7sb 1hs 1hs 5 1hsl 1 3 flpsbr invsbr invsbru invsbru 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Hydra Edge swor 7fc flc 7fc 100 4 1 28 68 14 10 100 142 105 16 69 51 0 0 flc flc 9fc 7fc 1hs 1hs 5 1hsl 1 3 flpflc invflc 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Phase Blade swor 7cr crs 7cr 100 2 1 31 35 14 1 -30 100 25 136 0 1 73 54 0 0 crs crs 9cr 7cr 1hs 1hs 5 1hsl 2 3 flpcrs invcrs invcrsu invcrsu 1 7 0 0 item_sword 12 item_sword 0 0 5 0 5 0 0 7 0 0 1 1 8 0 xxx xxx 1 0 +Conquest Sword swor 7bs bsd 7bs 100 4 1 37 53 14 100 142 112 16 78 58 0 0 bsd bsd 9bs 7bs 1hs 1hs 5 1hsl 2 3 flpbsd invbsd 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Cryptic Sword swor 7ls lsd 7ls 100 4 1 5 77 14 1 -10 100 99 109 22 82 61 0 0 lsd lsd 9ls 7ls 1hs 1hs 5 1hsl 2 3 flplsd invlsd invlsdu invlsdu 1 6 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Mythical Sword swor 7wd flc 7wd 100 4 1 40 50 14 1 100 147 124 22 85 66 0 0 flc wsd 9wd 7wd 1hs 1hs 5 1hsl 1 3 flpwsd invwsd 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Legend Sword swor 72h clm 72h 100 4 1 22 56 1 1 50 94 17 2 -15 100 175 100 22 59 44 0 0 clm 2hs 92h 72h 1hs 2hs 5 2hss 1 4 flp2hs inv2hs inv2hsu inv2hsu 1 5 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Highland Blade swor 7cm clm 7cm 100 4 1 22 62 1 1 67 96 17 2 -5 100 171 104 25 66 49 0 0 clm clm 9cm 7cm 1hs 2hs 5 2hss 1 4 flpclm invclm 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Balrog Blade swor 7gs gsd 7gs 100 4 1 15 75 1 1 55 118 17 2 100 185 87 25 71 53 0 0 gsd gis 9gs 7gs 1hs 2hs 5 2hss 1 4 flpgis invgis invgisu invgisu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Champion Sword swor 7b7 clm 7b7 100 4 1 24 54 1 1 71 83 17 2 -10 100 163 103 20 77 57 0 0 clm bsw 9b9 7b7 1hs 2hs 5 2hss 1 4 flpbsw invbsw invbswu invbswu 1 6 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Colossal Sword swor 7fb clm 7fb 100 4 1 26 70 1 1 61 121 17 2 10 100 182 95 25 80 60 0 0 clm flb 9fb 7fb 1hs 2hs 5 2hss 2 4 flpflb invflb 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Colossus Blade swor 7gd gsd 7gd 100 4 1 25 65 1 1 58 115 17 2 5 100 189 110 25 85 63 0 0 gsd gsd 9gd 7gd 1hs 2hs 5 2hss 2 4 flpgsd invgsd invgsdu invgsdu 1 7 0 0 item_largemetalweapon 12 item_largemetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Bone Knife knif 7dg dgr 7dg 100 2 1 23 49 14 -20 75 75 38 75 13 58 43 0 0 dgr dgr 9dg 7dg 1ht 1ht 5 1ht 1 2 flpdgr invdgr 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Mithral Point knif 7di dir 7di 100 2 1 37 53 14 75 75 55 98 28 70 52 0 0 dir dir 9di 7di 1ht 1ht 5 1ht 1 2 flpdir invdir 1 3 0 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Fanged Knife knif 7kr dir 7kr 100 2 1 15 57 14 -20 75 75 42 86 18 83 62 0 0 dir kri 9kr 7kr 1ht 1ht 5 1ht 1 3 flpkrs invkrs invkrsu invkrsu 1 5 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Legend Spike knif 7bl dgr 7bl 100 3 1 31 47 14 -10 75 75 65 67 24 85 66 0 0 dgr bld 9bl 7bl 1ht 1ht 5 1ht 1 3 flpbld invbld 1 4 0 0 item_sword 12 item_sword 0 0 5 0 0 0 0 7 0 0 3 1 2 0 xxx xxx 1 0 +Flying Knife tkni 7tk dgr 7tk 100 2 1 23 54 23 54 15 75 75 48 141 12 64 48 0 0 dgr tkf 9tk 7tk 1ht 1ht 5 1ht 1 2 1 40 200 75 flptkn invtkn 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 xxx xxx 1 0 +Flying Axe taxe 7ta hax 7ta 100 2 1 17 65 15 66 15 10 75 75 88 108 16 56 42 0 0 hax tax 9ta 7ta 1hs 1hs 5 1hsl 1 2 1 16 180 32 flptax invtax 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 35 7 5 0 3 5 2 0 xxx xxx 1 0 +Winged Knife tkni 7bk dgr 7bk 100 2 1 27 35 23 39 15 -20 75 75 45 142 10 77 57 0 0 dgr bkf 9bk 7bk 1ht 1ht 5 1ht 1 2 1 30 200 60 flpbkf invbkf invtk3 1 4 0 primarily thrown 0 item_smallmetalweapon 12 item_smallmetalweapon 0 0 5 0 0 0 36 7 4 0 3 1 2 0 xxx xxx 1 0 +Winged Axe taxe 7b8 hax 7b8 100 2 1 11 56 7 60 15 -10 75 75 96 122 16 80 60 0 0 hax bal 9b8 7b8 1hs 1hs 5 1hsl 2 3 1 16 180 24 flpbal invbal 1 4 0 primarily thrown 0 item_sword 12 item_sword 0 0 5 0 0 0 35 7 5 0 3 2 2 0 xxx xxx 1 0 +Hyperion Javelin jave 7ja jav 7ja 100 4 1 21 57 28 55 14 2 -10 75 75 98 123 10 54 40 0 0 jav jav 9ja 7ja 1ht 1ht 5 1ht 1 3 1 30 100 60 flpjav invjav 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 1 7 2 0 3 5 8 0 xxx xxx 1 0 +Stygian Pilum jave 7pi pil 7pi 100 4 1 14 64 21 75 14 2 75 75 118 112 12 62 46 0 0 pil pil 9pi 7pi 1ht 1ht 5 1ht 1 3 1 25 90 50 flppil invpil 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 5 8 0 xxx xxx 1 0 +Balrog Spear jave 7s7 jav 7s7 100 4 1 33 63 40 62 14 2 10 75 75 127 95 14 71 53 0 0 jav ssp 9s9 7s7 1ht 1ht 5 1ht 1 3 1 20 80 40 flpssp invssp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 5 8 0 xxx xxx 1 0 +Ghost Glaive jave 7gl glv 7gl 100 4 1 19 60 30 85 14 2 20 75 75 89 137 16 79 59 0 0 glv glv 9gl 7gl 1ht 1ht 5 1ht 1 4 1 8 75 20 flpglv invglv 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 1 2 0 xxx xxx 1 0 +Winged Harpoon jave 7ts pil 7ts 100 4 1 27 35 11 77 14 2 -10 75 75 76 145 18 85 65 0 0 pil tsp 9ts 7ts 1ht 1ht 5 1ht 1 4 1 32 80 80 flptsp invtsp 1 4 0 primarily thrown 0 item_javelins 12 item_javelins 0 0 5 0 0 0 37 7 2 0 3 1 2 0 xxx xxx 1 0 +Hyperion Spear spea 7sr spr 7sr 100 4 1 1 35 119 17 3 -10 100 155 120 15 58 43 0 0 spr spr 9sr 7sr 2ht 2ht 5 2ht 2 4 flpspr invspr 1 5 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Stygian Pike spea 7tr tri 7tr 100 4 1 1 29 144 17 3 100 168 97 18 66 49 0 0 tri tri 9tr 7tr 2ht 2ht 5 2ht 2 4 flptri invtri invtriu invtriu 1 6 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Mancatcher spea 7br brn 7br 100 4 1 1 42 92 17 4 -20 100 132 134 14 74 55 0 0 brn brn 9br 7br 2ht 2ht 5 2ht 2 4 flpbrn invbrn 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Ghost Spear spea 7st tri 7st 100 4 1 1 18 155 17 4 100 122 163 14 83 62 0 0 tri spt 9st 7st 2ht 2ht 5 2ht 2 4 flpspt invspt 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +War Pike spea 7p7 pik 7p7 100 4 1 1 33 178 17 4 20 100 165 106 13 85 66 0 0 pik pik 9p9 7p7 2ht 2ht 5 2ht 2 4 flppik invpik 1 7 0 3 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Ogre Axe pole 7o7 hal 7o7 100 4 1 1 28 145 17 2 100 195 75 25 60 45 0 0 hal bar 9b7 7o7 stf stf 5 2hsl 2 4 flpbar invbar 1 5 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Colossus Voulge pole 7vo hal 7vo 100 4 1 1 17 165 17 2 10 100 210 55 25 64 48 0 0 pax vou 9vo 7vo stf stf 5 2hsl 2 4 flpvou invvou 1 6 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Thresher pole 7s8 scy 7s8 100 4 1 1 12 141 17 1 -10 100 152 118 33 71 53 0 0 scy scy 9s8 7s8 stf stf 5 2hsl 2 4 flpscy invscy invscyu invscyu 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Cryptic Axe pole 7pa hal 7pa 100 4 1 1 33 150 17 3 10 100 165 103 33 79 59 0 0 pax pax 9pa 7pa stf stf 5 2hsl 2 4 flppax invpax 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Great Poleaxe pole 7h7 pax 7h7 100 4 1 1 46 127 17 4 100 179 99 28 84 63 0 0 hal hal 9h9 7h7 stf stf 5 2hsl 2 4 flphal invhal 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Giant Thresher pole 7wc scy 7wc 100 3 1 1 40 114 17 4 -10 100 188 140 28 85 66 0 0 scy wsc 9wc 7wc stf stf 5 2hsl 2 4 flpwsc invwsc 1 7 0 2 square reach 0 item_woodweaponlarge 12 item_woodweaponlarge 0 0 5 0 0 0 0 7 0 0 3 2 2 0 xxx xxx 1 0 +Walking Stick staf 6ss bst 6ss 100 2 1 1 69 85 17 1 -10 100 25 10 58 43 0 0 1 bst sst 8ss 6ss stf stf 5 club 1 3 flpsst invsst 1 4 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Stalagmite staf 6ls sst 6ls 100 2 1 1 75 107 17 1 10 100 63 35 15 66 49 0 0 1 sst lst 8ls 6ls stf stf 5 staf 1 4 flplst invlst 1 5 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Elder Staff staf 6cs cst 6cs 100 2 1 1 80 93 17 1 100 44 37 18 74 55 0 0 1 cst cst 8cs 6cs stf stf 5 staf 1 4 flpcst invcst invcstu invcstu 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Shillelah staf 6bs sst 6bs 100 2 1 1 65 108 17 1 100 52 27 20 83 62 0 0 1 sst bst 8bs 6bs stf stf 5 staf 1 4 flpbst invbst 1 6 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Archon Staff staf 6ws lst 6ws 100 2 1 1 83 99 17 1 10 100 34 13 85 66 0 0 1 lst wst 8ws 6ws stf stf 5 staf 2 4 flpwst invwst 1 7 0 magically charged 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Spider Bow bow 6sb sbw 6sb 100 2 1 1 23 50 13 5 100 64 143 10 1 55 41 0 0 sbw sbw 8sb 6sb bow bow 6 bow 2 3 flpsbw invsbw 1 5 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Blade Bow bow 6hb sbw 6hb 100 2 1 1 21 41 13 -10 100 76 119 16 1 60 45 0 0 sbw hbw 8hb 6hb bow bow 6 bow 2 3 flphbw invhbw 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Shadow Bow bow 6lb lbw 6lb 100 2 1 1 15 59 13 100 52 188 14 1 63 47 0 0 lbw lbw 8lb 6lb bow bow 6 bow 2 4 flplbw invlbw 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Great Bow bow 6cb lbw 6cb 100 2 1 1 12 52 13 -10 100 121 107 18 1 68 51 0 0 lbw cbw 8cb 6cb bow bow 6 bow 2 3 flpcbw invcbw invcbwu invcbwu 1 6 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Diamond Bow bow 6s7 sbb 6s7 100 2 1 1 33 40 13 100 89 132 20 1 72 54 0 0 sbb sbb 8s8 6s7 bow bow 6 bow 2 3 flpsbb invsbb invsbbu invsbbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Crusader Bow bow 6l7 lbb 6l7 100 2 1 1 15 63 13 10 100 97 121 22 1 77 57 0 0 lbb lbb 8l8 6l7 bow bow 6 bow 2 4 flplbb invlbb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Ward Bow bow 6sw sbb 6sw 100 2 1 1 20 53 13 100 72 146 24 1 80 60 0 0 sbb swb 8sw 6sw bow bow 6 bow 2 3 flpswb invswb invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Hydra Bow bow 6lw lbb 6lw 100 2 1 1 10 68 13 10 100 134 167 28 1 85 63 0 0 lbb lwb 8lw 6lw bow bow 6 bow 2 4 flplwb invlwb 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Pellet Bow xbow 6lx lxb 6lx 100 2 1 1 28 73 19 -10 100 83 155 15 1 57 42 0 0 lxb lxb 8lx 6lx xbw xbw 5 xbow 2 3 flplxb invlxb invlxbu invlxbu 1 5 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Gorgon Crossbow xbow 6mx lxb 6mx 100 2 1 1 25 87 19 100 117 105 20 1 67 50 0 0 lxb mxb 8mx 6mx xbw xbw 5 xbow 2 3 flpmxb invmxb invmxbu invmxbu 1 6 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Colossus Crossbow xbow 6hx hxb 6hx 100 2 1 1 32 91 19 10 100 163 77 25 1 75 56 0 0 hxb hxb 8hx 6hx xbw xbw 5 xbow 2 4 flphxb invhxb invhxbu invhxbu 1 7 0 reload lag between shots 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Demon Crossbow xbow 6rx hxb 6rx 100 2 1 1 26 40 19 -60 100 141 98 20 1 84 63 0 0 hxb rxb 8rx 6rx xbw xbw 5 xbow 2 3 flprxb invrxb invrxbu invrxbu 1 7 0 fires 5 shots before reload 0 item_crossbow 12 item_crossbow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Eagle Orb orb ob1 ob1 ob1 100 2 1 2 5 14 -10 100 10 1 0 0 0 1 303 bst ob1 ob6 obb 1hs 1hs 5 1hss 1 2 flpob1 invob1 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Sacred Globe orb ob2 ob1 ob2 100 3 1 3 8 14 -10 100 15 8 0 0 0 1 303 bst ob2 ob7 obc 1hs 1hs 5 1hss 1 2 flpob1 invob2 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Smoked Sphere orb ob3 ob3 ob3 100 3 1 4 10 14 100 18 12 8 0 0 1 303 bst ob3 ob8 obd 1hs 1hs 5 1hss 1 2 flpob3 invob3 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Clasped Orb orb ob4 ob4 ob4 100 4 1 5 12 14 100 20 17 13 0 0 1 303 bst ob4 ob9 obe 1hs 1hs 5 1hss 1 2 flpob4 invob4 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Dragon Stone orb ob5 ob4 ob5 100 5 1 8 18 14 10 100 25 24 18 0 0 1 303 bst ob5 oba obf 1hs 1hs 5 1hss 1 3 flpob5 invob5 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Stag Bow abow am1 am1 am1 100 3 1 1 7 12 13 100 30 45 24 1 18 14 0 0 300 sbw am1 am6 amb bow bow 6 bow 2 4 flpam1 invam1 invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Reflex Bow abow am2 am2 am2 100 3 1 1 9 19 13 10 100 35 60 28 1 27 20 0 0 300 sbw am2 am7 amc bow bow 6 bow 2 4 flpam2 invam2 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Maiden Spear aspe am3 am3 am3 100 3 1 1 18 24 15 4 80 50 54 40 14 18 14 0 0 302 spr am3 am8 amd 2ht 2ht 5 2ht 2 4 flpam3 invam3 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Maiden Pike aspe am4 pik am4 100 3 1 1 23 55 15 4 10 80 50 63 52 13 27 20 0 0 302 spr am4 am9 ame 2ht 2ht 5 2ht 2 4 flppik invam4 1 7 0 3 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Maiden Javelin ajav am5 pil am5 100 3 1 8 14 6 22 15 2 -10 80 50 33 47 15 23 17 0 0 302 jav am5 ama amf 1ht 1ht 5 1ht 1 3 1 32 80 80 flpam5 invam5 1 4 0 primarily thrown 0 item_staff 12 item_staff 0 0 5 0 0 0 371 7 2 0 3 1 2 0 xxx xxx 1 0 +Glowing Orb orb ob6 ob1 ob6 100 2 1 8 21 14 -10 100 10 32 24 0 0 1 303 bst ob1 ob6 obb 1hs 1hs 5 1hss 1 2 flpob1 invob1 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Crystalline Globe orb ob7 ob1 ob7 100 3 1 10 26 14 -10 100 15 37 27 0 0 1 303 bst ob2 ob7 obc 1hs 1hs 5 1hss 1 2 flpob1 invob2 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Cloudy Sphere orb ob8 ob3 ob8 100 3 1 11 29 14 100 18 41 30 0 0 1 303 bst ob3 ob8 obd 1hs 1hs 5 1hss 1 2 flpob3 invob3 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Sparkling Ball orb ob9 ob4 ob9 100 4 1 13 32 14 100 20 46 34 0 0 1 303 bst ob4 ob9 obe 1hs 1hs 5 1hss 1 2 flpob4 invob4 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Swirling Crystal orb oba ob4 oba 100 5 1 18 42 14 10 100 25 50 37 0 0 1 303 bst ob5 oba obf 1hs 1hs 5 1hss 1 3 flpob5 invob5 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Ashwood Bow abow am6 am1 am6 100 3 1 1 16 29 13 100 56 77 24 1 39 29 0 0 300 sbw am1 am6 amb bow bow 6 bow 2 4 flpam1 invam1 invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Ceremonial Bow abow am7 am2 am7 100 3 1 1 19 41 13 10 100 73 110 28 1 47 35 0 0 300 sbw am2 am7 amc bow bow 6 bow 2 4 flpam2 invam2 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Ceremonial Spear aspe am8 am3 am8 100 3 1 1 34 51 15 4 80 50 101 80 14 43 32 0 0 302 spr am3 am8 amd 2ht 2ht 5 2ht 2 4 flpam3 invam3 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Ceremonial Pike aspe am9 pik am9 100 3 1 1 42 101 15 4 20 80 50 115 98 13 51 38 0 0 302 spr am4 am9 ame 2ht 2ht 5 2ht 2 4 flppik invam4 1 7 0 3 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Ceremonial Javelin ajav ama pil ama 100 3 1 18 35 18 54 15 2 -10 80 50 25 109 15 35 26 0 0 302 jav am5 ama amf 1ht 1ht 5 1ht 1 3 1 32 80 80 flpam5 invam5 1 4 0 primarily thrown 0 item_staff 12 item_staff 0 0 5 0 0 0 371 7 2 0 3 1 2 0 xxx xxx 1 0 +Heavenly Stone orb obb ob1 obb 100 2 1 21 46 14 -10 100 10 59 44 0 0 1 303 bst ob1 ob6 obb 1hs 1hs 5 1hss 1 2 flpob1 invob1 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Eldritch Orb orb obc ob1 obc 100 3 1 18 50 14 -10 100 15 67 50 0 0 1 303 bst ob2 ob7 obc 1hs 1hs 5 1hss 1 2 flpob1 invob2 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Demon Heart orb obd ob3 obd 100 3 1 23 55 14 100 18 75 56 0 0 1 303 bst ob3 ob8 obd 1hs 1hs 5 1hss 1 2 flpob3 invob3 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Vortex Orb orb obe ob4 obe 100 4 1 12 66 14 100 20 84 63 0 0 1 303 bst ob4 ob9 obe 1hs 1hs 5 1hss 1 2 flpob4 invob4 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Dimensional Shard orb obf ob4 obf 100 5 1 30 53 14 10 100 25 85 66 0 0 1 303 bst ob5 oba obf 1hs 1hs 5 1hss 1 3 flpob5 invob5 1 6 0 0 item_orb 12 item_orb 0 0 5 0 1 0 0 7 0 0 5 5 8 0 xxx xxx 1 0 +Matriarchal Bow abow amb am1 amb 100 3 1 1 20 47 13 -10 100 87 187 24 1 53 39 0 0 300 sbw am1 am6 amb bow bow 6 bow 2 4 flpam1 invam1 invswbu invswbu 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Grand Matron Bow abow amc am2 amc 100 3 1 1 14 72 13 10 100 108 152 28 1 78 58 0 0 300 sbw am2 am7 amc bow bow 6 bow 2 4 flpam2 invam2 1 7 0 0 item_bow 12 item_bow 0 0 5 1 0 0 0 7 0 0 1 5 8 0 xxx xxx 1 0 +Matriarchal Spear aspe amd am3 amd 100 3 1 1 65 95 15 4 80 50 114 142 14 61 45 0 0 302 spr am3 am8 amd 2ht 2ht 5 2ht 2 4 flpam3 invam3 1 7 0 2 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +Matriarchal Pike aspe ame pik ame 100 3 1 1 37 153 15 4 20 80 50 132 149 13 81 60 0 0 302 spr am4 am9 ame 2ht 2ht 5 2ht 2 4 flppik invam4 1 7 0 3 square reach 0 item_staff 12 item_staff 0 0 5 0 0 0 0 7 0 0 3 5 8 0 xxx xxx 1 0 +MatriarchalJavelin ajav amf pil amf 100 3 1 30 54 35 66 15 2 -10 80 50 107 151 20 65 48 0 0 302 jav am5 ama amf 1ht 1ht 5 1ht 1 3 1 32 80 80 flpam5 invam5 1 4 0 primarily thrown 0 item_staff 12 item_staff 0 0 5 0 0 0 371 7 2 0 3 1 2 0 xxx xxx 1 0 diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..97f847e --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "gabordemooij/redbean": "^5.6" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..2b1ad97 --- /dev/null +++ b/composer.lock @@ -0,0 +1,60 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9ea61108070b0206355280587ef0593b", + "packages": [ + { + "name": "gabordemooij/redbean", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/gabordemooij/redbean.git", + "reference": "fac13cd25445129a64be2bbb4457a7d8a4fb9ef0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gabordemooij/redbean/zipball/fac13cd25445129a64be2bbb4457a7d8a4fb9ef0", + "reference": "fac13cd25445129a64be2bbb4457a7d8a4fb9ef0", + "shasum": "" + }, + "require": { + "php": ">=5.3.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RedBeanPHP\\": "RedBeanPHP" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Gabor de Mooij", + "email": "gabor@redbeanphp.com", + "homepage": "https://redbeanphp.com" + } + ], + "description": "RedBeanPHP ORM", + "homepage": "https://redbeanphp.com/", + "keywords": [ + "orm" + ], + "time": "2020-11-27T08:36:10+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/config.php b/config.php new file mode 100644 index 0000000..8963cee --- /dev/null +++ b/config.php @@ -0,0 +1,4 @@ +"; +//print_r($post); +//echo ""; + +if (!empty($_POST)) {fputcsv($fp, $post, "\t");} + diff --git a/uniqueitems.php b/uniqueitems.php index d00c46b..c38d2fa 100644 --- a/uniqueitems.php +++ b/uniqueitems.php @@ -43,10 +43,17 @@ input {

Unique Maker by Hash

'; +// print_r($uni); +// die(); + ?>
Index:
Version: - Enabled: +
+ + + + + +
Rarity: @@ -116,7 +134,7 @@ Usesound:
"; - +$html = ''; foreach (range(01, 12) as $p) { $html .= '

Prop ' . $p . '

'; @@ -135,7 +153,7 @@ foreach (range(01, 12) as $p) echo $html; ?>
-Usesound:
; +
diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..d52780e --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..62ecfd8 --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) Nils Adermann, Jordi Boggiano + +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. diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($vendorDir . '/gabordemooij/redbean/RedBeanPHP'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..47edeae --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,55 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit7881a223d5c4e5800bc6cdeb88fd0b5a::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..dcfa8d9 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,31 @@ + + array ( + 'RedBeanPHP\\' => 11, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'RedBeanPHP\\' => + array ( + 0 => __DIR__ . '/..' . '/gabordemooij/redbean/RedBeanPHP', + ), + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit7881a223d5c4e5800bc6cdeb88fd0b5a::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit7881a223d5c4e5800bc6cdeb88fd0b5a::$prefixDirsPsr4; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..48aa9f6 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,45 @@ +[ + { + "name": "gabordemooij/redbean", + "version": "v5.6.2", + "version_normalized": "5.6.2.0", + "source": { + "type": "git", + "url": "https://github.com/gabordemooij/redbean.git", + "reference": "fac13cd25445129a64be2bbb4457a7d8a4fb9ef0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gabordemooij/redbean/zipball/fac13cd25445129a64be2bbb4457a7d8a4fb9ef0", + "reference": "fac13cd25445129a64be2bbb4457a7d8a4fb9ef0", + "shasum": "" + }, + "require": { + "php": ">=5.3.4" + }, + "time": "2020-11-27T08:36:10+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "RedBeanPHP\\": "RedBeanPHP" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Gabor de Mooij", + "email": "gabor@redbeanphp.com", + "homepage": "https://redbeanphp.com" + } + ], + "description": "RedBeanPHP ORM", + "homepage": "https://redbeanphp.com/", + "keywords": [ + "orm" + ] + } +] diff --git a/vendor/gabordemooij/redbean/.gitignore b/vendor/gabordemooij/redbean/.gitignore new file mode 100644 index 0000000..adb5817 --- /dev/null +++ b/vendor/gabordemooij/redbean/.gitignore @@ -0,0 +1,29 @@ +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc + +# Logs and databases # +###################### +*.log + +# OS generated files # +###################### +.DS_Store* +ehthumbs.db +Icon? +Thumbs.db +/.project +/.settings/org.eclipse.php.core.prefs +/.settings/org.eclipse.php.debug.core.Debug_Process_Preferences.prefs +/rb.phar +/rb.php +/rb-mysql.php +/rb-postgres.php +/rb-sqlite.php +build/ diff --git a/vendor/gabordemooij/redbean/.travis.yml b/vendor/gabordemooij/redbean/.travis.yml new file mode 100644 index 0000000..dd53950 --- /dev/null +++ b/vendor/gabordemooij/redbean/.travis.yml @@ -0,0 +1,38 @@ +language: php + +matrix: + include: + - php: 5.3 + dist: precise + - php: 5.4 + dist: trusty + - php: 5.5 + dist: trusty + - php: 5.6 + dist: trusty + - php: 7.0 + dist: trusty + - php: 7.1 + dist: trusty + - php: 7.2 + dist: trusty + - php: 7.3 + dist: trusty + - php: 7.4 + dist: trusty + - php: 8.0 + dist: trusty + - php: nightly + dist: trusty + +before_script: + - touch /tmp/oodb.db + - mysql -e 'create database oodb;' + - psql template1 -c 'CREATE EXTENSION "uuid-ossp";' -U postgres + - psql -c 'create database oodb;' -U postgres + - php replica2.php onlyphp + - cp rb.php testing/cli/testcontainer/rb.php + - cd testing/cli + + +script: php runtests.php diff --git a/vendor/gabordemooij/redbean/README.markdown b/vendor/gabordemooij/redbean/README.markdown new file mode 100644 index 0000000..40f9a02 --- /dev/null +++ b/vendor/gabordemooij/redbean/README.markdown @@ -0,0 +1,77 @@ +RedBeanPHP 5 +============ + +[![Build Status](https://travis-ci.org/gabordemooij/redbean.svg?branch=master)](https://travis-ci.org/gabordemooij/redbean) + +RedBeanPHP is an easy to use ORM tool for PHP. + +* Automatically creates tables and columns as you go +* No configuration, just fire and forget +* No complicated package tools, no autoloaders, just ONE file + +Installation (recommended) +--------------------------- + +Download RedBeanPHP from the website: + +https://redbeanphp.com/download + +Extract the archive and put it in your PHP project, voila! + +Optional: sha256sum and check signature. + + +Installation via Composer (not recommended) +----------------------------------------- + +Just open your composer.json file and add the package name ```(e.g. "gabordemooij/redbean": "dev-master")``` in your require list. + +```json +{ + "require": { + "gabordemooij/redbean": "dev-master" + } +} +``` + +**NOTE**: +You will find many examples on the RedBean website make use of RedBean's `R` class. Because of namespaced autoloading in Composer, this class will be available as `\RedbeanPHP\R` instead of `R`. If you desire to use the much shorter `R` alias, you can add a `use` statement at the beginning of your code: + +```php +use \RedBeanPHP\R as R; +``` +**NOTE:** +It is important to note that when using RedBeanPHP with Composer, there are some extra precautions needed when working with [Models](https://redbeanphp.com/index.php?p=/models). Due to the namespace requirements of Composer, when creating Models we need to use the `SimpleModel` to extend, not `RedBean_SimpleModel`. Furthermore, we need to specify the namespace of the `SimpleModel`, so a full example of using a Model with RedBean with Composer is as follows: + +```php +use \RedBeanPHP\R; + +class Model_User extends \RedBeanPHP\SimpleModel +{ + ... +} +``` +Notice that we also need to add the `use \RedBeanPHP\R` statement so that we can use the `R::` shortcut within the Model. + + +Quick Example +------------- + +How we store a book object with RedBeanPHP: +```php +$book = R::dispense("book"); +$book->author = "Santa Claus"; +$book->title = "Secrets of Christmas"; +$id = R::store( $book ); +``` + +Yep, it's that simple. + + +More information +---------------- + +For more information about RedBeanPHP please consult +the RedBeanPHP website: + +https://www.redbeanphp.com/ diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Adapter.php b/vendor/gabordemooij/redbean/RedBeanPHP/Adapter.php new file mode 100644 index 0000000..81a35e3 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Adapter.php @@ -0,0 +1,207 @@ + + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Driver $database ADO Compatible DB Instance + */ + public function __construct( $database ) + { + $this->db = $database; + } + + /** + * Returns a string containing the most recent SQL query + * processed by the database adapter, thus conforming to the + * interface: + * + * @see Adapter::getSQL + * + * Methods like get(), getRow() and exec() cause this SQL cache + * to get filled. If no SQL query has been processed yet this function + * will return an empty string. + * + * @return string + */ + public function getSQL() + { + return $this->sql; + } + + /** + * @see Adapter::exec + */ + public function exec( $sql, $bindings = array(), $noevent = FALSE ) + { + if ( !$noevent ) { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + } + + return $this->db->Execute( $sql, $bindings ); + } + + /** + * @see Adapter::get + */ + public function get( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAll( $sql, $bindings ); + } + + /** + * @see Adapter::getRow + */ + public function getRow( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCol + */ + public function getCol( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetCol( $sql, $bindings ); + } + + /** + * @see Adapter::getAssoc + */ + public function getAssoc( $sql, $bindings = array() ) + { + $this->sql = $sql; + + $this->signal( 'sql_exec', $this ); + + $rows = $this->db->GetAll( $sql, $bindings ); + + if ( !$rows ) return array(); + + $assoc = array(); + + foreach ( $rows as $row ) { + if ( empty( $row ) ) continue; + + $key = array_shift( $row ); + switch ( count( $row ) ) { + case 0: + $value = $key; + break; + case 1: + $value = reset( $row ); + break; + default: + $value = $row; + } + + $assoc[$key] = $value; + } + + return $assoc; + } + + /** + * @see Adapter::getAssocRow + */ + public function getAssocRow($sql, $bindings = array()) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAssocRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCell + */ + public function getCell( $sql, $bindings = array(), $noSignal = NULL ) + { + $this->sql = $sql; + + if ( !$noSignal ) $this->signal( 'sql_exec', $this ); + + return $this->db->GetOne( $sql, $bindings ); + } + + /** + * @see Adapter::getCursor + */ + public function getCursor( $sql, $bindings = array() ) + { + return $this->db->GetCursor( $sql, $bindings ); + } + + /** + * @see Adapter::getInsertID + */ + public function getInsertID() + { + return $this->db->getInsertID(); + } + + /** + * @see Adapter::getAffectedRows + */ + public function getAffectedRows() + { + return $this->db->Affected_Rows(); + } + + /** + * @see Adapter::getDatabase + */ + public function getDatabase() + { + return $this->db; + } + + /** + * @see Adapter::startTransaction + */ + public function startTransaction() + { + $this->db->StartTrans(); + } + + /** + * @see Adapter::commit + */ + public function commit() + { + $this->db->CommitTrans(); + } + + /** + * @see Adapter::rollback + */ + public function rollback() + { + $this->db->FailTrans(); + } + + /** + * @see Adapter::close. + */ + public function close() + { + $this->db->close(); + } + + /** + * Sets initialization code for connection. + * + * @param callable $code + */ + public function setInitCode($code) { + $this->db->setInitCode($code); + } + + /** + * @see Adapter::setOption + */ + public function setOption( $optionKey, $optionValue ) { + if ( method_exists( $this->db, $optionKey ) ) { + call_user_func( array( $this->db, $optionKey ), $optionValue ); + return TRUE; + } + return FALSE; + } + + /** + * @see Adapter::getDatabaseServerVersion + */ + public function getDatabaseServerVersion() + { + return $this->db->DatabaseServerVersion(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/AssociationManager.php b/vendor/gabordemooij/redbean/RedBeanPHP/AssociationManager.php new file mode 100644 index 0000000..7ef7102 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/AssociationManager.php @@ -0,0 +1,364 @@ +oodb->isFrozen() || !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() + ) + ) { + throw $exception; + } + } + + /** + * Internal method. + * Returns the many-to-many related rows of table $type for bean $bean using additional SQL in $sql and + * $bindings bindings. If $getLinks is TRUE, link rows are returned instead. + * + * @param OODBBean $bean reference bean instance + * @param string $type target bean type + * @param string $sql additional SQL snippet + * @param array $bindings bindings for query + * + * @return array + */ + private function relatedRows( $bean, $type, $sql = '', $bindings = array() ) + { + $ids = array( $bean->id ); + $sourceType = $bean->getMeta( 'type' ); + try { + return $this->writer->queryRecordRelated( $sourceType, $type, $ids, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + return array(); + } + } + + /** + * Associates a pair of beans. This method associates two beans, no matter + * what types. Accepts a base bean that contains data for the linking record. + * This method is used by associate. This method also accepts a base bean to be used + * as the template for the link record in the database. + * + * @param OODBBean $bean1 first bean + * @param OODBBean $bean2 second bean + * @param OODBBean $bean base bean (association record) + * + * @return mixed + */ + protected function associateBeans( OODBBean $bean1, OODBBean $bean2, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $property1 = $bean1->getMeta( 'type' ) . '_id'; + $property2 = $bean2->getMeta( 'type' ) . '_id'; + + if ( $property1 == $property2 ) { + $property2 = $bean2->getMeta( 'type' ) . '2_id'; + } + + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $bean->setMeta( "cast.$property1", "id" ); + $bean->setMeta( "cast.$property2", "id" ); + $bean->setMeta( 'sys.buildcommand.unique', array( $property1, $property2 ) ); + + $bean->$property1 = $bean1->id; + $bean->$property2 = $bean2->id; + + $results = array(); + + try { + $id = $this->oodb->store( $bean ); + $results[] = $id; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + return $results; + } + + /** + * Constructor, creates a new instance of the Association Manager. + * The association manager can be used to create and manage + * many-to-many relations (for example sharedLists). In a many-to-many relation, + * one bean can be associated with many other beans, while each of those beans + * can also be related to multiple beans. To create an Association Manager + * instance you'll need to pass a ToolBox object. + * + * @param ToolBox $tools toolbox supplying core RedBeanPHP objects + */ + public function __construct( ToolBox $tools ) + { + $this->oodb = $tools->getRedBean(); + $this->adapter = $tools->getDatabaseAdapter(); + $this->writer = $tools->getWriter(); + $this->toolbox = $tools; + } + + /** + * Creates a table name based on a types array. + * Manages the get the correct name for the linking table for the + * types provided. + * + * @param array $types 2 types as strings + * + * @return string + */ + public function getTable( $types ) + { + return $this->writer->getAssocTable( $types ); + } + + /** + * Associates two beans in a many-to-many relation. + * This method will associate two beans and store the connection between the + * two in a link table. Instead of two single beans this method also accepts + * two sets of beans. Returns the ID or the IDs of the linking beans. + * + * @param OODBBean|array $beans1 one or more beans to form the association + * @param OODBBean|array $beans2 one or more beans to form the association + * + * @return array + */ + public function associate( $beans1, $beans2 ) + { + if ( !is_array( $beans1 ) ) { + $beans1 = array( $beans1 ); + } + + if ( !is_array( $beans2 ) ) { + $beans2 = array( $beans2 ); + } + + $results = array(); + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + $table = $this->getTable( array( $bean1->getMeta( 'type' ), $bean2->getMeta( 'type' ) ) ); + $bean = $this->oodb->dispense( $table ); + $results[] = $this->associateBeans( $bean1, $bean2, $bean ); + } + } + + return ( count( $results ) > 1 ) ? $results : reset( $results ); + } + + /** + * Counts the number of related beans in an N-M relation. + * This method returns the number of beans of type $type associated + * with reference bean(s) $bean. The query can be tuned using an + * SQL snippet for additional filtering. + * + * @param OODBBean|array $bean a bean object or an array of beans + * @param string $type type of bean you're interested in + * @param string $sql SQL snippet (optional) + * @param array $bindings bindings for your SQL string + * + * @return integer + */ + public function relatedCount( $bean, $type, $sql = NULL, $bindings = array() ) + { + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( + 'Expected array or OODBBean but got:' . gettype( $bean ) + ); + } + + if ( !$bean->id ) { + return 0; + } + + $beanType = $bean->getMeta( 'type' ); + + try { + return $this->writer->queryRecordCountRelated( $beanType, $type, $bean->id, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + + return 0; + } + } + + /** + * Breaks the association between two beans. This method unassociates two beans. If the + * method succeeds the beans will no longer form an association. In the database + * this means that the association record will be removed. This method uses the + * OODB trash() method to remove the association links, thus giving FUSE models the + * opportunity to hook-in additional business logic. If the $fast parameter is + * set to boolean TRUE this method will remove the beans without their consent, + * bypassing FUSE. This can be used to improve performance. + * + * @param OODBBean $beans1 first bean in target association + * @param OODBBean $beans2 second bean in target association + * @param boolean $fast if TRUE, removes the entries by query without FUSE + * + * @return void + */ + public function unassociate( $beans1, $beans2, $fast = NULL ) + { + $beans1 = ( !is_array( $beans1 ) ) ? array( $beans1 ) : $beans1; + $beans2 = ( !is_array( $beans2 ) ) ? array( $beans2 ) : $beans2; + + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + try { + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $type1 = $bean1->getMeta( 'type' ); + $type2 = $bean2->getMeta( 'type' ); + + $row = $this->writer->queryRecordLink( $type1, $type2, $bean1->id, $bean2->id ); + + if ( !$row ) return; + + $linkType = $this->getTable( array( $type1, $type2 ) ); + + if ( $fast ) { + $this->writer->deleteRecord( $linkType, array( 'id' => $row['id'] ) ); + + return; + } + + $beans = $this->oodb->convertToBeans( $linkType, array( $row ) ); + + if ( count( $beans ) > 0 ) { + $bean = reset( $beans ); + $this->oodb->trash( $bean ); + } + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + } + } + + /** + * Removes all relations for a bean. This method breaks every connection between + * a certain bean $bean and every other bean of type $type. Warning: this method + * is really fast because it uses a direct SQL query however it does not inform the + * models about this. If you want to notify FUSE models about deletion use a foreach-loop + * with unassociate() instead. (that might be slower though) + * + * @param OODBBean $bean reference bean + * @param string $type type of beans that need to be unassociated + * + * @return void + */ + public function clearRelations( OODBBean $bean, $type ) + { + $this->oodb->store( $bean ); + try { + $this->writer->deleteRelations( $bean->getMeta( 'type' ), $type, $bean->id ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + + /** + * Returns all the beans associated with $bean. + * This method will return an array containing all the beans that have + * been associated once with the associate() function and are still + * associated with the bean specified. The type parameter indicates the + * type of beans you are looking for. You can also pass some extra SQL and + * values for that SQL to filter your results after fetching the + * related beans. + * + * Don't try to make use of subqueries, a subquery using IN() seems to + * be slower than two queries! + * + * Since 3.2, you can now also pass an array of beans instead just one + * bean as the first parameter. + * + * @param OODBBean|array $bean the bean you have + * @param string $type the type of beans you want + * @param string $sql SQL snippet for extra filtering + * @param array $bindings values to be inserted in SQL slots + * + * @return array + */ + public function related( $bean, $type, $sql = '', $bindings = array() ) + { + $sql = $this->writer->glueSQLCondition( $sql ); + $rows = $this->relatedRows( $bean, $type, $sql, $bindings ); + $links = array(); + + foreach ( $rows as $key => $row ) { + if ( !isset( $links[$row['id']] ) ) $links[$row['id']] = array(); + $links[$row['id']][] = $row['linked_by']; + unset( $rows[$key]['linked_by'] ); + } + + $beans = $this->oodb->convertToBeans( $type, $rows ); + foreach ( $beans as $bean ) $bean->setMeta( 'sys.belongs-to', $links[$bean->id] ); + + return $beans; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/BeanCollection.php b/vendor/gabordemooij/redbean/RedBeanPHP/BeanCollection.php new file mode 100644 index 0000000..274f3e7 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/BeanCollection.php @@ -0,0 +1,101 @@ +type = $type; + $this->cursor = $cursor; + $this->repository = $repository; + } + + /** + * Returns the next bean in the collection. + * If called the first time, this will return the first bean in the collection. + * If there are no more beans left in the collection, this method + * will return NULL. + * + * @return OODBBean|NULL + */ + public function next() + { + $row = $this->cursor->getNextItem(); + if ( $row ) { + $beans = $this->repository->convertToBeans( $this->type, array( $row ) ); + return reset( $beans ); + } + return NULL; + } + + /** + * Resets the collection from the start, like a fresh() on a bean. + * + * @return void + */ + public function reset() + { + $this->cursor->reset(); + } + + /** + * Closes the underlying cursor (needed for some databases). + * + * @return void + */ + public function close() + { + $this->cursor->close(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/BeanHelper.php b/vendor/gabordemooij/redbean/RedBeanPHP/BeanHelper.php new file mode 100644 index 0000000..d2b7486 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/BeanHelper.php @@ -0,0 +1,60 @@ +getMeta( 'type' ); + $prefix = defined( 'REDBEAN_MODEL_PREFIX' ) ? REDBEAN_MODEL_PREFIX : '\\Model_'; + + if ( strpos( $model, '_' ) !== FALSE ) { + $modelParts = explode( '_', $model ); + $modelName = ''; + foreach( $modelParts as $part ) { + $modelName .= ucfirst( $part ); + } + $modelName = $prefix . $modelName; + if ( !class_exists( $modelName ) ) { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + } else { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + $obj = self::factory( $modelName ); + $obj->loadBean( $bean ); + return $obj; + } + + /** + * @see BeanHelper::getExtractedToolbox + */ + public function getExtractedToolbox() + { + return Facade::getExtractedToolbox(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Cursor.php b/vendor/gabordemooij/redbean/RedBeanPHP/Cursor.php new file mode 100644 index 0000000..66288b7 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Cursor.php @@ -0,0 +1,49 @@ +res = $res; + $this->fetchStyle = $fetchStyle; + } + + /** + * @see Cursor::getNextItem + */ + public function getNextItem() + { + return $this->res->fetch(); + } + + /** + * @see Cursor::reset + */ + public function reset() + { + $this->close(); + $this->res->execute(); + } + + /** + * @see Cursor::close + */ + public function close() + { + $this->res->closeCursor(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Driver.php b/vendor/gabordemooij/redbean/RedBeanPHP/Driver.php new file mode 100644 index 0000000..ef15932 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Driver.php @@ -0,0 +1,194 @@ +123) will bind the integer 123 to the key :key in the + * SQL. This method has no return value. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array Affected Rows + */ + public function Execute( $sql, $bindings = array() ); + + /** + * Returns the latest insert ID if driver does support this + * feature. + * + * @return integer + */ + public function GetInsertID(); + + /** + * Returns the number of rows affected by the most recent query + * if the currently selected driver driver supports this feature. + * + * @return integer + */ + public function Affected_Rows(); + + /** + * Returns a cursor-like object from the database. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetCursor( $sql, $bindings = array() ); + + /** + * Toggles debug mode. In debug mode the driver will print all + * SQL to the screen together with some information about the + * results. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param boolean $trueFalse turn on/off + * @param Logger $logger logger instance + * + * @return void + */ + public function setDebugMode( $tf, $customLogger ); + + /** + * Starts a transaction. + * + * @return void + */ + public function CommitTrans(); + + /** + * Commits a transaction. + * + * @return void + */ + public function StartTrans(); + + /** + * Rolls back a transaction. + * + * @return void + */ + public function FailTrans(); + + /** + * Resets the internal Query Counter. + * + * @return self + */ + public function resetCounter(); + + /** + * Returns the number of SQL queries processed. + * + * @return integer + */ + public function getQueryCount(); + + /** + * Sets initialization code for connection. + * + * @param callable $code code + * + * @return void + */ + public function setInitCode( $code ); + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion(); +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Driver/RPDO.php b/vendor/gabordemooij/redbean/RedBeanPHP/Driver/RPDO.php new file mode 100644 index 0000000..cfed9e2 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Driver/RPDO.php @@ -0,0 +1,950 @@ + &$value ) { + $k = is_integer( $key ) ? $key + 1 : $key; + + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) { + $statement->bindValue( $k, NULL, \PDO::PARAM_NULL ); + continue; + } + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( !$this->flagUseStringOnlyBinding && AQueryWriter::canBeTreatedAsInt( $value ) && abs( $value ) <= $this->max ) { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + $statement->bindParam( $k, $value, $paramType ); + } + } + + /** + * This method runs the actual SQL query and binds a list of parameters to the query. + * slots. The result of the query will be stored in the protected property + * $rs (always array). The number of rows affected (result of rowcount, if supported by database) + * is stored in protected property $affectedRows. If the debug flag is set + * this function will send debugging output to screen buffer. + * + * @param string $sql the SQL string to be send to database server + * @param array $bindings the values that need to get bound to the query slots + * @param array $options + * + * @return mixed + * @throws SQL + */ + protected function runQuery( $sql, $bindings, $options = array() ) + { + $this->connect(); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( $sql, $bindings ); + } + try { + if ( strpos( 'pgsql', $this->dsn ) === 0 ) { + if (defined('\\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT')) { + $statement = @$this->pdo->prepare($sql, array(\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT => TRUE)); + } else { + $statement = $this->pdo->prepare($sql); + } + } else { + $statement = $this->pdo->prepare( $sql ); + } + $this->bindParams( $statement, $bindings ); + $statement->execute(); + $this->queryCounter ++; + $this->affectedRows = $statement->rowCount(); + if ( $statement->columnCount() ) { + $fetchStyle = ( isset( $options['fetchStyle'] ) ) ? $options['fetchStyle'] : NULL; + if ( isset( $options['noFetch'] ) && $options['noFetch'] ) { + $this->resultArray = array(); + return $statement; + } + $this->resultArray = $statement->fetchAll( $fetchStyle ); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( 'resultset: ' . count( $this->resultArray ) . ' rows' ); + } + } else { + $this->resultArray = array(); + } + } catch ( \PDOException $e ) { + //Unfortunately the code field is supposed to be int by default (php) + //So we need a property to convey the SQL State code. + $err = $e->getMessage(); + if ( $this->loggingEnabled && $this->logger ) $this->logger->log( 'An error occurred: ' . $err ); + $exception = new SQL( $err, 0, $e ); + $exception->setSQLState( $e->getCode() ); + $exception->setDriverDetails( $e->errorInfo ); + throw $exception; + } + } + + /** + * Try to fix MySQL character encoding problems. + * MySQL < 5.5.3 does not support proper 4 byte unicode but they + * seem to have added it with version 5.5.3 under a different label: utf8mb4. + * We try to select the best possible charset based on your version data. + * + * @return void + */ + protected function setEncoding() + { + $driver = $this->pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ); + if ($driver === 'mysql') { + $charset = $this->hasCap( 'utf8mb4' ) ? 'utf8mb4' : 'utf8'; + $collate = $this->hasCap( 'utf8mb4_520' ) ? '_unicode_520_ci' : '_unicode_ci'; + $this->pdo->setAttribute(\PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES '. $charset ); //on every re-connect + /* #624 removed space before SET NAMES because it causes trouble with ProxySQL */ + $this->pdo->exec('SET NAMES '. $charset); //also for current connection + $this->mysqlCharset = $charset; + $this->mysqlCollate = $charset . $collate; + } + } + + /** + * Determine if a database supports a particular feature. + * Currently this function can be used to detect the following features: + * + * - utf8mb4 + * - utf8mb4 520 + * + * Usage: + * + * + * $this->hasCap( 'utf8mb4_520' ); + * + * + * By default, RedBeanPHP uses this method under the hood to make sure + * you use the latest UTF8 encoding possible for your database. + * + * @param $db_cap identifier of database capability + * + * @return int|false Whether the database feature is supported, FALSE otherwise. + **/ + protected function hasCap( $db_cap ) + { + $compare = FALSE; + $version = $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ); + switch ( strtolower( $db_cap ) ) { + case 'utf8mb4': + //oneliner, to boost code coverage (coverage does not span versions) + if ( version_compare( $version, '5.5.3', '<' ) ) { return FALSE; } + $client_version = $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); + /* + * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server. + * mysqlnd has supported utf8mb4 since 5.0.9. + */ + if ( strpos( $client_version, 'mysqlnd' ) !== FALSE ) { + $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); + $compare = version_compare( $client_version, '5.0.9', '>=' ); + } else { + $compare = version_compare( $client_version, '5.5.3', '>=' ); + } + break; + case 'utf8mb4_520': + $compare = version_compare( $version, '5.6', '>=' ); + break; + } + + return $compare; + } + + /** + * Constructor. You may either specify dsn, user and password or + * just give an existing PDO connection. + * + * Usage: + * + * + * $driver = new RPDO( $dsn, $user, $password ); + * + * + * The example above illustrates how to create a driver + * instance from a database connection string (dsn), a username + * and a password. It's also possible to pass a PDO object. + * + * Usage: + * + * + * $driver = new RPDO( $existingConnection ); + * + * + * The second example shows how to create an RPDO instance + * from an existing PDO object. + * + * @param string|object $dsn database connection string + * @param string $user optional, usename to sign in + * @param string $pass optional, password for connection login + * + * @return void + */ + public function __construct( $dsn, $user = NULL, $pass = NULL, $options = array() ) + { + if ( is_object( $dsn ) ) { + $this->pdo = $dsn; + $this->isConnected = TRUE; + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); + // make sure that the dsn at least contains the type + $this->dsn = $this->getDatabaseType(); + } else { + $this->dsn = $dsn; + $this->connectInfo = array( 'pass' => $pass, 'user' => $user ); + if (is_array($options)) $this->connectInfo['options'] = $options; + } + + //PHP 5.3 PDO SQLite has a bug with large numbers: + if ( ( strpos( $this->dsn, 'sqlite' ) === 0 && PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 3 ) || defined('HHVM_VERSION') || $this->dsn === 'test-sqlite-53' ) { + $this->max = 2147483647; //otherwise you get -2147483648 ?! demonstrated in build #603 on Travis. + } elseif ( strpos( $this->dsn, 'cubrid' ) === 0 ) { + $this->max = 2147483647; //bindParam in pdo_cubrid also fails... + } else { + $this->max = PHP_INT_MAX; //the normal value of course (makes it possible to use large numbers in LIMIT clause) + } + } + + /** + * Sets PDO in stringify fetch mode. + * If set to TRUE, this method will make sure all data retrieved from + * the database will be fetched as a string. Default: TRUE. + * + * To set it to FALSE... + * + * Usage: + * + * + * R::getDatabaseAdapter()->getDatabase()->stringifyFetches( FALSE ); + * + * + * Important! + * Note, this method only works if you set the value BEFORE the connection + * has been establish. Also, this setting ONLY works with SOME drivers. + * It's up to the driver to honour this setting. + * + * @param boolean $bool + */ + public function stringifyFetches( $bool ) { + $this->stringifyFetches = $bool; + } + + /** + * Returns the best possible encoding for MySQL based on version data. + * This method can be used to obtain the best character set parameters + * possible for your database when constructing a table creation query + * containing clauses like: CHARSET=... COLLATE=... + * This is a MySQL-specific method and not part of the driver interface. + * + * Usage: + * + * + * $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); + * + * + * @param boolean $retCol pass TRUE to return both charset/collate + * + * @return string|array + */ + public function getMysqlEncoding( $retCol = FALSE ) + { + if( $retCol ) + return array( 'charset' => $this->mysqlCharset, 'collate' => $this->mysqlCollate ); + return $this->mysqlCharset; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return void + */ + public function setUseStringOnlyBinding( $yesNo ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + if ( $this->loggingEnabled && $this->logger && method_exists($this->logger,'setUseStringOnlyBinding')) { + $this->logger->setUseStringOnlyBinding( $this->flagUseStringOnlyBinding ); + } + } + + /** + * Sets the maximum value to be bound as integer, normally + * this value equals PHP's MAX INT constant, however sometimes + * PDO driver bindings cannot bind large integers as integers. + * This method allows you to manually set the max integer binding + * value to manage portability/compatibility issues among different + * PHP builds. This method will return the old value. + * + * @param integer $max maximum value for integer bindings + * + * @return integer + */ + public function setMaxIntBind( $max ) + { + if ( !is_integer( $max ) ) throw new RedException( 'Parameter has to be integer.' ); + $oldMax = $this->max; + $this->max = $max; + return $oldMax; + } + + /** + * Sets initialization code to execute upon connecting. + * + * @param callable $code + * + * @return void + */ + public function setInitCode($code) + { + $this->initCode= $code; + } + + /** + * Establishes a connection to the database using PHP\PDO + * functionality. If a connection has already been established this + * method will simply return directly. This method also turns on + * UTF8 for the database and PDO-ERRMODE-EXCEPTION as well as + * PDO-FETCH-ASSOC. + * + * @return void + */ + public function connect() + { + if ( $this->isConnected ) return; + try { + $user = $this->connectInfo['user']; + $pass = $this->connectInfo['pass']; + $options = array(); + if (isset($this->connectInfo['options']) && is_array($this->connectInfo['options'])) { + $options = $this->connectInfo['options']; + } + $this->pdo = new \PDO( $this->dsn, $user, $pass, $options ); + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $this->stringifyFetches ); + //cant pass these as argument to constructor, CUBRID driver does not understand... + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC ); + $this->isConnected = TRUE; + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } catch ( \PDOException $exception ) { + $matches = array(); + $dbname = ( preg_match( '/dbname=(\w+)/', $this->dsn, $matches ) ) ? $matches[1] : '?'; + throw new \PDOException( 'Could not connect to database (' . $dbname . ').', $exception->getCode() ); + } + } + + /** + * Directly sets PDO instance into driver. + * This method might improve performance, however since the driver does + * not configure this instance terrible things may happen... only use + * this method if you are an expert on RedBeanPHP, PDO and UTF8 connections and + * you know your database server VERY WELL. + * + * - connected TRUE|FALSE (treat this instance as connected, default: TRUE) + * - setEncoding TRUE|FALSE (let RedBeanPHP set encoding for you, default: TRUE) + * - setAttributes TRUE|FALSE (let RedBeanPHP set attributes for you, default: TRUE)* + * - setDSNString TRUE|FALSE (extract DSN string from PDO instance, default: TRUE) + * - stringFetch TRUE|FALSE (whether you want to stringify fetches or not, default: TRUE) + * - runInitCode TRUE|FALSE (run init code if any, default: TRUE) + * + * *attributes: + * - RedBeanPHP will ask database driver to throw Exceptions on errors (recommended for compatibility) + * - RedBeanPHP will ask database driver to use associative arrays when fetching (recommended for compatibility) + * + * @param PDO $pdo PDO instance + * @param array $options Options to apply + * + * @return void + */ + public function setPDO( \PDO $pdo, $options = array() ) { + $this->pdo = $pdo; + + $connected = TRUE; + $setEncoding = TRUE; + $setAttributes = TRUE; + $setDSNString = TRUE; + $runInitCode = TRUE; + $stringFetch = TRUE; + + if ( isset($options['connected']) ) $connected = $options['connected']; + if ( isset($options['setEncoding']) ) $setEncoding = $options['setEncoding']; + if ( isset($options['setAttributes']) ) $setAttributes = $options['setAttributes']; + if ( isset($options['setDSNString']) ) $setDSNString = $options['setDSNString']; + if ( isset($options['runInitCode']) ) $runInitCode = $options['runInitCode']; + if ( isset($options['stringFetch']) ) $stringFetch = $options['stringFetch']; + + if ($connected) $this->connected = $connected; + if ($setEncoding) $this->setEncoding(); + if ($setAttributes) { + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); + $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $stringFetch ); + } + if ($runInitCode) { + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } + if ($setDSNString) $this->dsn = $this->getDatabaseType(); + } + + /** + * @see Driver::GetAll + */ + public function GetAll( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->resultArray; + } + + /** + * @see Driver::GetAssocRow + */ + public function GetAssocRow( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings, array( + 'fetchStyle' => \PDO::FETCH_ASSOC + ) + ); + return $this->resultArray; + } + + /** + * @see Driver::GetCol + */ + public function GetCol( $sql, $bindings = array() ) + { + $rows = $this->GetAll( $sql, $bindings ); + + if ( empty( $rows ) || !is_array( $rows ) ) { + return array(); + } + + $cols = array(); + foreach ( $rows as $row ) { + $cols[] = reset( $row ); + } + + return $cols; + } + + /** + * @see Driver::GetOne + */ + public function GetOne( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( empty( $arr[0] ) || !is_array( $arr[0] ) ) { + return NULL; + } + + return reset( $arr[0] ); + } + + /** + * Alias for getOne(). + * Backward compatibility. + * + * @param string $sql SQL + * @param array $bindings bindings + * + * @return mixed + */ + public function GetCell( $sql, $bindings = array() ) + { + return $this->GetOne( $sql, $bindings ); + } + + /** + * @see Driver::GetRow + */ + public function GetRow( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( is_array( $arr ) && count( $arr ) ) { + return reset( $arr ); + } + + return array(); + } + + /** + * @see Driver::Excecute + */ + public function Execute( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->affectedRows; + } + + /** + * @see Driver::GetInsertID + */ + public function GetInsertID() + { + $this->connect(); + + return (int) $this->pdo->lastInsertId(); + } + + /** + * @see Driver::GetCursor + */ + public function GetCursor( $sql, $bindings = array() ) + { + $statement = $this->runQuery( $sql, $bindings, array( 'noFetch' => TRUE ) ); + $cursor = new PDOCursor( $statement, \PDO::FETCH_ASSOC ); + return $cursor; + } + + /** + * @see Driver::Affected_Rows + */ + public function Affected_Rows() + { + $this->connect(); + return (int) $this->affectedRows; + } + + /** + * @see Driver::setDebugMode + */ + public function setDebugMode( $tf, $logger = NULL ) + { + $this->connect(); + $this->loggingEnabled = (bool) $tf; + if ( $this->loggingEnabled and !$logger ) { + $logger = new RDefault(); + } + $this->setLogger( $logger ); + } + + /** + * Injects Logger object. + * Sets the logger instance you wish to use. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param Logger $logger the logger instance to be used for logging + * + * @return self + */ + public function setLogger( Logger $logger ) + { + $this->logger = $logger; + return $this; + } + + /** + * Gets Logger object. + * Returns the currently active Logger instance. + * + * @return Logger + */ + public function getLogger() + { + return $this->logger; + } + + /** + * @see Driver::StartTrans + */ + public function StartTrans() + { + $this->connect(); + $this->pdo->beginTransaction(); + } + + /** + * @see Driver::CommitTrans + */ + public function CommitTrans() + { + $this->connect(); + $this->pdo->commit(); + } + + /** + * @see Driver::FailTrans + */ + public function FailTrans() + { + $this->connect(); + $this->pdo->rollback(); + } + + /** + * Returns the name of database driver for PDO. + * Uses the PDO attribute DRIVER NAME to obtain the name of the + * PDO driver. Use this method to identify the current PDO driver + * used to provide access to the database. Example of a database + * driver string: + * + * + * mysql + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseType(); + * + * + * The example above prints the current database driver string to + * stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * @return string + */ + public function getDatabaseType() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME ); + } + + /** + * Returns the version identifier string of the database client. + * This method can be used to identify the currently installed + * database client. Note that this method will also establish a connection + * (because this is required to obtain the version information). + * + * Example of a version string: + * + * + * mysqlnd 5.0.12-dev - 20150407 - $Id: b5c5906d452ec590732a93b051f3827e02749b83 $ + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseVersion(); + * + * + * The example above will print the version string to stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * To obtain the database server version, use getDatabaseServerVersion() + * instead. + * + * @return mixed + */ + public function getDatabaseVersion() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); + } + + /** + * Returns the underlying PHP PDO instance. + * For some low-level database operations you'll need access to the PDO + * object. Not that this method is only available in RPDO and other + * PDO based database drivers for RedBeanPHP. Other drivers may not have + * a method like this. The following example demonstrates how to obtain + * a reference to the PDO instance from the facade: + * + * Usage: + * + * + * $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * @return PDO + */ + public function getPDO() + { + $this->connect(); + return $this->pdo; + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public function close() + { + $this->pdo = NULL; + $this->isConnected = FALSE; + } + + /** + * Returns TRUE if the current PDO instance is connected. + * + * @return boolean + */ + public function isConnected() + { + return $this->isConnected && $this->pdo; + } + + /** + * Toggles logging, enables or disables logging. + * + * @param boolean $enable TRUE to enable logging + * + * @return self + */ + public function setEnableLogging( $enable ) + { + $this->loggingEnabled = (boolean) $enable; + return $this; + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return self + */ + public function resetCounter() + { + $this->queryCounter = 0; + return $this; + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return integer + */ + public function getQueryCount() + { + return $this->queryCounter; + } + + /** + * Returns the maximum value treated as integer parameter + * binding. + * + * This method is mainly for testing purposes but it can help + * you solve some issues relating to integer bindings. + * + * @return integer + */ + public function getIntegerBindingMax() + { + return $this->max; + } + + /** + * Sets a query to be executed upon connecting to the database. + * This method provides an opportunity to configure the connection + * to a database through an SQL-based interface. Objects can provide + * an SQL string to be executed upon establishing a connection to + * the database. This has been used to solve issues with default + * foreign key settings in SQLite3 for instance, see Github issues: + * #545 and #548. + * + * @param string $sql SQL query to run upon connecting to database + * + * @return self + */ + public function setInitQuery( $sql ) { + $this->initSQL = $sql; + return $this; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion() { + return trim( strval( $this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION) ) ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/DuplicationManager.php b/vendor/gabordemooij/redbean/RedBeanPHP/DuplicationManager.php new file mode 100644 index 0000000..b5bb93b --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/DuplicationManager.php @@ -0,0 +1,467 @@ +$shared = array(); + + foreach ( $beans as $subBean ) { + array_push( $copy->$shared, $subBean ); + } + } + + /** + * Copies the own beans in a bean, i.e. all the ownBean-lists. + * Each bean in the own-list belongs exclusively to its owner so + * we need to invoke the duplicate method again to duplicate each bean here. + * + * @param OODBBean $copy target bean to copy lists to + * @param string $owned name of the own list + * @param array $beans array with shared beans to copy + * @param array $trail array with former beans to detect recursion + * @param boolean $preserveIDs TRUE means preserve IDs, for export only + * + * @return void + */ + private function copyOwnBeans( OODBBean $copy, $owned, $beans, $trail, $preserveIDs ) + { + $copy->$owned = array(); + foreach ( $beans as $subBean ) { + array_push( $copy->$owned, $this->duplicate( $subBean, $trail, $preserveIDs ) ); + } + } + + /** + * Creates a copy of bean $bean and copies all primitive properties (not lists) + * and the parents beans to the newly created bean. Also sets the ID of the bean + * to 0. + * + * @param OODBBean $bean bean to copy + * + * @return OODBBean + */ + private function createCopy( OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + + $copy = $this->redbean->dispense( $type ); + $copy->setMeta( 'sys.dup-from-id', $bean->id ); + $copy->setMeta( 'sys.old-id', $bean->id ); + $copy->importFrom( $bean ); + if ($this->copyMeta) $copy->copyMetaFrom($bean); + $copy->id = 0; + + return $copy; + } + + /** + * Generates a key from the bean type and its ID and determines if the bean + * occurs in the trail, if not the bean will be added to the trail. + * Returns TRUE if the bean occurs in the trail and FALSE otherwise. + * + * @param array $trail list of former beans + * @param OODBBean $bean currently selected bean + * + * @return boolean + */ + private function inTrailOrAdd( &$trail, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $key = $type . $bean->getID(); + + if ( isset( $trail[$key] ) ) { + return TRUE; + } + + $trail[$key] = $bean; + + return FALSE; + } + + /** + * Given the type name of a bean this method returns the canonical names + * of the own-list and the shared-list properties respectively. + * Returns a list with two elements: name of the own-list, and name + * of the shared list. + * + * @param string $typeName bean type name + * + * @return array + */ + private function getListNames( $typeName ) + { + $owned = 'own' . ucfirst( $typeName ); + $shared = 'shared' . ucfirst( $typeName ); + + return array( $owned, $shared ); + } + + /** + * Determines whether the bean has an own list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you want to detect + * + * @return boolean + */ + protected function hasOwnList( $type, $target ) + { + return isset( $this->columns[$target][$type . '_id'] ); + } + + /** + * Determines whether the bea has a shared list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you are looking for + * + * @return boolean + */ + protected function hasSharedList( $type, $target ) + { + return in_array( AQueryWriter::getAssocTableFormat( array( $type, $target ) ), $this->tables ); + } + + /** + * @see DuplicationManager::dup + * + * @param OODBBean $bean bean to be copied + * @param array $trail trail to prevent infinite loops + * @param boolean $preserveIDs preserve IDs + * + * @return OODBBean + */ + protected function duplicate( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( $this->inTrailOrAdd( $trail, $bean ) ) return $bean; + + $type = $bean->getMeta( 'type' ); + + $copy = $this->createCopy( $bean ); + foreach ( $this->tables as $table ) { + + if ( !empty( $this->filters ) ) { + if ( !in_array( $table, $this->filters ) ) continue; + } + + list( $owned, $shared ) = $this->getListNames( $table ); + + if ( $this->hasSharedList( $type, $table ) ) { + if ( $beans = $bean->$shared ) { + $this->copySharedBeans( $copy, $shared, $beans ); + } + } elseif ( $this->hasOwnList( $type, $table ) ) { + if ( $beans = $bean->$owned ) { + $this->copyOwnBeans( $copy, $owned, $beans, $trail, $preserveIDs ); + } + + $copy->setMeta( 'sys.shadow.' . $owned, NULL ); + } + + $copy->setMeta( 'sys.shadow.' . $shared, NULL ); + } + + $copy->id = ( $preserveIDs ) ? $bean->id : $copy->id; + + return $copy; + } + + /** + * Constructor, + * creates a new instance of DupManager. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Recursively turns the keys of an array into + * camelCase. + * + * @param array $array array to camelize + * @param boolean $dolphinMode whether you want the exception for IDs. + * + * @return array + */ + public function camelfy( $array, $dolphinMode = FALSE ) { + $newArray = array(); + foreach( $array as $key => $element ) { + $newKey = preg_replace_callback( '/_(\w)/', function( &$matches ){ + return strtoupper( $matches[1] ); + }, $key); + + if ( $dolphinMode ) { + $newKey = preg_replace( '/(\w)Id$/', '$1ID', $newKey ); + } + + $newArray[$newKey] = ( is_array($element) ) ? $this->camelfy( $element, $dolphinMode ) : $element; + } + return $newArray; + } + + /** + * For better performance you can pass the tables in an array to this method. + * If the tables are available the duplication manager will not query them so + * this might be beneficial for performance. + * + * This method allows two array formats: + * + * + * array( TABLE1, TABLE2 ... ) + * + * + * or + * + * + * array( TABLE1 => array( COLUMN1, COLUMN2 ... ) ... ) + * + * + * @param array $tables a table cache array + * + * @return void + */ + public function setTables( $tables ) + { + foreach ( $tables as $key => $value ) { + if ( is_numeric( $key ) ) { + $this->tables[] = $value; + } else { + $this->tables[] = $key; + $this->columns[$key] = $value; + } + } + + $this->cacheTables = TRUE; + } + + /** + * Returns a schema array for cache. + * You can use the return value of this method as a cache, + * store it in RAM or on disk and pass it to setTables later. + * + * @return array + */ + public function getSchema() + { + return $this->columns; + } + + /** + * Indicates whether you want the duplication manager to cache the database schema. + * If this flag is set to TRUE the duplication manager will query the database schema + * only once. Otherwise the duplicationmanager will, by default, query the schema + * every time a duplication action is performed (dup()). + * + * @param boolean $yesNo TRUE to use caching, FALSE otherwise + */ + public function setCacheTables( $yesNo ) + { + $this->cacheTables = $yesNo; + } + + /** + * A filter array is an array with table names. + * By setting a table filter you can make the duplication manager only take into account + * certain bean types. Other bean types will be ignored when exporting or making a + * deep copy. If no filters are set all types will be taking into account, this is + * the default behavior. + * + * @param array $filters list of tables to be filtered + * + * @return void + */ + public function setFilters( $filters ) + { + if ( !is_array( $filters ) ) { + $filters = array( $filters ); + } + + $this->filters = $filters; + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * this function actually passes the arguments to a protected function called + * duplicate() that does all the work. This method takes care of creating a clone + * of the bean to avoid the bean getting tainted (triggering saving when storing it). + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $preserveIDs for internal usage + * + * @return OODBBean + */ + public function dup( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( !count( $this->tables ) ) { + $this->tables = $this->toolbox->getWriter()->getTables(); + } + + if ( !count( $this->columns ) ) { + foreach ( $this->tables as $table ) { + $this->columns[$table] = $this->toolbox->getWriter()->getColumns( $table ); + } + } + + $rs = $this->duplicate( ( clone $bean ), $trail, $preserveIDs ); + + if ( !$this->cacheTables ) { + $this->tables = array(); + $this->columns = array(); + } + + return $rs; + } + + /** + * Exports a collection of beans recursively. + * This method will export an array of beans in the first argument to a + * set of arrays. This can be used to send JSON or XML representations + * of bean hierarchies to the client. + * + * For every bean in the array this method will export: + * + * - contents of the bean + * - all own bean lists (recursively) + * - all shared beans (but not THEIR own lists) + * + * If the second parameter is set to TRUE the parents of the beans in the + * array will be exported as well (but not THEIR parents). + * + * The third parameter can be used to provide a white-list array + * for filtering. This is an array of strings representing type names, + * only the type names in the filter list will be exported. + * + * The fourth parameter can be used to change the keys of the resulting + * export arrays. The default mode is 'snake case' but this leaves the + * keys as-is, because 'snake' is the default case style used by + * RedBeanPHP in the database. You can set this to 'camel' for + * camel cased keys or 'dolphin' (same as camelcase but id will be + * converted to ID instead of Id). + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents also export parents + * @param array $filters only these types (whitelist) + * @param string $caseStyle case style identifier + * @param boolean $meta export meta data as well + * + * @return array + */ + public function exportAll( $beans, $parents = FALSE, $filters = array(), $caseStyle = 'snake', $meta = FALSE) + { + $array = array(); + if ( !is_array( $beans ) ) { + $beans = array( $beans ); + } + $this->copyMeta = $meta; + foreach ( $beans as $bean ) { + $this->setFilters( $filters ); + $duplicate = $this->dup( $bean, array(), TRUE ); + $array[] = $duplicate->export( $meta, $parents, FALSE, $filters ); + } + if ( $caseStyle === 'camel' ) $array = $this->camelfy( $array ); + if ( $caseStyle === 'dolphin' ) $array = $this->camelfy( $array, TRUE ); + return $array; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Facade.php b/vendor/gabordemooij/redbean/RedBeanPHP/Facade.php new file mode 100644 index 0000000..f372060 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Facade.php @@ -0,0 +1,3354 @@ +isFrozen() ) { + try { + $rs = Facade::$adapter->$method( $sql, $bindings ); + } catch ( SQLException $exception ) { + if ( self::$writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) + ,$exception->getDriverDetails() + ) + ) { + return ( $method === 'getCell' ) ? NULL : array(); + } else { + throw $exception; + } + } + + return $rs; + } else { + return Facade::$adapter->$method( $sql, $bindings ); + } + } + + /** + * Sets allow hybrid mode flag. In Hybrid mode (default off), + * store/storeAll take an extra argument to switch to fluid + * mode in case of an exception. You can use this to speed up + * fluid mode. This method returns the previous value of the + * flag. + * + * @param boolean $hybrid + */ + public static function setAllowHybridMode( $hybrid ) + { + $old = self::$allowHybridMode; + self::$allowHybridMode = $hybrid; + return $old; + } + + /** + * Returns the RedBeanPHP version string. + * The RedBeanPHP version string always has the same format "X.Y" + * where X is the major version number and Y is the minor version number. + * Point releases are not mentioned in the version string. + * + * @return string + */ + public static function getVersion() + { + return self::C_REDBEANPHP_VERSION; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public static function getDatabaseServerVersion() + { + return self::$adapter->getDatabaseServerVersion(); + } + + /** + * Tests the database connection. + * Returns TRUE if connection has been established and + * FALSE otherwise. Suppresses any warnings that may + * occur during the testing process and catches all + * exceptions that might be thrown during the test. + * + * @return boolean + */ + public static function testConnection() + { + if ( !isset( self::$adapter ) ) return FALSE; + + $database = self::$adapter->getDatabase(); + try { + @$database->connect(); + } catch ( \Exception $e ) {} + return $database->isConnected(); + } + + /** + * Kickstarts redbean for you. This method should be called before you start using + * RedBeanPHP. The Setup() method can be called without any arguments, in this case it will + * try to create a SQLite database in /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::setup( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that setup() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load + * a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * @param boolean $partialBeans TRUE to enable partial bean updates + * @param array $options Additional (PDO) options to pass + * + * @return ToolBox + */ + public static function setup( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_null( $dsn ) ) { + $dsn = 'sqlite:' . DIRECTORY_SEPARATOR . sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'red.db'; + } + + self::addDatabase( 'default', $dsn, $username, $password, $frozen, $partialBeans, $options ); + self::selectDatabase( 'default' ); + + return self::$toolbox; + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * + * SELECT {table}.* + * + * + * instead of + * + * + * SELECT * + * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $mode ) + { + AQueryWriter::setNarrowFieldMode( $mode ); + } + + /** + * Toggles fluid transactions. By default fluid transactions + * are not active. Starting, committing or rolling back a transaction + * through the facade in fluid mode will have no effect. If you wish + * to replace this standard portable behavor with behavior depending + * on how the used database platform handles fluid (DDL) transactions + * set this flag to TRUE. + * + * @param boolean $mode allow fluid transaction mode + * + * @return void + */ + public static function setAllowFluidTransactions( $mode ) + { + self::$allowFluidTransactions = $mode; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $mode ) + { + self::getWriter()->flushCache(); /* otherwise same queries might fail (see Unit test XNull) */ + return AQueryWriter::useISNULLConditions( $mode ); + } + + /** + * Wraps a transaction around a closure or string callback. + * If an Exception is thrown inside, the operation is automatically rolled back. + * If no Exception happens, it commits automatically. + * It also supports (simulated) nested transactions (that is useful when + * you have many methods that needs transactions but are unaware of + * each other). + * + * Example: + * + * + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( $callback ) + { + return Transaction::transaction( self::$adapter, $callback ); + } + + /** + * Adds a database to the facade, afterwards you can select the database using + * selectDatabase($key), where $key is the name you assigned to this database. + * + * Usage: + * + * + * R::addDatabase( 'database-1', 'sqlite:/tmp/db1.txt' ); + * R::selectDatabase( 'database-1' ); //to select database again + * + * + * This method allows you to dynamically add (and select) new databases + * to the facade. Adding a database with the same key will cause an exception. + * + * @param string $key ID for the database + * @param string $dsn DSN for the database + * @param string $user user for connection + * @param NULL|string $pass password for connection + * @param bool $frozen whether this database is frozen or not + * + * @return void + */ + public static function addDatabase( $key, $dsn, $user = NULL, $pass = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'A database has already been specified for this key.' ); + } + + self::$toolboxes[$key] = self::createToolbox($dsn, $user, $pass, $frozen, $partialBeans, $options); + } + + /** + * Creates a toolbox. This method can be called if you want to use redbean non-static. + * It has the same interface as R::setup(). The createToolbx() method can be called + * without any arguments, in this case it will try to create a SQLite database in + * /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::createToolbox( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that createToolbox() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * + * @return ToolBox + */ + public static function createToolbox( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_object($dsn) ) { + $db = new RPDO( $dsn ); + $dbType = $db->getDatabaseType(); + } else { + $db = new RPDO( $dsn, $username, $password, $options ); + $dbType = substr( $dsn, 0, strpos( $dsn, ':' ) ); + } + + $adapter = new DBAdapter( $db ); + + $writers = array( + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLiteT', + 'cubrid' => 'CUBRID', + 'mysql' => 'MySQL', + 'sqlsrv' => 'SQLServer', + ); + + $wkey = trim( strtolower( $dbType ) ); + if ( !isset( $writers[$wkey] ) ) { + $wkey = preg_replace( '/\W/', '' , $wkey ); + throw new RedException( 'Unsupported database ('.$wkey.').' ); + } + $writerClass = '\\RedBeanPHP\\QueryWriter\\'.$writers[$wkey]; + $writer = new $writerClass( $adapter ); + $redbean = new OODB( $writer, $frozen ); + + if ( $partialBeans ) { + $redbean->getCurrentRepository()->usePartialBeans( $partialBeans ); + } + + return new ToolBox( $redbean, $adapter, $writer ); + } + + /** + * Determines whether a database identified with the specified key has + * already been added to the facade. This function will return TRUE + * if the database indicated by the key is available and FALSE otherwise. + * + * @param string $key the key/name of the database to check for + * + * @return boolean + */ + public static function hasDatabase( $key ) + { + return ( isset( self::$toolboxes[$key] ) ); + } + + /** + * Selects a different database for the Facade to work with. + * If you use the R::setup() you don't need this method. This method is meant + * for multiple database setups. This method selects the database identified by the + * database ID ($key). Use addDatabase() to add a new database, which in turn + * can be selected using selectDatabase(). If you use R::setup(), the resulting + * database will be stored under key 'default', to switch (back) to this database + * use R::selectDatabase( 'default' ). This method returns TRUE if the database has been + * switched and FALSE otherwise (for instance if you already using the specified database). + * + * @param string $key Key of the database to select + * + * @return boolean + */ + public static function selectDatabase( $key, $force = FALSE ) + { + if ( self::$currentDB === $key && !$force ) { + return FALSE; + } + + if ( !isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'Database not found in registry. Add database using R::addDatabase().' ); + } + + self::configureFacadeWithToolbox( self::$toolboxes[$key] ); + self::$currentDB = $key; + + return TRUE; + } + + /** + * Toggles DEBUG mode. + * In Debug mode all SQL that happens under the hood will + * be printed to the screen and/or logged. + * If no database connection has been configured using R::setup() or + * R::selectDatabase() this method will throw an exception. + * + * There are 2 debug styles: + * + * Classic: separate parameter bindings, explicit and complete but less readable + * Fancy: interpersed bindings, truncates large strings, highlighted schema changes + * + * Fancy style is more readable but sometimes incomplete. + * + * The first parameter turns debugging ON or OFF. + * The second parameter indicates the mode of operation: + * + * 0 Log and write to STDOUT classic style (default) + * 1 Log only, class style + * 2 Log and write to STDOUT fancy style + * 3 Log only, fancy style + * + * This function always returns the logger instance created to generate the + * debug messages. + * + * @param boolean $tf debug mode (TRUE or FALSE) + * @param integer $mode mode of operation + * + * @return RDefault + * @throws RedException + */ + public static function debug( $tf = TRUE, $mode = 0 ) + { + if ($mode > 1) { + $mode -= 2; + $logger = new Debug; + } else { + $logger = new RDefault; + } + + if ( !isset( self::$adapter ) ) { + throw new RedException( 'Use R::setup() first.' ); + } + $logger->setMode($mode); + self::$adapter->getDatabase()->setDebugMode( $tf, $logger ); + + return $logger; + } + + /** + * Turns on the fancy debugger. + * In 'fancy' mode the debugger will output queries with bound + * parameters inside the SQL itself. This method has been added to + * offer a convenient way to activate the fancy debugger system + * in one call. + * + * @param boolean $toggle TRUE to activate debugger and select 'fancy' mode + * + * @return void + */ + public static function fancyDebug( $toggle = TRUE ) + { + self::debug( $toggle, 2 ); + } + + /** + * Inspects the database schema. If you pass the type of a bean this + * method will return the fields of its table in the database. + * The keys of this array will be the field names and the values will be + * the column types used to store their values. + * If no type is passed, this method returns a list of all tables in the database. + * + * @param string $type Type of bean (i.e. table) you want to inspect + * + * @return array + */ + public static function inspect( $type = NULL ) + { + return ($type === NULL) ? self::$writer->getTables() : self::$writer->getColumns( $type ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param OODBBean|SimpleModel $bean bean to store + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return integer|string + */ + public static function store( $bean, $unfreezeIfNeeded = FALSE ) + { + $result = NULL; + try { + $result = self::$redbean->store( $bean ); + } catch (SQLException $exception) { + $wasFrozen = self::$redbean->isFrozen(); + if ( !self::$allowHybridMode || !$unfreezeIfNeeded ) throw $exception; + self::freeze( FALSE ); + $result = self::$redbean->store( $bean ); + self::freeze( $wasFrozen ); + } + return $result; + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chilly mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $tf mode of operation (TRUE means frozen) + */ + public static function freeze( $tf = TRUE ) + { + self::$redbean->freeze( $tf ); + } + + /** + * Loads multiple types of beans with the same ID. + * This might look like a strange method, however it can be useful + * for loading a one-to-one relation. In a typical 1-1 relation, + * you have two records sharing the same primary key. + * RedBeanPHP has only limited support for 1-1 relations. + * In general it is recommended to use 1-N for this. + * + * Usage: + * + * + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function loadMulti( $types, $id ) + { + return MultiLoader::load( self::$redbean, $types, $id ); + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * @param string $snippet string to use after select (optional) + * + * @return OODBBean + */ + public static function load( $type, $id, $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + $bean = self::$redbean->load( $type, $id ); + return $bean; + } + + /** + * Same as load, but selects the bean for update, thus locking the bean. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::loadForUpdate( 'bean', $id ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public static function loadForUpdate( $type, $id ) + { + return self::load( $type, $id, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Same as find(), but selects the beans for update, thus locking the beans. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::findForUpdate( + * 'bean', + * ' title LIKE ? ', + * array('title') + * ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findForUpdate( $type, $sql = NULL, $bindings = array() ) + { + return self::find( $type, $sql, $bindings, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Convenience method. + * Same as findForUpdate but returns just one bean and adds LIMIT-clause. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findOneForUpdate( $type, $sql = NULL, $bindings = array() ) + { + $sql = self::getWriter()->glueLimitOne( $sql ); + $beans = self::findForUpdate($type, $sql, $bindings); + return !empty($beans) ? reset($beans) : NULL; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * This facade method also accepts a type-id combination, + * in the latter case this method will attempt to load the specified bean + * and THEN trash it. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string|OODBBean|SimpleModel $beanOrType bean you want to remove from database + * @param integer $id ID if the bean to trash (optional, type-id variant only) + * + * @return void + */ + public static function trash( $beanOrType, $id = NULL ) + { + if ( is_string( $beanOrType ) ) return self::trash( self::load( $beanOrType, $id ) ); + return self::$redbean->trash( $beanOrType ); + } + + /** + * Dispenses a new RedBean OODB Bean for use with + * the rest of the methods. RedBeanPHP thinks in beans, the bean is the + * primary way to interact with RedBeanPHP and the database managed by + * RedBeanPHP. To load, store and delete data from the database using RedBeanPHP + * you exchange these RedBeanPHP OODB Beans. The only exception to this rule + * are the raw query methods like R::getCell() or R::exec() and so on. + * The dispense method is the 'preferred way' to create a new bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) + { + return DispenseHelper::dispense( self::$redbean, $typeOrBeanArray, $num, $alwaysReturnArray ); + } + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( $order, $onlyArrays = FALSE ) + { + return DispenseHelper::dispenseAll( self::$redbean, $order, $onlyArrays ); + } + + /** + * Convience method. Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return array + */ + public static function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + return self::$finder->findOrDispense( $type, $sql, $bindings ); + } + + /** + * Same as findOrDispense but returns just one element. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return OODBBean + */ + public static function findOneOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + $arrayOfBeans = self::findOrDispense( $type, $sql, $bindings ); + return reset($arrayOfBeans); + } + + /** + * Finds beans using a type and optional SQL statement. + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * Your SQL does not have to start with a WHERE-clause condition. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * @param string $snippet SQL snippet to include in query (for example: FOR UPDATE) + * + * @return array + */ + public static function find( $type, $sql = NULL, $bindings = array(), $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Alias for find(). + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findAll( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public static function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findAndExport( $type, $sql, $bindings ); + } + + /** + * Like R::find() but returns the first bean only. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findOne( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findOne( $type, $sql, $bindings ); + } + + /** + * @deprecated + * + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * Please do not use this function, it is horribly ineffective. + * Instead use a reversed ORDER BY clause and a LIMIT 1 with R::findOne(). + * This function should never be used and only remains for + * the sake of backward compatibility. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findLast( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findLast( $type, $sql, $bindings ); + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public static function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findCollection( $type, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B + * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * 'matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * + * @return array + */ + public static function findMulti( $types, $sql, $bindings = array(), $remappings = array() ) + { + return self::$finder->findMulti( $types, $sql, $bindings, $remappings ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function batch( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Alias for batch(). Batch method is older but since we added so-called *All + * methods like storeAll, trashAll, dispenseAll and findAll it seemed logical to + * improve the consistency of the Facade API and also add an alias for batch() called + * loadAll. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function loadAll( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return integer + */ + public static function exec( $sql, $bindings = array() ) + { + return self::query( 'exec', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns all rows + * and all columns. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAll( $sql, $bindings = array() ) + { + return self::query( 'get', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single cell. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return string + */ + public static function getCell( $sql, $bindings = array() ) + { + return self::query( 'getCell', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a PDOCursor instance. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return RedBeanPHP\Cursor\PDOCursor + */ + public static function getCursor( $sql, $bindings = array() ) + { + return self::query( 'getCursor', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getRow( $sql, $bindings = array() ) + { + return self::query( 'getRow', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single column. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getCol( $sql, $bindings = array() ) + { + return self::query( 'getCol', $sql, $bindings ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * Results will be returned as an associative array. The first + * column in the select clause will be used for the keys in this array and + * the second column will be used for the values. If only one column is + * selected in the query, both key and value of the array will have the + * value of this field for each row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssoc( $sql, $bindings = array() ) + { + return self::query( 'getAssoc', $sql, $bindings ); + } + + /** + *Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns an associative array. + * Results will be returned as an associative array indexed by the first + * column in the select. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssocRow( $sql, $bindings = array() ) + { + return self::query( 'getAssocRow', $sql, $bindings ); + } + + /** + * Returns the insert ID for databases that support/require this + * functionality. Alias for R::getAdapter()->getInsertID(). + * + * @return mixed + */ + public static function getInsertID() + { + return self::$adapter->getInsertID(); + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * @deprecated + * This function is deprecated in favour of R::duplicate(). + * This function has a confusing method signature, the R::duplicate() function + * only accepts two arguments: bean and filters. + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $pid for internal usage + * @param array $filters white list filter with bean types to duplicate + * + * @return array + */ + public static function dup( $bean, $trail = array(), $pid = FALSE, $filters = array() ) + { + self::$duplicationManager->setFilters( $filters ); + return self::$duplicationManager->dup( $bean, $trail, $pid ); + } + + /** + * Makes a deep copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following: + * + * * All beans in own-lists will be duplicated as well + * * All references to shared beans will be copied but not the shared beans themselves + * * All references to parent objects (_id fields) will be copied but not the parents themselves + * + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * This is a simplified version of the deprecated R::dup() function. + * + * @param OODBBean $bean bean to be copied + * @param array $white white list filter with bean types to duplicate + * + * @return array + */ + public static function duplicate( $bean, $filters = array() ) + { + return self::dup( $bean, array(), FALSE, $filters ); + } + + /** + * Exports a collection of beans. Handy for XML/JSON exports with a + * Javascript framework like Dojo or ExtJS. + * What will be exported: + * + * * contents of the bean + * * all own bean lists (recursively) + * * all shared beans (not THEIR own lists) + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents whether you want parent beans to be exported + * @param array $filters whitelist of types + * @param boolean $meta export meta data as well + * + * @return array + */ + public static function exportAll( $beans, $parents = FALSE, $filters = array(), $meta = FALSE ) + { + return self::$duplicationManager->exportAll( $beans, $parents, $filters, self::$exportCaseStyle, $meta ); + } + + /** + * Selects case style for export. + * This will determine the case style for the keys of exported beans (see exportAll). + * The following options are accepted: + * + * * 'default' RedBeanPHP by default enforces Snake Case (i.e. book_id is_valid ) + * * 'camel' Camel Case (i.e. bookId isValid ) + * * 'dolphin' Dolphin Case (i.e. bookID isValid ) Like CamelCase but ID is written all uppercase + * + * @warning RedBeanPHP transforms camelCase to snake_case using a slightly different + * algorithm, it also converts isACL to is_acl (not is_a_c_l) and bookID to book_id. + * Due to information loss this cannot be corrected. However if you might try + * DolphinCase for IDs it takes into account the exception concerning IDs. + * + * @param string $caseStyle case style identifier + * + * @return void + */ + public static function useExportCase( $caseStyle = 'default' ) + { + if ( !in_array( $caseStyle, array( 'default', 'camel', 'dolphin' ) ) ) throw new RedException( 'Invalid case selected.' ); + self::$exportCaseStyle = $caseStyle; + } + + /** + * Converts a series of rows to beans. + * This method converts a series of rows to beans. + * The type of the desired output beans can be specified in the + * first parameter. The second parameter is meant for the database + * result rows. + * + * Usage: + * + * + * $rows = R::getAll( 'SELECT * FROM ...' ) + * $beans = R::convertToBeans( $rows ); + * + * + * As of version 4.3.2 you can specify a meta-mask. + * Data from columns with names starting with the value specified in the mask + * will be transferred to the meta section of a bean (under data.bundle). + * + * + * $rows = R::getAll( 'SELECT FROM... COUNT(*) AS extra_count ...' ); + * $beans = R::convertToBeans( $rows, 'extra_' ); + * $bean = reset( $beans ); + * $data = $bean->getMeta( 'data.bundle' ); + * $extra_count = $data['extra_count']; + * + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans to produce + * @param array $rows must contain an array of array + * @param string $metamask meta mask to apply (optional) + * + * @return array + */ + public static function convertToBeans( $type, $rows, $metamask = NULL ) + { + return self::$redbean->convertToBeans( $type, $rows, $metamask ); + } + + /** + * Just like converToBeans, but for one bean. + * + * @param string $type type of bean to produce + * @param array $row one row from the database + * @param string $metamask metamask (see convertToBeans) + * + * @return OODBBean|NULL + */ + public static function convertToBean( $type, $row, $metamask = NULL ) + { + if ( !count( $row ) ) return NULL; + $beans = self::$redbean->convertToBeans( $type, array( $row ), $metamask ); + $bean = reset( $beans ); + return $bean; + } + + /** + * Convenience function to 'find' beans from an SQL query. + * Used mostly to obtain a series of beans as well as + * pagination data (to paginate results) and optionally + * other data as well (that should not be considered part of + * a bean). + * + * Example: + * + * $books = R::findFromSQL('book'," + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} ", ['total']); + * + * This is the same as doing (example uses PostgreSQL dialect): + * + * $rows = R::getAll(" + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} + * ", $params); + * $books = R::convertToBeans('book', $rows, ['total']); + * + * The additional data can be obtained using: + * + * $book->info('total'); + * + * For further details see R::convertToBeans(). + * If you set $autoExtract to TRUE and meta mask is an array, + * an array will be returned containing two nested arrays, the + * first of those nested arrays will contain the meta values + * you requested, the second array will contain the beans. + * + * @param string $type Type of bean to produce + * @param string $sql SQL query snippet to use + * @param array $bindings bindings for query (optional) + * @param mixed $metamask meta mask (optional, defaults to 'extra_') + * @param boolean $autoExtract TRUE to return meta mask values as first item of array + * + * @return array + */ + public static function findFromSQL( $type, $sql, $bindings = array(), $metamask = 'extra_', $autoExtract = false) { + $rows = self::query( 'get', $sql, $bindings ); + $beans = array(); + if (count($rows)) $beans = self::$redbean->convertToBeans( $type, $rows, $metamask ); + if ($autoExtract && is_array($metamask)) { + $values = array(); + $firstBean = NULL; + if (count($beans)) $firstBean = reset($beans); + foreach($metamask as $key) { + $values[$key] = ($firstBean) ? $firstBean->info($key) : NULL; + } + return array( $values, $beans ); + } + return $beans; + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public static function hasTag( $bean, $tags, $all = FALSE ) + { + return self::$tagManager->hasTag( $bean, $tags, $all ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public static function untag( $bean, $tagList ) + { + self::$tagManager->untag( $bean, $tagList ); + } + + /** + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public static function tag( OODBBean $bean, $tagList = NULL ) + { + return self::$tagManager->tag( $bean, $tagList ); + } + + /** + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public static function addTags( OODBBean $bean, $tagList ) + { + self::$tagManager->addTags( $bean, $tagList ); + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public static function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->tagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public static function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->taggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as taggedAll() but counts beans only (does not return beans). + * + * @see R::taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTaggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as tagged() but counts beans only (does not return beans). + * + * @see R::tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Wipes all beans of type $beanType. + * + * @param string $beanType type of bean you want to destroy entirely + * + * @return boolean + */ + public static function wipe( $beanType ) + { + return Facade::$redbean->wipe( $beanType ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public static function count( $type, $addSQL = '', $bindings = array() ) + { + return Facade::$redbean->count( $type, $addSQL, $bindings ); + } + + /** + * Configures the facade, want to have a new Writer? A new Object Database or a new + * Adapter and you want it on-the-fly? Use this method to hot-swap your facade with a new + * toolbox. + * + * @param ToolBox $tb toolbox to configure facade with + * + * @return ToolBox + */ + public static function configureFacadeWithToolbox( ToolBox $tb ) + { + $oldTools = self::$toolbox; + self::$toolbox = $tb; + self::$writer = self::$toolbox->getWriter(); + self::$adapter = self::$toolbox->getDatabaseAdapter(); + self::$redbean = self::$toolbox->getRedBean(); + self::$finder = new Finder( self::$toolbox ); + self::$associationManager = new AssociationManager( self::$toolbox ); + self::$tree = new Tree( self::$toolbox ); + self::$redbean->setAssociationManager( self::$associationManager ); + self::$labelMaker = new LabelMaker( self::$toolbox ); + $helper = new SimpleModelHelper(); + $helper->attachEventListeners( self::$redbean ); + if (self::$redbean->getBeanHelper() == NULL) { + self::$redbean->setBeanHelper( new SimpleFacadeBeanHelper ); + } + self::$duplicationManager = new DuplicationManager( self::$toolbox ); + self::$tagManager = new TagManager( self::$toolbox ); + return $oldTools; + } + + /** + * Facade Convience method for adapter transaction system. + * Begins a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function begin() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->startTransaction(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Commits a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function commit() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->commit(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Rolls back a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function rollback() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->rollback(); + return TRUE; + } + + /** + * Returns a list of columns. Format of this array: + * array( fieldname => type ) + * Note that this method only works in fluid mode because it might be + * quite heavy on production servers! + * + * @param string $table name of the table (not type) you want to get columns of + * + * @return array + */ + public static function getColumns( $table ) + { + return self::$writer->getColumns( $table ); + } + + /** + * Generates question mark slots for an array of values. + * Given an array and an optional template string this method + * will produce string containing parameter slots for use in + * an SQL query string. + * + * Usage: + * + * + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + return ArrayTool::genSlots( $array, $template ); + } + + /** + * Convenience method to quickly attach parent beans. + * Although usually this can also be done with findMulti(), that + * approach can be a bit verbose sometimes. This convenience method + * uses a default yet overridable SQL snippet to perform the + * operation, leveraging the power of findMulti(). + * + * Usage: + * + * + * $users = R::find('user'); + * $users = R::loadJoined( $users, 'country' ); + * + * + * This is an alternative for: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $gebruikers)] + * ); + * + * + * @param array $beans a list of OODBBeans + * @param string $type a type string + * @param string $sqlTemplate an SQL template string for the SELECT-query + * + * @return array + */ + public static function loadJoined( $beans, $type, $sqlTemplate = 'SELECT %s.* FROM %s WHERE id IN (%s)' ) + { + if (!count($beans)) return array(); + $ids = array(); + $key = "{$type}_id"; + foreach( $beans as $bean ) $ids[] = $bean->{$key}; + $result = self::findMulti($type, self::genSlots( $beans,sprintf($sqlTemplate, $type, $type, '%s')), $ids, array( Finder::onmap($type, $beans) ) ); + $bean = reset($beans); + return $result[ $bean->getMeta('type') ]; + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + return ArrayTool::flat( $array, $result ); + } + + /** + * Nukes the entire database. + * This will remove all schema structures from the database. + * Only works in fluid mode. Be careful with this method. + * + * @warning dangerous method, will remove all tables, columns etc. + * + * @return void + */ + public static function nuke() + { + return self::wipeAll( TRUE ); + } + + /** + * Truncates or drops all database tables/views. + * Empties the database. If the deleteTables flag is set to TRUE + * this function will also remove the database structures. + * The latter only works in fluid mode. + * + * @param boolean $alsoDeleteTables TRUE to clear entire database. + * + * @return void + */ + public static function wipeAll( $alsoDeleteTables = FALSE ) + { + if ( $alsoDeleteTables ) { + if ( !self::$redbean->isFrozen() ) { + self::$writer->wipeAll(); + } + } else { + foreach ( self::$writer->getTables() as $table ) { + self::wipe( $table ); + } + } + } + + /** + * Short hand function to store a set of beans at once, IDs will be + * returned as an array. For information please consult the R::store() + * function. + * A loop saver. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param array $beans list of beans to be stored + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return array + */ + public static function storeAll( $beans, $unfreezeIfNeeded = FALSE ) + { + $ids = array(); + foreach ( $beans as $bean ) { + $ids[] = self::store( $bean, $unfreezeIfNeeded ); + } + return $ids; + } + + /** + * Short hand function to trash a set of beans at once. + * For information please consult the R::trash() function. + * A loop saver. + * + * @param array $beans list of beans to be trashed + * + * @return void + */ + public static function trashAll( $beans ) + { + $numberOfDeletion = 0; + foreach ( $beans as $bean ) { + $numberOfDeletion += self::trash( $bean ); + } + return $numberOfDeletion; + } + + /** + * Short hand function to trash a series of beans using + * only IDs. This function combines trashAll and batch loading + * in one call. Note that while this function accepts just + * bean IDs, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have beeb + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * @param string type $type the bean type you wish to trash + * @param string array $ids list of bean IDs + * + * @return void + */ + public static function trashBatch( $type, $ids ) + { + self::trashAll( self::batch( $type, $ids ) ); + } + + /** + * Short hand function to find and trash beans. + * This function combines trashAll and find. + * Given a bean type, a query snippet and optionally some parameter + * bindings, this function will search for the beans described in the + * query and its parameters and then feed them to the trashAll function + * to be trashed. + * + * Note that while this function accepts just + * a bean type and query snippet, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have been + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * Returns the number of beans deleted. + * + * @param string $type bean type to look for in database + * @param string $sqlSnippet an SQL query snippet + * @param array $bindings SQL parameter bindings + * + * @return int + */ + public static function hunt( $type, $sqlSnippet = NULL, $bindings = array() ) + { + $numberOfTrashedBeans = 0; + $beans = self::findCollection( $type, $sqlSnippet, $bindings ); + while( $bean = $beans->next() ) { + self::trash( $bean ); + $numberOfTrashedBeans++; + } + return $numberOfTrashedBeans; + } + + /** + * Toggles Writer Cache. + * Turns the Writer Cache on or off. The Writer Cache is a simple + * query based caching system that may improve performance without the need + * for cache management. This caching system will cache non-modifying queries + * that are marked with special SQL comments. As soon as a non-marked query + * gets executed the cache will be flushed. Only non-modifying select queries + * have been marked therefore this mechanism is a rather safe way of caching, requiring + * no explicit flushes or reloads. Of course this does not apply if you intend to test + * or simulate concurrent querying. + * + * @param boolean $yesNo TRUE to enable cache, FALSE to disable cache + * + * @return void + */ + public static function useWriterCache( $yesNo ) + { + self::getWriter()->setUseCache( $yesNo ); + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public static function dispenseLabels( $type, $labels ) + { + return self::$labelMaker->dispenseLabels( $type, $labels ); + } + + /** + * Generates and returns an ENUM value. This is how RedBeanPHP handles ENUMs. + * Either returns a (newly created) bean respresenting the desired ENUM + * value or returns a list of all enums for the type. + * + * To obtain (and add if necessary) an ENUM value: + * + * + * $tea->flavour = R::enum( 'flavour:apple' ); + * + * + * Returns a bean of type 'flavour' with name = apple. + * This will add a bean with property name (set to APPLE) to the database + * if it does not exist yet. + * + * To obtain all flavours: + * + * + * R::enum('flavour'); + * + * + * To get a list of all flavour names: + * + * + * R::gatherLabels( R::enum( 'flavour' ) ); + * + * + * @param string $enum either type or type-value + * + * @return array|OODBBean + */ + public static function enum( $enum ) + { + return self::$labelMaker->enum( $enum ); + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the values of the name properties of each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * @param array $beans list of beans to loop + * + * @return array + */ + public static function gatherLabels( $beans ) + { + return self::$labelMaker->gatherLabels( $beans ); + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public static function close() + { + if ( isset( self::$adapter ) ) { + self::$adapter->close(); + } + } + + /** + * Simple convenience function, returns ISO date formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDate( $time = NULL ) + { + if ( !$time ) { + $time = time(); + } + + return @date( 'Y-m-d', $time ); + } + + /** + * Simple convenience function, returns ISO date time + * formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDateTime( $time = NULL ) + { + if ( !$time ) $time = time(); + return @date( 'Y-m-d H:i:s', $time ); + } + + /** + * Sets the database adapter you want to use. + * The database adapter manages the connection to the database + * and abstracts away database driver specific interfaces. + * + * @param Adapter $adapter Database Adapter for facade to use + * + * @return void + */ + public static function setDatabaseAdapter( Adapter $adapter ) + { + self::$adapter = $adapter; + } + + /** + * Sets the Query Writer you want to use. + * The Query Writer writes and executes database queries using + * the database adapter. It turns RedBeanPHP 'commands' into + * database 'statements'. + * + * @param QueryWriter $writer Query Writer instance for facade to use + * + * @return void + */ + public static function setWriter( QueryWriter $writer ) + { + self::$writer = $writer; + } + + /** + * Sets the OODB you want to use. + * The RedBeanPHP Object oriented database is the main RedBeanPHP + * interface that allows you to store and retrieve RedBeanPHP + * objects (i.e. beans). + * + * @param OODB $redbean Object Database for facade to use + */ + public static function setRedBean( OODB $redbean ) + { + self::$redbean = $redbean; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return DBAdapter + */ + public static function getDatabaseAdapter() + { + return self::$adapter; + } + + /** + * In case you use PDO (which is recommended and the default but not mandatory, hence + * the database adapter), you can use this method to obtain the PDO object directly. + * This is a convenience method, it will do the same as: + * + * + * R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * If the PDO object could not be found, for whatever reason, this method + * will return NULL instead. + * + * @return NULL|PDO + */ + public static function getPDO() + { + $databaseAdapter = self::getDatabaseAdapter(); + if ( is_null( $databaseAdapter ) ) return NULL; + $database = $databaseAdapter->getDatabase(); + if ( is_null( $database ) ) return NULL; + if ( !method_exists( $database, 'getPDO' ) ) return NULL; + return $database->getPDO(); + } + + /** + * Returns the current duplication manager instance. + * + * @return DuplicationManager + */ + public static function getDuplicationManager() + { + return self::$duplicationManager; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return QueryWriter + */ + public static function getWriter() + { + return self::$writer; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return OODB + */ + public static function getRedBean() + { + return self::$redbean; + } + + /** + * Returns the toolbox currently used by the facade. + * To set the toolbox use R::setup() or R::configureFacadeWithToolbox(). + * To create a toolbox use Setup::kickstart(). Or create a manual + * toolbox using the ToolBox class. + * + * @return ToolBox + */ + public static function getToolBox() + { + return self::$toolbox; + } + + /** + * Mostly for internal use, but might be handy + * for some users. + * This returns all the components of the currently + * selected toolbox. + * + * Returns the components in the following order: + * + * # OODB instance (getRedBean()) + * # Database Adapter + * # Query Writer + * # Toolbox itself + * + * @return array + */ + public static function getExtractedToolbox() + { + return array( self::$redbean, self::$adapter, self::$writer, self::$toolbox ); + } + + /** + * Facade method for AQueryWriter::renameAssociation() + * + * @param string|array $from + * @param string $to + * + * @return void + */ + public static function renameAssociation( $from, $to = NULL ) + { + AQueryWriter::renameAssociation( $from, $to ); + } + + /** + * Little helper method for Resty Bean Can server and others. + * Takes an array of beans and exports each bean. + * Unlike exportAll this method does not recurse into own lists + * and shared lists, the beans are exported as-is, only loaded lists + * are exported. + * + * @param array $beans beans + * + * @return array + */ + public static function beansToArray( $beans ) + { + $list = array(); + foreach( $beans as $bean ) $list[] = $bean->export(); + return $list; + } + + /** + * Sets the error mode for FUSE. + * What to do if a FUSE model method does not exist? + * You can set the following options: + * + * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL + * * OODBBean::C_ERR_LOG, logs the incident using error_log + * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE + * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING + * * OODBBean::C_ERR_EXCEPTION, throws an exception + * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) + * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR + * + * + * Custom handler method signature: handler( array ( + * 'message' => string + * 'bean' => OODBBean + * 'method' => string + * ) ) + * + * + * This method returns the old mode and handler as an array. + * + * @param integer $mode mode, determines how to handle errors + * @param callable|NULL $func custom handler (if applicable) + * + * @return array + */ + public static function setErrorHandlingFUSE( $mode, $func = NULL ) + { + return OODBBean::setErrorHandlingFUSE( $mode, $func ); + } + + /** + * Dumps bean data to array. + * Given a one or more beans this method will + * return an array containing first part of the string + * representation of each item in the array. + * + * Usage: + * + * + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + return Dump::dump( $data ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * Example: + * + * + * R::bindFunc( 'read', 'location.point', 'asText' ); + * R::bindFunc( 'write', 'location.point', 'GeomFromText' ); + * + * + * Passing NULL as the function will reset (clear) the function + * for this column/mode. + * + * @param string $mode mode for function: i.e. read or write + * @param string $field field (table.column) to bind function to + * @param string $function SQL function to bind to specified column + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public static function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + self::$redbean->bindFunc( $mode, $field, $function, $isTemplate ); + } + + /** + * Sets global aliases. + * Registers a batch of aliases in one go. This works the same as + * fetchAs but explicitly. For instance if you register + * the alias 'cover' for 'page' a property containing a reference to a + * page bean called 'cover' will correctly return the page bean and not + * a (non-existant) cover bean. + * + * + * R::aliases( array( 'cover' => 'page' ) ); + * $book = R::dispense( 'book' ); + * $page = R::dispense( 'page' ); + * $book->cover = $page; + * R::store( $book ); + * $book = $book->fresh(); + * $cover = $book->cover; + * echo $cover->getMeta( 'type' ); //page + * + * + * The format of the aliases registration array is: + * + * {alias} => {actual type} + * + * In the example above we use: + * + * cover => page + * + * From that point on, every bean reference to a cover + * will return a 'page' bean. + * + * @param array $list list of global aliases to use + * + * @return void + */ + public static function aliases( $list ) + { + OODBBean::aliases( $list ); + } + + /** + * Tries to find a bean matching a certain type and + * criteria set. If no beans are found a new bean + * will be created, the criteria will be imported into this + * bean and the bean will be stored and returned. + * If multiple beans match the criteria only the first one + * will be returned. + * + * @param string $type type of bean to search for + * @param array $like criteria set describing the bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public static function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + return self::$finder->findOrCreate( $type, $like, $sql = '', $hasBeenCreated ); + } + + /** + * Tries to find beans matching the specified type and + * criteria set. + * + * If the optional additional SQL snippet is a condition, it will + * be glued to the rest of the query using the AND operator. + * + * @param string $type type of bean to search for + * @param array $like optional criteria set describing the bean to search for + * @param string $sql optional additional SQL for sorting + * @param array $bindings bindings + * + * @return array + */ + public static function findLike( $type, $like = array(), $sql = '', $bindings = array() ) + { + return self::$finder->findLike( $type, $like, $sql, $bindings ); + } + + /** + * Starts logging queries. + * Use this method to start logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * Usage: + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @return void + */ + public static function startLogging() + { + self::debug( TRUE, RDefault::C_LOGGER_ARRAY ); + } + + /** + * Stops logging and flushes the logs, + * convient method to stop logging of queries. + * Use this method to stop logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return void + */ + public static function stopLogging() + { + self::debug( FALSE ); + } + + /** + * Returns the log entries written after the startLogging. + * + * Use this method to obtain the query logs gathered + * by the logging mechanisms. + * Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * The logs may look like: + * + * [1] => SELECT `book`.* FROM `book` WHERE id > ? -- keep-cache + * [2] => array ( 0 => 0, ) + * [3] => resultset: 1 rows + * + * Basically, element in the array is a log entry. + * Parameter bindings are represented as nested arrays (see 2). + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return array + */ + public static function getLogs() + { + return self::getLogger()->getLogs(); + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return void + */ + public static function resetQueryCount() + { + self::$adapter->getDatabase()->resetCounter(); + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return integer + */ + public static function getQueryCount() + { + return self::$adapter->getDatabase()->getQueryCount(); + } + + /** + * Returns the current logger instance being used by the + * database object. + * + * @return Logger + */ + public static function getLogger() + { + return self::$adapter->getDatabase()->getLogger(); + } + + /** + * @deprecated + */ + public static function setAutoResolve( $automatic = TRUE ){} + + /** + * Toggles 'partial bean mode'. If this mode has been + * selected the repository will only update the fields of a bean that + * have been changed rather than the entire bean. + * Pass the value TRUE to select 'partial mode' for all beans. + * Pass the value FALSE to disable 'partial mode'. + * Pass an array of bean types if you wish to use partial mode only + * for some types. + * This method will return the previous value. + * + * @param boolean|array $yesNoBeans List of type names or 'all' + * + * @return mixed + */ + public static function usePartialBeans( $yesNoBeans ) + { + return self::$redbean->getCurrentRepository()->usePartialBeans( $yesNoBeans ); + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public static function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE ) + { + $quickExport = new QuickExport( self::$toolbox ); + $quickExport->csv( $sql, $bindings, $columns, $path, $output ); + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public static function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) { + $matchUp = new MatchUp( self::$toolbox ); + return $matchUp->matchUp( $type, $sql, $bindings, $onFoundDo, $onNotFoundDo, $bean ); + } + + /** + * @deprecated + * + * Returns an instance of the Look Helper class. + * The instance will be configured with the current toolbox. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * For more details regarding the Look functionality, please consult R::look(). + * @see Facade::look + * @see Look::look + * + * @return Look + */ + public static function getLook() + { + return new Look( self::$toolbox ); + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public static function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + return self::getLook()->look( $sql, $bindings, $keys, $template, $filter, $glue ); + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $bean reference beans + * @param OODBBean|array $other beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public static function diff( $bean, $other, $filters = array( 'created', 'modified' ), $pattern = '%s.%s.%s' ) + { + $diff = new Diff( self::$toolbox ); + return $diff->diff( $bean, $other, $filters, $pattern ); + } + + /** + * The gentleman's way to register a RedBeanPHP ToolBox instance + * with the facade. Stores the toolbox in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to register a toolbox. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return void + */ + public static function addToolBoxWithKey( $key, ToolBox $toolbox ) + { + self::$toolboxes[$key] = $toolbox; + } + + /** + * The gentleman's way to remove a RedBeanPHP ToolBox instance + * from the facade. Removes the toolbox identified by + * the specified key in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to remove a toolbox. + * Returns TRUE if the specified toolbox was found and removed. + * Returns FALSE otherwise. + * + * @param string $key identifier of the toolbox to remove + * + * @return boolean + */ + public static function removeToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return FALSE; + } + unset( self::$toolboxes[$key] ); + return TRUE; + } + + /** + * Returns the toolbox associated with the specified key. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return ToolBox|NULL + */ + public static function getToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return NULL; + } + return self::$toolboxes[$key]; + } + + /** + * Toggles JSON column features. + * Invoking this method with boolean TRUE causes 2 JSON features to be enabled. + * Beans will automatically JSONify any array that's not in a list property and + * the Query Writer (if capable) will attempt to create a JSON column for strings that + * appear to contain JSON. + * + * Feature #1: + * AQueryWriter::useJSONColumns + * + * Toggles support for automatic generation of JSON columns. + * Using JSON columns means that strings containing JSON will + * cause the column to be created (not modified) as a JSON column. + * However it might also trigger exceptions if this means the DB attempts to + * convert a non-json column to a JSON column. + * + * Feature #2: + * OODBBean::convertArraysToJSON + * + * Toggles array to JSON conversion. If set to TRUE any array + * set to a bean property that's not a list will be turned into + * a JSON string. Used together with AQueryWriter::useJSONColumns this + * extends the data type support for JSON columns. + * + * So invoking this method is the same as: + * + * + * AQueryWriter::useJSONColumns( $flag ); + * OODBBean::convertArraysToJSON( $flag ); + * + * + * Unlike the methods above, that return the previous state, this + * method does not return anything (void). + * + * @param boolean $flag feature flag (either TRUE or FALSE) + * + * @return void + */ + public static function useJSONFeatures( $flag ) + { + AQueryWriter::useJSONColumns( $flag ); + OODBBean::convertArraysToJSON( $flag ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean together with all + * child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->children( $bean, $sql, $bindings ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countChildren( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all parent beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countParents( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean along with all parent beans + * in a hierarchically structured bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->parents( $bean, $sql, $bindings ); + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function noNuke( $yesNo ) { + return AQueryWriter::forbidNuke( $yesNo ); + } + + /** + * Selects the feature set you want as specified by + * the label. + * + * Usage: + * + * + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function useFeatureSet( $label ) { + return Feature::feature($label); + } + + /** + * Dynamically extends the facade with a plugin. + * Using this method you can register your plugin with the facade and then + * use the plugin by invoking the name specified plugin name as a method on + * the facade. + * + * Usage: + * + * + * R::ext( 'makeTea', function() { ... } ); + * + * + * Now you can use your makeTea plugin like this: + * + * + * R::makeTea(); + * + * + * @param string $pluginName name of the method to call the plugin + * @param callable $callable a PHP callable + * + * @return void + */ + public static function ext( $pluginName, $callable ) + { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + self::$plugins[$pluginName] = $callable; + } + + /** + * Call static for use with dynamic plugins. This magic method will + * intercept static calls and route them to the specified plugin. + * + * @param string $pluginName name of the plugin + * @param array $params list of arguments to pass to plugin method + * + * @return mixed + */ + public static function __callStatic( $pluginName, $params ) + { + if ( !isset( self::$plugins[$pluginName] ) ) { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + throw new RedException( 'Plugin \''.$pluginName.'\' does not exist, add this plugin using: R::ext(\''.$pluginName.'\')' ); + } + return call_user_func_array( self::$plugins[$pluginName], $params ); + } +} + diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Finder.php b/vendor/gabordemooij/redbean/RedBeanPHP/Finder.php new file mode 100644 index 0000000..957d22c --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Finder.php @@ -0,0 +1,560 @@ +toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product,price', + * 'SELECT shop.*, product.*, price.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id + * LEFT JOIN price ON price.product_id = product.id', [], [ + * Finder::map( 'shop', 'product' ), + * Finder::map( 'product', 'price' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function map($parentName,$childName) { + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child ) use ( $parentName, $childName ) { + $propertyName = 'own' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + $property = "{$parentName}ID"; + return ( $child->$property == $parent->id ); + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'own'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'book,book_tag,tag', + * 'SELECT book.*, book_tag.*, tag.* FROM book + * LEFT JOIN book_tag ON book_tag.book_id = book.id + * LEFT JOIN tag ON book_tag.tag_id = tag.id', [], [ + * Finder::nmMap( 'book', 'tag' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function nmMap( $parentName, $childName ) + { + $types = array($parentName, $childName); + sort( $types ); + $link = implode( '_', $types ); + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child, $beans ) use ( $parentName, $childName, $link ) { + $propertyName = 'shared' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + foreach( $beans[$link] as $linkBean ) { + if ( $linkBean["{$parentName}ID"] == $parent->id && $linkBean["{$childName}ID"] == $child->id ) { + return true; + } + } + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'shared'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * Finder::onMap() -> One-to-N mapping. + * A custom record-to-bean mapping function for findMulti. + * Opposite of Finder::map(). Maps child beans to parents. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product', + * 'SELECT shop.*, product.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id', + * [], [ + * Finder::onmap( 'product', 'shop' ), + * ]); + * + * + * Can also be used for instance to attach related beans + * in one-go to save some queries: + * + * Given $users that have a country_id: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $gebruikers)] + * ); + * + * + * For your convenience, an even shorter notation has been added: + * + * $countries = R::loadJoined( $users, 'country' ); + * + * @param string $parentName name of the parent bean + * @param string|array $childName name of the child bean + * + * @return array + */ + public static function onMap($parentName,$childNameOrBeans) { + return array( + 'a' => $parentName, + 'b' => $childNameOrBeans, + 'matcher' => array( $parentName, "{$parentName}_id" ), + 'do' => 'match' + ); + } + + /** + * Finds a bean using a type and a where clause (SQL). + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function find( $type, $sql = NULL, $bindings = array() ) + { + if ( !is_array( $bindings ) ) { + throw new RedException( + 'Expected array, ' . gettype( $bindings ) . ' given.' + ); + } + + return $this->redbean->find( $type, array(), $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + $arr = array(); + foreach ( $this->find( $type, $sql, $bindings ) as $key => $item ) { + $arr[] = $item->export(); + } + + return $arr; + } + + /** + * Like find() but returns just one bean instead of an array of beans. + * This method will return only the first bean of the array. + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findOne( $type, $sql = NULL, $bindings = array() ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return reset( $items ); + } + + /** + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findLast( $type, $sql = NULL, $bindings = array() ) + { + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return end( $items ); + } + + /** + * Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + $foundBeans = $this->find( $type, $sql, $bindings ); + + if ( empty( $foundBeans ) ) { + return array( $this->redbean->dispense( $type ) ); + } else { + return $foundBeans; + } + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + return $this->redbean->findCollection( $type, $sql, $bindings ); + } + + /** + * Finds or creates a bean. + * Tries to find a bean with certain properties specified in the second + * parameter ($like). If the bean is found, it will be returned. + * If multiple beans are found, only the first will be returned. + * If no beans match the criteria, a new bean will be dispensed, + * the criteria will be imported as properties and this new bean + * will be stored and returned. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * @param string $type type of bean to search for + * @param array $like criteria set describing bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + $beans = $this->findLike( $type, $like, $sql ); + if ( count( $beans ) ) { + $bean = reset( $beans ); + $hasBeenCreated = false; + return $bean; + } + + $bean = $this->redbean->dispense( $type ); + $bean->import( $like ); + $this->redbean->store( $bean ); + $hasBeenCreated = true; + return $bean; + } + + /** + * Finds beans by its type and a certain criteria set. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * If the additional SQL is a condition, this condition will be glued to the rest + * of the query using an AND operator. Note that this is as far as this method + * can go, there is no way to glue additional SQL using an OR-condition. + * This method provides access to an underlying mechanism in the RedBeanPHP architecture + * to find beans using criteria sets. However, please do not use this method + * for complex queries, use plain SQL instead ( the regular find method ) as it is + * more suitable for the job. This method is + * meant for basic search-by-example operations. + * + * @param string $type type of bean to search for + * @param array $conditions criteria set describing the bean to search for + * @param string $sql additional SQL (for sorting) + * @param array $bindings bindings + * + * @return array + */ + public function findLike( $type, $conditions = array(), $sql = '', $bindings = array() ) + { + return $this->redbean->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B OR BEANS + * 'matcher' => + * MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * OR ARRAY + * WITH FIELD on B that should match with FIELD on A + * AND FIELD on A that should match with FIELD on B + * OR TRUE + * TO JUST PERFORM THE DO-FUNCTION ON EVERY A-BEAN + * + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * (ONLY IF MATCHER IS ALSO A FUNCTION) + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * The Query Template parameter is optional as well but can be used to + * set a different SQL template (sprintf-style) for processing the original query. + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @note the performance of this function is poor, if you deal with large number of records + * please use plain SQL instead. This function has been added as a bridge between plain SQL + * and bean oriented approaches but it is really on the edge of both worlds. You can safely + * use this function to load additional records as beans in paginated context, let's say + * 50-250 records. Anything above that will gradually perform worse. RedBeanPHP was never + * intended to replace SQL but offer tooling to integrate SQL with object oriented + * designs. If you have come to this function, you have reached the final border between + * SQL-oriented design and OOP. Anything after this will be just as good as custom mapping + * or plain old database querying. I recommend the latter. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql optional, an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * @param string $queryTemplate optional, query template + * + * @return array + */ + public function findMulti( $types, $sql = NULL, $bindings = array(), $remappings = array(), $queryTemplate = ' %s.%s AS %s__%s' ) + { + if ( !is_array( $types ) ) $types = array_map( 'trim', explode( ',', $types ) ); + if ( is_null( $sql ) ) { + $beans = array(); + foreach( $types as $type ) $beans[$type] = $this->redbean->find( $type ); + } else { + if ( !is_array( $sql ) ) { + $writer = $this->toolbox->getWriter(); + $adapter = $this->toolbox->getDatabaseAdapter(); + + //Repair the query, replace book.* with book.id AS book_id etc.. + foreach( $types as $type ) { + $regex = "#( (`?{$type}`?)\.\*)#"; + if ( preg_match( $regex, $sql, $matches ) ) { + $pattern = $matches[1]; + $table = $matches[2]; + $newSelectorArray = array(); + $columns = $writer->getColumns( $type ); + foreach( $columns as $column => $definition ) { + $newSelectorArray[] = sprintf( $queryTemplate, $table, $column, $type, $column ); + } + $newSelector = implode( ',', $newSelectorArray ); + $sql = str_replace( $pattern, $newSelector, $sql ); + } + } + + $rows = $adapter->get( $sql, $bindings ); + } else { + $rows = $sql; + } + + //Gather the bean data from the query results using the prefix + $wannaBeans = array(); + foreach( $types as $type ) { + $wannaBeans[$type] = array(); + $prefix = "{$type}__"; + foreach( $rows as $rowkey=>$row ) { + $wannaBean = array(); + foreach( $row as $cell => $value ) { + if ( strpos( $cell, $prefix ) === 0 ) { + $property = substr( $cell, strlen( $prefix ) ); + unset( $rows[$rowkey][$cell] ); + $wannaBean[$property] = $value; + } + } + if ( !isset( $wannaBean['id'] ) ) continue; + if ( is_null( $wannaBean['id'] ) ) continue; + $wannaBeans[$type][$wannaBean['id']] = $wannaBean; + } + } + + //Turn the rows into beans + $beans = array(); + foreach( $wannaBeans as $type => $wannabees ) { + $beans[$type] = $this->redbean->convertToBeans( $type, $wannabees ); + } + } + + //Apply additional re-mappings + foreach($remappings as $remapping) { + $a = $remapping['a']; + $b = $remapping['b']; + if (is_array($b)) { + $firstBean = reset($b); + $type = $firstBean->getMeta('type'); + $beans[$type] = $b; + $b = $type; + } + $matcher = $remapping['matcher']; + if (is_callable($matcher) || $matcher === TRUE) { + $do = $remapping['do']; + foreach( $beans[$a] as $bean ) { + if ( $matcher === TRUE ) { + $do( $bean, $beans[$b], $beans, $remapping ); + continue; + } + foreach( $beans[$b] as $putBean ) { + if ( $matcher( $bean, $putBean, $beans ) ) $do( $bean, $putBean, $beans, $remapping ); + } + } + } else { + list($field1, $field2) = $matcher; + foreach( $beans[$b] as $key => $bean ) { + $beans[$b][$key]->{$field1} = $beans[$a][$bean->{$field2}]; + } + } + } + return $beans; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Functions.php b/vendor/gabordemooij/redbean/RedBeanPHP/Functions.php new file mode 100644 index 0000000..b66c4c5 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Functions.php @@ -0,0 +1,78 @@ + + * R::find( 'paint', ' color_id = ? ', [ EID('color:yellow') ] ); + * + * + * If a function called EID() already exists you'll have to write this + * wrapper yourself ;) + * + * @param string $enumName enum code as you would pass to R::enum() + * + * @return mixed + */ +if (!function_exists('EID')) { + + function EID($enumName) + { + return \RedBeanPHP\Facade::enum( $enumName )->id; + } + +} + +/** + * Prints the result of R::dump() to the screen using + * print_r. + * + * @param mixed $data data to dump + * + * @return void + */ +if ( !function_exists( 'dmp' ) ) { + + function dmp( $list ) + { + print_r( \RedBeanPHP\Facade::dump( $list ) ); + } +} + +/** + * Function alias for R::genSlots(). + */ +if ( !function_exists( 'genslots' ) ) { + + function genslots( $slots, $tpl = NULL ) + { + return \RedBeanPHP\Facade::genSlots( $slots, $tpl ); + } +} + +/** + * Function alias for R::flat(). + */ +if ( !function_exists( 'array_flatten' ) ) { + + function array_flatten( $array ) + { + return \RedBeanPHP\Facade::flat( $array ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/LabelMaker.php b/vendor/gabordemooij/redbean/RedBeanPHP/LabelMaker.php new file mode 100644 index 0000000..23010fa --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/LabelMaker.php @@ -0,0 +1,182 @@ +toolbox = $toolbox; + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * + * $people = R::dispenseLabels( 'person', [ 'Santa', 'Claus' ] ); + * + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public function dispenseLabels( $type, $labels ) + { + $labelBeans = array(); + foreach ( $labels as $label ) { + $labelBean = $this->toolbox->getRedBean()->dispense( $type ); + $labelBean->name = $label; + $labelBeans[] = $labelBean; + } + + return $labelBeans; + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the value of the name property for each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * Usage: + * + * + * $o1->name = 'hamburger'; + * $o2->name = 'pizza'; + * implode( ',', R::gatherLabels( [ $o1, $o2 ] ) ); //hamburger,pizza + * + * + * Note that the return value is an array of strings, not beans. + * + * @param array $beans list of beans to loop through + * + * @return array + */ + public function gatherLabels( $beans ) + { + $labels = array(); + + foreach ( $beans as $bean ) { + $labels[] = $bean->name; + } + + sort( $labels ); + + return $labels; + } + + /** + * Fetches an ENUM from the database and creates it if necessary. + * An ENUM has the following format: + * + * + * ENUM:VALUE + * + * + * If you pass 'ENUM' only, this method will return an array of its + * values: + * + * + * implode( ',', R::gatherLabels( R::enum( 'flavour' ) ) ) //'BANANA,MOCCA' + * + * + * If you pass 'ENUM:VALUE' this method will return the specified enum bean + * and create it in the database if it does not exist yet: + * + * + * $bananaFlavour = R::enum( 'flavour:banana' ); + * $bananaFlavour->name; + * + * + * So you can use this method to set an ENUM value in a bean: + * + * + * $shake->flavour = R::enum( 'flavour:banana' ); + * + * + * the property flavour now contains the enum bean, a parent bean. + * In the database, flavour_id will point to the flavour record with name 'banana'. + * + * @param string $enum ENUM specification for label + * + * @return array|OODBBean + */ + public function enum( $enum ) + { + $oodb = $this->toolbox->getRedBean(); + + if ( strpos( $enum, ':' ) === FALSE ) { + $type = $enum; + $value = FALSE; + } else { + list( $type, $value ) = explode( ':', $enum ); + $value = preg_replace( '/\W+/', '_', strtoupper( trim( $value ) ) ); + } + + /** + * We use simply find here, we could use inspect() in fluid mode etc, + * but this would be useless. At first sight it looks clean, you could even + * bake this into find(), however, find not only has to deal with the primary + * search type, people can also include references in the SQL part, so avoiding + * find failures does not matter, this is still the quickest way making use + * of existing functionality. + * + * @note There seems to be a bug in XDebug v2.3.2 causing suppressed + * exceptions like these to surface anyway, to prevent this use: + * + * "xdebug.default_enable = 0" + * + * Also see Github Issue #464 + */ + $values = $oodb->find( $type ); + + if ( $value === FALSE ) { + return $values; + } + + foreach( $values as $enumItem ) { + if ( $enumItem->name === $value ) return $enumItem; + } + + $newEnumItems = $this->dispenseLabels( $type, array( $value ) ); + $newEnumItem = reset( $newEnumItems ); + + $oodb->store( $newEnumItem ); + + return $newEnumItem; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Logger.php b/vendor/gabordemooij/redbean/RedBeanPHP/Logger.php new file mode 100644 index 0000000..b9c9535 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Logger.php @@ -0,0 +1,35 @@ +mode === self::C_LOGGER_ECHO ) { + echo $log; + } else { + $this->logs[] = $log; + } + } else { + if ( $this->mode === self::C_LOGGER_ECHO ) { + echo $argument; + } else { + $this->logs[] = $argument; + } + } + + if ( $this->mode === self::C_LOGGER_ECHO ) echo "
" . PHP_EOL; + } + } + + /** + * Returns the internal log array. + * The internal log array is where all log messages are stored. + * + * @return array + */ + public function getLogs() + { + return $this->logs; + } + + /** + * Clears the internal log array, removing all + * previously stored entries. + * + * @return self + */ + public function clear() + { + $this->logs = array(); + return $this; + } + + /** + * Selects a logging mode. + * There are several options available. + * + * * C_LOGGER_ARRAY - log silently, stores entries in internal log array only + * * C_LOGGER_ECHO - also forward log messages directly to STDOUT + * + * @param integer $mode mode of operation for logging object + * + * @return self + */ + public function setMode( $mode ) + { + if ($mode !== self::C_LOGGER_ARRAY && $mode !== self::C_LOGGER_ECHO ) { + throw new RedException( 'Invalid mode selected for logger, use C_LOGGER_ARRAY or C_LOGGER_ECHO.' ); + } + $this->mode = $mode; + return $this; + } + + /** + * Searches for all log entries in internal log array + * for $needle and returns those entries. + * This method will return an array containing all matches for your + * search query. + * + * @param string $needle phrase to look for in internal log array + * + * @return array + */ + public function grep( $needle ) + { + $found = array(); + foreach( $this->logs as $logEntry ) { + if ( strpos( $logEntry, $needle ) !== FALSE ) $found[] = $logEntry; + } + return $found; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Logger/RDefault/Debug.php b/vendor/gabordemooij/redbean/RedBeanPHP/Logger/RDefault/Debug.php new file mode 100644 index 0000000..6f028c3 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Logger/RDefault/Debug.php @@ -0,0 +1,270 @@ + $value ) { + if ( strpos( $slot, ':' ) === 0 ) { + $newStr = str_replace( $slot, $this->fillInValue( $value ), $newStr ); + } + } + return $newStr; + } + + /** + * Fills in a value of a binding and truncates the + * resulting string if necessary. + * + * @param mixed $value bound value + * + * @return string + */ + protected function fillInValue( $value ) + { + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) $value = 'NULL'; + + if ( $this->flagUseStringOnlyBinding ) $paramType = \PDO::PARAM_STR; + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( \RedBeanPHP\QueryWriter\AQueryWriter::canBeTreatedAsInt( $value ) || $value === 'NULL') { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + if ( strlen( $value ) > ( $this->strLen ) ) { + $value = substr( $value, 0, ( $this->strLen ) ).'... '; + } + + if ($paramType === \PDO::PARAM_STR) { + $value = '\''.$value.'\''; + } + + return $value; + } + + /** + * Dependending on the current mode of operation, + * this method will either log and output to STDIN or + * just log. + * + * Depending on the value of constant PHP_SAPI this function + * will format output for console or HTML. + * + * @param string $str string to log or output and log + * + * @return void + */ + protected function output( $str ) + { + $this->logs[] = $str; + if ( !$this->mode ) { + $highlight = FALSE; + /* just a quick heuritsic to highlight schema changes */ + if ( strpos( $str, 'CREATE' ) === 0 + || strpos( $str, 'ALTER' ) === 0 + || strpos( $str, 'DROP' ) === 0) { + $highlight = TRUE; + } + if (PHP_SAPI === 'cli' && !self::$noCLI) { + if ($highlight) echo "\e[91m"; + echo $str, PHP_EOL; + echo "\e[39m"; + } else { + if ($highlight) { + echo "{$str}"; + } else { + echo $str; + } + echo '
'; + } + } + } + + /** + * Normalizes the slots in an SQL string. + * Replaces question mark slots with :slot1 :slot2 etc. + * + * @param string $sql sql to normalize + * + * @return string + */ + protected function normalizeSlots( $sql ) + { + $newSql = $sql; + $i = 0; + while(strpos($newSql, '?') !== FALSE ){ + $pos = strpos( $newSql, '?' ); + $slot = ':slot'.$i; + $begin = substr( $newSql, 0, $pos ); + $end = substr( $newSql, $pos+1 ); + if (PHP_SAPI === 'cli' && !self::$noCLI) { + $newSql = "{$begin}\e[32m{$slot}\e[39m{$end}"; + } else { + $newSql = "{$begin}$slot{$end}"; + } + $i ++; + } + return $newSql; + } + + /** + * Normalizes the bindings. + * Replaces numeric binding keys with :slot1 :slot2 etc. + * + * @param array $bindings bindings to normalize + * + * @return array + */ + protected function normalizeBindings( $bindings ) + { + $i = 0; + $newBindings = array(); + foreach( $bindings as $key => $value ) { + if ( is_numeric($key) ) { + $newKey = ':slot'.$i; + $newBindings[$newKey] = $value; + $i++; + } else { + $newBindings[$key] = $value; + } + } + return $newBindings; + } + + /** + * Logger method. + * + * Takes a number of arguments tries to create + * a proper debug log based on the available data. + * + * @return void + */ + public function log() + { + if ( func_num_args() < 1 ) return; + + $sql = func_get_arg( 0 ); + + if ( func_num_args() < 2) { + $bindings = array(); + } else { + $bindings = func_get_arg( 1 ); + } + + if ( !is_array( $bindings ) ) { + return $this->output( $sql ); + } + + $newSql = $this->normalizeSlots( $sql ); + $newBindings = $this->normalizeBindings( $bindings ); + $newStr = $this->writeQuery( $newSql, $newBindings ); + $this->output( $newStr ); + } + + /** + * Sets the max string length for the parameter output in + * SQL queries. Set this value to a reasonable number to + * keep you SQL queries readable. + * + * @param integer $len string length + * + * @return self + */ + public function setParamStringLength( $len = 20 ) + { + $this->strLen = max(0, $len); + return $this; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return self + */ + public function setUseStringOnlyBinding( $yesNo = false ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + return $this; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/OODB.php b/vendor/gabordemooij/redbean/RedBeanPHP/OODB.php new file mode 100644 index 0000000..53f702d --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/OODB.php @@ -0,0 +1,596 @@ +unbox(); + } + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( 'OODB Store requires a bean, got: ' . gettype( $bean ) ); + } + + return $bean; + } + + /** + * Constructor, requires a query writer. + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param QueryWriter $writer writer + * @param array|boolean $frozen mode of operation: TRUE (frozen), FALSE (default, fluid) or ARRAY (chilled) + */ + public function __construct( QueryWriter $writer, $frozen = FALSE ) + { + if ( $writer instanceof QueryWriter ) { + $this->writer = $writer; + } + + $this->freeze( $frozen ); + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chilly mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $toggle TRUE if you want to use OODB instance in frozen mode + * + * @return void + */ + public function freeze( $toggle ) + { + if ( is_array( $toggle ) ) { + $this->chillList = $toggle; + $this->isFrozen = FALSE; + } else { + $this->isFrozen = (boolean) $toggle; + } + + if ( $this->isFrozen ) { + if ( !$this->frozenRepository ) { + $this->frozenRepository = new FrozenRepo( $this, $this->writer ); + } + + $this->repository = $this->frozenRepository; + + } else { + if ( !$this->fluidRepository ) { + $this->fluidRepository = new FluidRepo( $this, $this->writer ); + } + + $this->repository = $this->fluidRepository; + } + + if ( count( self::$sqlFilters ) ) { + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } + + } + + /** + * Returns the current mode of operation of RedBean. + * In fluid mode the database + * structure is adjusted to accomodate your objects. + * In frozen mode + * this is not the case. + * + * @return boolean + */ + public function isFrozen() + { + return (bool) $this->isFrozen; + } + + /** + * Determines whether a type is in the chill list. + * If a type is 'chilled' it's frozen, so its schema cannot be + * changed anymore. However other bean types may still be modified. + * This method is a convenience method for other objects to check if + * the schema of a certain type is locked for modification. + * + * @param string $type the type you wish to check + * + * @return boolean + */ + public function isChilled( $type ) + { + return (boolean) ( in_array( $type, $this->chillList ) ); + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * @param string $type type of bean you want to dispense + * @param string $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + if ( $number < 1 ) { + if ( $alwaysReturnArray ) return array(); + return NULL; + } + + return $this->repository->dispense( $type, $number, $alwaysReturnArray ); + } + + /** + * Sets bean helper to be given to beans. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @param BeanHelper $beanhelper helper + * + * @return void + */ + public function setBeanHelper( BeanHelper $beanhelper ) + { + $this->beanhelper = $beanhelper; + } + + /** + * Returns the current bean helper. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @return BeanHelper + */ + public function getBeanHelper() + { + return $this->beanhelper; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + $this->repository->check( $bean ); + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + return $this->repository->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Same as find() but returns a BeanCollection. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return BeanCollection + */ + public function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return $this->repository->findCollection( $type, $sql, $bindings ); + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->repository->tableExists( $table ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + $id = $this->repository->store( $bean ); + if ( self::$autoClearHistoryAfterStore ) { + $bean->clearHistory(); + } + return $id; + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + return $this->repository->load( $type, $id ); + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + return $this->repository->trash( $bean ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + return $this->repository->batch( $type, $ids ); + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask mask to apply for meta data + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = NULL ) + { + return $this->repository->convertToBeans( $type, $rows, $mask ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + return $this->repository->count( $type, $addSQL, $bindings ); + } + + /** + * Trash all beans of a given type. Wipes an entire type of bean. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + return $this->repository->wipe( $type ); + } + + /** + * Returns an Association Manager for use with OODB. + * A simple getter function to obtain a reference to the association manager used for + * storage and more. + * + * @return AssociationManager + */ + public function getAssociationManager() + { + if ( !isset( $this->assocManager ) ) { + throw new RedException( 'No association manager available.' ); + } + + return $this->assocManager; + } + + /** + * Sets the association manager instance to be used by this OODB. + * A simple setter function to set the association manager to be used for storage and + * more. + * + * @param AssociationManager $assocManager sets the association manager to be used + * + * @return void + */ + public function setAssociationManager( AssociationManager $assocManager ) + { + $this->assocManager = $assocManager; + } + + /** + * Returns the currently used repository instance. + * For testing purposes only. + * + * @return Repository + */ + public function getCurrentRepository() + { + return $this->repository; + } + + /** + * Clears all function bindings. + * + * @return void + */ + public function clearAllFuncBindings() + { + self::$sqlFilters = array(); + AQueryWriter::setSQLFilters( self::$sqlFilters, FALSE ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * @param string $mode mode to set function for, i.e. read or write + * @param string $field field (table.column) to bind SQL function to + * @param string $function SQL function to bind to field + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + list( $type, $property ) = explode( '.', $field ); + $mode = ($mode === 'write') ? QueryWriter::C_SQLFILTER_WRITE : QueryWriter::C_SQLFILTER_READ; + + if ( !isset( self::$sqlFilters[$mode] ) ) self::$sqlFilters[$mode] = array(); + if ( !isset( self::$sqlFilters[$mode][$type] ) ) self::$sqlFilters[$mode][$type] = array(); + + if ( is_null( $function ) ) { + unset( self::$sqlFilters[$mode][$type][$property] ); + } else { + if ($mode === QueryWriter::C_SQLFILTER_WRITE) { + if ($isTemplate) { + $code = sprintf( $function, '?' ); + } else { + $code = "{$function}(?)"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } else { + if ($isTemplate) { + $code = sprintf( $function, $field ); + } else { + $code = "{$function}({$field})"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } + } + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/OODBBean.php b/vendor/gabordemooij/redbean/RedBeanPHP/OODBBean.php new file mode 100644 index 0000000..45dd134 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/OODBBean.php @@ -0,0 +1,2281 @@ +ownCount() will + * return 0 if the table does not exists. + * Only for backward compatibility. + * Returns previouds value. + * + * @param boolean $toggle toggle + * + * @return boolean + */ + public static function useFluidCount( $toggle ) + { + $old = self::$useFluidCount; + self::$useFluidCount = $toggle; + return $old; + } + + /** + * If this is set to TRUE, the __toString function will + * encode all properties as UTF-8 to repair invalid UTF-8 + * encodings and prevent exceptions (which are uncatchable from within + * a __toString-function). + * + * @param boolean $toggle TRUE to enforce UTF-8 encoding (slower) + * + * @return void + */ + public static function setEnforceUTF8encoding( $toggle ) + { + self::$enforceUTF8encoding = (boolean) $toggle; + } + + /** + * Sets the error mode for FUSE. + * What to do if a FUSE model method does not exist? + * You can set the following options: + * + * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL + * * OODBBean::C_ERR_LOG, logs the incident using error_log + * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE + * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING + * * OODBBean::C_ERR_EXCEPTION, throws an exception + * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) + * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR + * + * + * Custom handler method signature: handler( array ( + * 'message' => string + * 'bean' => OODBBean + * 'method' => string + * ) ) + * + * + * This method returns the old mode and handler as an array. + * + * @param integer $mode error handling mode + * @param callable|NULL $func custom handler + * + * @return array + */ + public static function setErrorHandlingFUSE($mode, $func = NULL) { + if ( + $mode !== self::C_ERR_IGNORE + && $mode !== self::C_ERR_LOG + && $mode !== self::C_ERR_NOTICE + && $mode !== self::C_ERR_WARN + && $mode !== self::C_ERR_EXCEPTION + && $mode !== self::C_ERR_FUNC + && $mode !== self::C_ERR_FATAL + ) throw new \Exception( 'Invalid error mode selected' ); + + if ( $mode === self::C_ERR_FUNC && !is_callable( $func ) ) { + throw new \Exception( 'Invalid error handler' ); + } + + $old = array( self::$errorHandlingFUSE, self::$errorHandler ); + self::$errorHandlingFUSE = $mode; + if ( is_callable( $func ) ) { + self::$errorHandler = $func; + } else { + self::$errorHandler = NULL; + } + return $old; + } + + /** + * Toggles array to JSON conversion. If set to TRUE any array + * set to a bean property that's not a list will be turned into + * a JSON string. Used together with AQueryWriter::useJSONColumns this + * extends the data type support for JSON columns. Returns the previous + * value of the flag. + * + * @param boolean $flag flag + * + * @return boolean + */ + public static function convertArraysToJSON( $flag ) + { + $old = self::$convertArraysToJSON; + self::$convertArraysToJSON = $flag; + return $old; + } + + /** + * Sets global aliases. + * Registers a batch of aliases in one go. This works the same as + * fetchAs and setAutoResolve but explicitly. For instance if you register + * the alias 'cover' for 'page' a property containing a reference to a + * page bean called 'cover' will correctly return the page bean and not + * a (non-existant) cover bean. + * + * + * R::aliases( array( 'cover' => 'page' ) ); + * $book = R::dispense( 'book' ); + * $page = R::dispense( 'page' ); + * $book->cover = $page; + * R::store( $book ); + * $book = $book->fresh(); + * $cover = $book->cover; + * echo $cover->getMeta( 'type' ); //page + * + * + * The format of the aliases registration array is: + * + * {alias} => {actual type} + * + * In the example above we use: + * + * cover => page + * + * From that point on, every bean reference to a cover + * will return a 'page' bean. Note that with autoResolve this + * feature along with fetchAs() is no longer very important, although + * relying on explicit aliases can be a bit faster. + * + * @param array $list list of global aliases to use + * + * @return void + */ + public static function aliases( $list ) + { + self::$aliases = $list; + } + + /** + * Return list of global aliases + * + * @return array + */ + public static function getAliases() + { + return self::$aliases; + } + + /** + * Sets a meta property for all beans. This is a quicker way to set + * the meta properties for a collection of beans because this method + * can directly access the property arrays of the beans. + * This method returns the beans. + * + * @param array $beans beans to set the meta property of + * @param string $property property to set + * @param mixed $value value + * + * @return array + */ + public static function setMetaAll( $beans, $property, $value ) + { + foreach( $beans as $bean ) { + if ( $bean instanceof OODBBean ) $bean->__info[ $property ] = $value; + if ( $property == 'type' && !empty($bean->beanHelper)) { + $bean->__info['model'] = $bean->beanHelper->getModelForBean( $bean ); + } + } + return $beans; + } + + /** + * Accesses the shared list of a bean. + * To access beans that have been associated with the current bean + * using a many-to-many relationship use sharedXList where + * X is the type of beans in the list. + * + * Usage: + * + * + * $person = R::load( 'person', $id ); + * $friends = $person->sharedFriendList; + * + * + * The code snippet above demonstrates how to obtain all beans of + * type 'friend' that have associated using an N-M relation. + * This is a private method used by the magic getter / accessor. + * The example illustrates usage through these accessors. + * + * @param string $type the name of the list you want to retrieve + * @param OODB $redbean instance of the RedBeanPHP OODB class + * @param ToolBox $toolbox instance of ToolBox (to get access to core objects) + * + * @return array + */ + private function getSharedList( $type, $redbean, $toolbox ) + { + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + } + $this->via = NULL; + } + $beans = array(); + if ($this->getID()) { + $type = $this->beau( $type ); + $assocManager = $redbean->getAssociationManager(); + $beans = $assocManager->related( $this, $type, $this->withSql, $this->withParams ); + } + return $beans; + } + + /** + * Accesses the ownList. The 'own' list contains beans + * associated using a one-to-many relation. The own-lists can + * be accessed through the magic getter/setter property + * ownXList where X is the type of beans in that list. + * + * Usage: + * + * + * $book = R::load( 'book', $id ); + * $pages = $book->ownPageList; + * + * + * The example above demonstrates how to access the + * pages associated with the book. Since this is a private method + * meant to be used by the magic accessors, the example uses the + * magic getter instead. + * + * @param string $type name of the list you want to retrieve + * @param OODB $oodb The RB OODB object database instance + * + * @return array + */ + private function getOwnList( $type, $redbean ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $parentField = $this->aliasName; + $myFieldLink = $parentField . '_id'; + + $this->__info['sys.alias.' . $type] = $this->aliasName; + + $this->aliasName = NULL; + } else { + $parentField = $this->__info['type']; + $myFieldLink = $parentField . '_id'; + } + $beans = array(); + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + $beans = $redbean->find( $type, array(), $sql, $bindings ); + } + foreach ( $beans as $beanFromList ) { + $beanFromList->__info['sys.parentcache.' . $parentField] = $this; + } + return $beans; + } + + /** + * Initializes a bean. Used by OODB for dispensing beans. + * It is not recommended to use this method to initialize beans. Instead + * use the OODB object to dispense new beans. You can use this method + * if you build your own bean dispensing mechanism. + * This is not recommended. + * + * Unless you know what you are doing, do NOT use this method. + * This is for advanced users only! + * + * @param string $type type of the new bean + * @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model + * + * @return void + */ + public function initializeForDispense( $type, $beanhelper = NULL ) + { + $this->beanHelper = $beanhelper; + $this->__info['type'] = $type; + $this->__info['sys.id'] = 'id'; + $this->__info['sys.orig'] = array( 'id' => 0 ); + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->__info['changelist'] = array(); + if ( $beanhelper ) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + $this->properties['id'] = 0; + } + + /** + * Sets the Bean Helper. Normally the Bean Helper is set by OODB. + * Here you can change the Bean Helper. The Bean Helper is an object + * providing access to a toolbox for the bean necessary to retrieve + * nested beans (bean lists: ownBean, sharedBean) without the need to + * rely on static calls to the facade (or make this class dep. on OODB). + * + * @param BeanHelper $helper helper to use for this bean + * + * @return void + */ + public function setBeanHelper( BeanHelper $helper ) + { + $this->beanHelper = $helper; + } + + /** + * Returns an ArrayIterator so you can treat the bean like + * an array with the properties container as its contents. + * This method is meant for PHP and allows you to access beans as if + * they were arrays, i.e. using array notation: + * + * + * $bean[$key] = $value; + * + * + * Note that not all PHP functions work with the array interface. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator( $this->properties ); + } + + /** + * Imports all values from an associative array $array. Chainable. + * This method imports the values in the first argument as bean + * propery and value pairs. Use the second parameter to provide a + * selection. If a selection array is passed, only the entries + * having keys mentioned in the selection array will be imported. + * Set the third parameter to TRUE to preserve spaces in selection keys. + * + * @param array $array what you want to import + * @param string|array $selection selection of values + * @param boolean $notrim if TRUE selection keys will NOT be trimmed + * + * @return OODBBean + */ + public function import( $array, $selection = FALSE, $notrim = FALSE ) + { + if ( is_string( $selection ) ) { + $selection = explode( ',', $selection ); + } + if ( is_array( $selection ) ) { + if ( $notrim ) { + $selected = array_flip($selection); + } else { + $selected = array(); + foreach ( $selection as $key => $select ) { + $selected[trim( $select )] = TRUE; + } + } + } else { + $selected = FALSE; + } + foreach ( $array as $key => $value ) { + if ( $key != '__info' ) { + if ( !$selected || isset( $selected[$key] ) ) { + if ( is_array($value ) ) { + if ( isset( $value['_type'] ) ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $value['_type'] ); + unset( $value['_type'] ); + $bean->import($value); + $this->$key = $bean; + } else { + $listBeans = array(); + foreach( $value as $listKey => $listItem ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $listItem['_type'] ); + unset( $listItem['_type'] ); + $bean->import($listItem); + $list = &$this->$key; + $list[ $listKey ] = $bean; + } + } + } else { + $this->$key = $value; + } + } + } + } + return $this; + } + + /** + * Imports an associative array directly into the + * internal property array of the bean as well as the + * meta property sys.orig and sets the changed flag to FALSE. + * This is used by the repository objects to inject database rows + * into the beans. It is not recommended to use this method outside + * of a bean repository. + * + * @param array $row a database row + * + * @return self + */ + public function importRow( $row ) + { + $this->properties = $row; + $this->__info['sys.orig'] = $row; + $this->__info['changed'] = FALSE; + $this->__info['changelist'] = array(); + return $this; + } + + /** + * Imports data from another bean. Chainable. + * Copies the properties from the source bean to the internal + * property list. + * + * Usage: + * + * + * $copy->importFrom( $bean ); + * + * + * The example above demonstrates how to make a shallow copy + * of a bean using the importFrom() method. + * + * @param OODBBean $sourceBean the source bean to take properties from + * + * @return OODBBean + */ + public function importFrom( OODBBean $sourceBean ) + { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->properties = $sourceBean->properties; + + return $this; + } + + /** + * Injects the properties of another bean but keeps the original ID. + * Just like import() but keeps the original ID. + * Chainable. + * + * @param OODBBean $otherBean the bean whose properties you would like to copy + * + * @return OODBBean + */ + public function inject( OODBBean $otherBean ) + { + $myID = $this->properties['id']; + $this->import( $otherBean->export( FALSE, FALSE, TRUE ) ); + $this->id = $myID; + + return $this; + } + + /** + * Exports the bean as an array. + * This function exports the contents of a bean to an array and returns + * the resulting array. Depending on the parameters you can also + * export an entire graph of beans, apply filters or exclude meta data. + * + * Usage: + * + * + * $bookData = $book->export( TRUE, TRUE, FALSE, [ 'author' ] ); + * + * + * The example above exports all bean properties to an array + * called $bookData including its meta data, parent objects but without + * any beans of type 'author'. + * + * @param boolean $meta set to TRUE if you want to export meta data as well + * @param boolean $parents set to TRUE if you want to export parents as well + * @param boolean $onlyMe set to TRUE if you want to export only this bean + * @param array $filters optional whitelist for export + * + * @return array + */ + public function export( $meta = FALSE, $parents = FALSE, $onlyMe = FALSE, $filters = array() ) + { + $arr = array(); + if ( $parents ) { + foreach ( $this as $key => $value ) { + if ( substr( $key, -3 ) != '_id' ) continue; + + $prop = substr( $key, 0, strlen( $key ) - 3 ); + $this->$prop; + } + } + $hasFilters = is_array( $filters ) && count( $filters ); + foreach ( $this as $key => $value ) { + if ( !$onlyMe && is_array( $value ) ) { + $vn = array(); + + foreach ( $value as $i => $b ) { + if ( !( $b instanceof OODBBean ) ) continue; + $vn[] = $b->export( $meta, FALSE, FALSE, $filters ); + $value = $vn; + } + } elseif ( $value instanceof OODBBean ) { if ( $hasFilters ) { //has to be on one line, otherwise code coverage miscounts as miss + if ( !in_array( strtolower( $value->getMeta( 'type' ) ), $filters ) ) continue; + } + $value = $value->export( $meta, $parents, FALSE, $filters ); + } + $arr[$key] = $value; + } + if ( $meta ) { + $arr['__info'] = $this->__info; + } + return $arr; + } + + /** + * Implements isset() function for use as an array. + * This allows you to use isset() on bean properties. + * + * Usage: + * + * + * $book->title = 'my book'; + * echo isset($book['title']); //TRUE + * + * + * The example illustrates how one can apply the + * isset() function to a bean. + * + * @param string $property name of the property you want to check + * + * @return boolean + */ + public function __isset( $property ) + { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + return isset( $this->properties[$property] ); + } + + /** + * Checks whether a related bean exists. + * For instance if a post bean has a related author, this method + * can be used to check if the author is set without loading the author. + * This method works by checking the related ID-field. + * + * @param string $property name of the property you wish to check + * + * @return boolean + */ + public function exists( $property ) + { + $property = $this->beau( $property ); + /* fixes issue #549, see Base/Bean test */ + $hiddenRelationField = "{$property}_id"; + if ( array_key_exists( $hiddenRelationField, $this->properties ) ) { + if ( !is_null( $this->properties[$hiddenRelationField] ) ) { + return TRUE; + } + } + return FALSE; + } + + /** + * Returns the ID of the bean. + * If for some reason the ID has not been set, this method will + * return NULL. This is actually the same as accessing the + * id property using $bean->id. The ID of a bean is it's primary + * key and should always correspond with a table column named + * 'id'. + * + * @return string|null + */ + public function getID() + { + return ( isset( $this->properties['id'] ) ) ? (string) $this->properties['id'] : NULL; + } + + /** + * Unsets a property of a bean. + * Magic method, gets called implicitly when + * performing the unset() operation + * on a bean property. + * + * @param string $property property to unset + * + * @return void + */ + public function __unset( $property ) + { + $property = $this->beau( $property ); + + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + unset( $this->properties[$property] ); + $shadowKey = 'sys.shadow.'.$property; + if ( isset( $this->__info[ $shadowKey ] ) ) unset( $this->__info[$shadowKey] ); + //also clear modifiers + $this->clearModifiers(); + return; + } + + /** + * Adds WHERE clause conditions to ownList retrieval. + * For instance to get the pages that belong to a book you would + * issue the following command: $book->ownPage + * However, to order these pages by number use: + * + * + * $book->with(' ORDER BY `number` ASC ')->ownPage + * + * + * the additional SQL snippet will be merged into the final + * query. + * + * @param string $sql SQL to be added to retrieval query. + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function with( $sql, $bindings = array() ) + { + $this->withSql = $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Just like with(). Except that this method prepends the SQL query snippet + * with AND which makes it slightly more comfortable to use a conditional + * SQL snippet. For instance to filter an own-list with pages (belonging to + * a book) on specific chapters you can use: + * + * $book->withCondition(' chapter = 3 ')->ownPage + * + * This will return in the own list only the pages having 'chapter == 3'. + * + * @param string $sql SQL to be added to retrieval query (prefixed by AND) + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function withCondition( $sql, $bindings = array() ) + { + $this->withSql = ' AND ' . $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Tells the bean to (re)load the following list without any + * conditions. If you have an ownList or sharedList with a + * condition you can use this method to reload the entire list. + * + * Usage: + * + * + * $bean->with( ' LIMIT 3 ' )->ownPage; //Just 3 + * $bean->all()->ownPage; //Reload all pages + * + * + * @return self + */ + public function all() + { + $this->all = TRUE; + return $this; + } + + /** + * Tells the bean to only access the list but not load + * its contents. Use this if you only want to add something to a list + * and you have no interest in retrieving its contents from the database. + * + * Usage: + * + * + * $book->noLoad()->ownPage[] = $newPage; + * + * + * In the example above we add the $newPage bean to the + * page list of book without loading all the pages first. + * If you know in advance that you are not going to use + * the contents of the list, you may use the noLoad() modifier + * to make sure the queries required to load the list will not + * be executed. + * + * @return self + */ + public function noLoad() + { + $this->noLoad = TRUE; + return $this; + } + + /** + * Prepares an own-list to use an alias. This is best explained using + * an example. Imagine a project and a person. The project always involves + * two persons: a teacher and a student. The person beans have been aliased in this + * case, so to the project has a teacher_id pointing to a person, and a student_id + * also pointing to a person. Given a project, we obtain the teacher like this: + * + * + * $project->fetchAs('person')->teacher; + * + * + * Now, if we want all projects of a teacher we cant say: + * + * + * $teacher->ownProject + * + * + * because the $teacher is a bean of type 'person' and no project has been + * assigned to a person. Instead we use the alias() method like this: + * + * + * $teacher->alias('teacher')->ownProject + * + * + * now we get the projects associated with the person bean aliased as + * a teacher. + * + * @param string $aliasName the alias name to use + * + * @return OODBBean + */ + public function alias( $aliasName ) + { + $this->aliasName = $this->beau( $aliasName ); + return $this; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * This method returns an array with the properties array and + * the type (string). + * + * @return array + */ + public function getPropertiesAndType() + { + return array( $this->properties, $this->__info['type'] ); + } + + /** + * Turns a camelcase property name into an underscored property name. + * + * Examples: + * + * - oneACLRoute -> one_acl_route + * - camelCase -> camel_case + * + * Also caches the result to improve performance. + * + * @param string $property property to un-beautify + * + * @return string + */ + public function beau( $property ) + { + static $beautifulColumns = array(); + + if ( ctype_lower( $property ) ) return $property; + if ( + ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) + || ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) + || ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) + ) { + + $property = preg_replace( '/List$/', '', $property ); + return $property; + } + if ( !isset( $beautifulColumns[$property] ) ) { + $beautifulColumns[$property] = AQueryWriter::camelsSnake( $property ); + } + return $beautifulColumns[$property]; + } + + /** + * Modifiers are a powerful concept in RedBeanPHP, they make it possible + * to change the way a property has to be loaded. + * RedBeanPHP uses property modifiers using a prefix notation like this: + * + * + * $book->fetchAs('page')->cover; + * + * + * Here, we load a bean of type page, identified by the cover property + * (or cover_id in the database). Because the modifier is called before + * the property is accessed, the modifier must be remembered somehow, + * this changes the state of the bean. Accessing a property causes the + * bean to clear its modifiers. To clear the modifiers manually you can + * use this method. + * + * Usage: + * + * + * $book->with( 'LIMIT 1' ); + * $book->clearModifiers()->ownPageList; + * + * + * In the example above, the 'LIMIT 1' clause is + * cleared before accessing the pages of the book, causing all pages + * to be loaded in the list instead of just one. + * + * @return self + */ + public function clearModifiers() + { + $this->withSql = ''; + $this->withParams = array(); + $this->aliasName = NULL; + $this->fetchType = NULL; + $this->noLoad = FALSE; + $this->all = FALSE; + $this->via = NULL; + return $this; + } + + /** + * Determines whether a list is opened in exclusive mode or not. + * If a list has been opened in exclusive mode this method will return TRUE, + * othwerwise it will return FALSE. + * + * @param string $listName name of the list to check + * + * @return boolean + */ + public function isListInExclusiveMode( $listName ) + { + $listName = $this->beau( $listName ); + + if ( strpos( $listName, 'xown' ) === 0 && ctype_upper( substr( $listName, 4, 1 ) ) ) { + $listName = substr($listName, 1); + } + $listName = lcfirst( substr( $listName, 3 ) ); + return ( isset( $this->__info['sys.exclusive-'.$listName] ) && $this->__info['sys.exclusive-'.$listName] ); + } + + /** + * Magic Getter. Gets the value for a specific property in the bean. + * If the property does not exist this getter will make sure no error + * occurs. This is because RedBean allows you to query (probe) for + * properties. If the property can not be found this method will + * return NULL instead. + * + * Usage: + * + * + * $title = $book->title; + * $pages = $book->ownPageList; + * $tags = $book->sharedTagList; + * + * + * The example aboves lists several ways to invoke the magic getter. + * You can use the magic setter to access properties, own-lists, + * exclusive own-lists (xownLists) and shared-lists. + * + * @param string $property name of the property you wish to obtain the value of + * + * @return mixed + */ + public function &__get( $property ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } + $fieldLink = $property . '_id'; + $exists = isset( $this->properties[$property] ); + + //If not exists and no field link and no list, bail out. + if ( !$exists && !isset($this->$fieldLink) && (!$isOwn && !$isShared )) { + $this->clearModifiers(); + /** + * Github issue: + * Remove $NULL to directly return NULL #625 + * @@ -1097,8 +1097,7 @@ public function &__get( $property ) + * $this->all = FALSE; + * $this->via = NULL; + * + * - $NULL = NULL; + * - return $NULL; + * + return NULL; + * + * leads to regression: + * PHP Stack trace: + * PHP 1. {main}() testje.php:0 + * PHP 2. RedBeanPHP\OODBBean->__get() testje.php:22 + * Notice: Only variable references should be returned by reference in rb.php on line 2529 + */ + $NULL = NULL; + return $NULL; + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $hasAll = (boolean) ($this->all); + + //If exists and no list or exits and list not changed, bail out. + if ( $exists && ((!$isOwn && !$isShared ) || (!$hasSQL && !$differentAlias && !$hasAll)) ) { + $this->clearModifiers(); + return $this->properties[$property]; + } + + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + + //If it's another bean, then we load it and return + if ( isset( $this->$fieldLink ) ) { + $this->__info['tainted'] = TRUE; + if ( isset( $this->__info["sys.parentcache.$property"] ) ) { + $bean = $this->__info["sys.parentcache.$property"]; + } else { + if ( isset( self::$aliases[$property] ) ) { + $type = self::$aliases[$property]; + } elseif ( $this->fetchType ) { + $type = $this->fetchType; + $this->fetchType = NULL; + } else { + $type = $property; + } + $bean = NULL; + if ( !is_null( $this->properties[$fieldLink] ) ) { + $bean = $redbean->load( $type, $this->properties[$fieldLink] ); + } + } + $this->properties[$property] = $bean; + $this->clearModifiers(); + return $this->properties[$property]; + } + + /* Implicit: elseif ( $isOwn || $isShared ) */ + if ( $this->noLoad ) { + $beans = array(); + } elseif ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->properties[$property] = $beans; + $this->__info["sys.shadow.$property"] = $beans; + $this->__info['tainted'] = TRUE; + + $this->clearModifiers(); + return $this->properties[$property]; + + } + + /** + * Magic Setter. Sets the value for a specific property. + * This setter acts as a hook for OODB to mark beans as tainted. + * The tainted meta property can be retrieved using getMeta("tainted"). + * The tainted meta property indicates whether a bean has been modified and + * can be used in various caching mechanisms. + * + * @param string $property name of the property you wish to assign a value to + * @param mixed $value the value you want to assign + * + * @return void + */ + public function __set( $property, $value ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } elseif ( self::$convertArraysToJSON && is_array( $value ) ) { + $value = json_encode( $value ); + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $exists = isset( $this->properties[$property] ); + $fieldLink = $property . '_id'; + $isFieldLink = (($pos = strrpos($property, '_id')) !== FALSE) && array_key_exists( ($fieldName = substr($property, 0, $pos)), $this->properties ); + + + if ( ($isOwn || $isShared) && (!$exists || $hasSQL || $differentAlias) ) { + + if ( !$this->noLoad ) { + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + if ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->__info["sys.shadow.$property"] = $beans; + } + } + + $this->clearModifiers(); + + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + array_push( $this->__info['changelist'], $property ); + + if ( array_key_exists( $fieldLink, $this->properties ) && !( $value instanceof OODBBean ) ) { + if ( is_null( $value ) || $value === FALSE ) { + + unset( $this->properties[ $property ]); + $this->properties[ $fieldLink ] = NULL; + + return; + } else { + throw new RedException( 'Cannot cast to bean.' ); + } + } + + if ( $isFieldLink ){ + unset( $this->properties[ $fieldName ]); + $this->properties[ $property ] = NULL; + } + + + if ( $value === FALSE ) { + $value = '0'; + } elseif ( $value === TRUE ) { + $value = '1'; + /* for some reason there is some kind of bug in xdebug so that it doesnt count this line otherwise... */ + } elseif ( $value instanceof \DateTime ) { $value = $value->format( 'Y-m-d H:i:s' ); } + $this->properties[$property] = $value; + } + + /** + * @deprecated + * + * Sets a property of the bean allowing you to keep track of + * the state yourself. This method sets a property of the bean and + * allows you to control how the state of the bean will be affected. + * + * While there may be some circumstances where this method is needed, + * this method is considered to be extremely dangerous. + * This method is only for advanced users. + * + * @param string $property property + * @param mixed $value value + * @param boolean $updateShadow whether you want to update the shadow + * @param boolean $taint whether you want to mark the bean as tainted + * + * @return void + */ + public function setProperty( $property, $value, $updateShadow = FALSE, $taint = FALSE ) + { + $this->properties[$property] = $value; + + if ( $updateShadow ) { + $this->__info['sys.shadow.' . $property] = $value; + } + + if ( $taint ) { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + } + } + + /** + * Returns the value of a meta property. A meta property + * contains additional information about the bean object that will not + * be stored in the database. Meta information is used to instruct + * RedBeanPHP as well as other systems how to deal with the bean. + * If the property cannot be found this getter will return NULL instead. + * + * Example: + * + * + * $bean->setMeta( 'flush-cache', TRUE ); + * + * + * RedBeanPHP also stores meta data in beans, this meta data uses + * keys prefixed with 'sys.' (system). + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function getMeta( $path, $default = NULL ) + { + return ( isset( $this->__info[$path] ) ) ? $this->__info[$path] : $default; + } + + /** + * Returns a value from the data bundle. + * The data bundle might contain additional data send from an SQL query, + * for instance, the total number of rows. If the property cannot be + * found, the default value will be returned. If no default has + * been specified, this method returns NULL. + * + * @param string $key key + * @param mixed $default default (defaults to NULL) + * + * @return mixed; + */ + public function info( $key, $default = NULL ) { + return ( isset( $this->__info['data.bundle'][$key] ) ) ? $this->__info['data.bundle'][$key] : $default; + } + + /** + * Gets and unsets a meta property. + * Moves a meta property out of the bean. + * This is a short-cut method that can be used instead + * of combining a get/unset. + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function moveMeta( $path, $value = NULL ) + { + if ( isset( $this->__info[$path] ) ) { + $value = $this->__info[ $path ]; + unset( $this->__info[ $path ] ); + } + return $value; + } + + /** + * Stores a value in the specified Meta information property. + * The first argument should be the key to store the value under, + * the second argument should be the value. It is common to use + * a path-like notation for meta data in RedBeanPHP like: + * 'my.meta.data', however the dots are purely for readability, the + * meta data methods do not store nested structures or hierarchies. + * + * @param string $path path / key to store value under + * @param mixed $value value to store in bean (not in database) as meta data + * + * @return OODBBean + */ + public function setMeta( $path, $value ) + { + $this->__info[$path] = $value; + if ( $path == 'type' && !empty($this->beanHelper)) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + + return $this; + } + + /** + * Copies the meta information of the specified bean + * This is a convenience method to enable you to + * exchange meta information easily. + * + * @param OODBBean $bean bean to copy meta data of + * + * @return OODBBean + */ + public function copyMetaFrom( OODBBean $bean ) + { + $this->__info = $bean->__info; + + return $this; + } + + /** + * Sends the call to the registered model. + * This method can also be used to override bean behaviour. + * In that case you don't want an error or exception to be triggered + * if the method does not exist in the model (because it's optional). + * Unfortunately we cannot add an extra argument to __call() for this + * because the signature is fixed. Another option would be to set + * a special flag ( i.e. $this->isOptionalCall ) but that would + * cause additional complexity because we have to deal with extra temporary state. + * So, instead I allowed the method name to be prefixed with '@', in practice + * nobody creates methods like that - however the '@' symbol in PHP is widely known + * to suppress error handling, so we can reuse the semantics of this symbol. + * If a method name gets passed starting with '@' the overrideDontFail variable + * will be set to TRUE and the '@' will be stripped from the function name before + * attempting to invoke the method on the model. This way, we have all the + * logic in one place. + * + * @param string $method name of the method + * @param array $args argument list + * + * @return mixed + */ + public function __call( $method, $args ) + { + if ( empty( $this->__info['model'] ) ) { + return NULL; + } + + $overrideDontFail = FALSE; + if ( strpos( $method, '@' ) === 0 ) { + $method = substr( $method, 1 ); + $overrideDontFail = TRUE; + } + + if ( !is_callable( array( $this->__info['model'], $method ) ) ) { + + if ( self::$errorHandlingFUSE === FALSE || $overrideDontFail ) { + return NULL; + } + + if ( in_array( $method, array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ), TRUE ) ) { + return NULL; + } + + $message = "FUSE: method does not exist in model: $method"; + if ( self::$errorHandlingFUSE === self::C_ERR_LOG ) { + error_log( $message ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_NOTICE ) { + trigger_error( $message, E_USER_NOTICE ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_WARN ) { + trigger_error( $message, E_USER_WARNING ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_EXCEPTION ) { + throw new \Exception( $message ); + } elseif ( self::$errorHandlingFUSE === self::C_ERR_FUNC ) { + $func = self::$errorHandler; + return $func(array( + 'message' => $message, + 'method' => $method, + 'args' => $args, + 'bean' => $this + )); + } + trigger_error( $message, E_USER_ERROR ); + return NULL; + } + + return call_user_func_array( array( $this->__info['model'], $method ), $args ); + } + + /** + * Implementation of __toString Method + * Routes call to Model. If the model implements a __toString() method this + * method will be called and the result will be returned. In case of an + * echo-statement this result will be printed. If the model does not + * implement a __toString method, this method will return a JSON + * representation of the current bean. + * + * @return string + */ + public function __toString() + { + $string = $this->__call( '@__toString', array() ); + + if ( $string === NULL ) { + $list = array(); + foreach($this->properties as $property => $value) { + if (is_scalar($value)) { + if ( self::$enforceUTF8encoding ) { + $list[$property] = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + } else { + $list[$property] = $value; + } + } + } + $data = json_encode( $list ); + return $data; + } else { + return $string; + } + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Call gets routed to __set. + * + * @param mixed $offset offset string + * @param mixed $value value + * + * @return void + */ + public function offsetSet( $offset, $value ) + { + $this->__set( $offset, $value ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return boolean + */ + public function offsetExists( $offset ) + { + return $this->__isset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Unsets a value from the array/bean. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return void + */ + public function offsetUnset( $offset ) + { + $this->__unset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Returns value of a property. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return mixed + */ + public function &offsetGet( $offset ) + { + return $this->__get( $offset ); + } + + /** + * Chainable method to cast a certain ID to a bean; for instance: + * $person = $club->fetchAs('person')->member; + * This will load a bean of type person using member_id as ID. + * + * @param string $type preferred fetch type + * + * @return OODBBean + */ + public function fetchAs( $type ) + { + $this->fetchType = $type; + + return $this; + } + + /** + * Prepares to load a bean using the bean type specified by + * another property. + * Similar to fetchAs but uses a column instead of a direct value. + * + * Usage: + * + * + * $car = R::load( 'car', $id ); + * $engine = $car->poly('partType')->part; + * + * + * In the example above, we have a bean of type car that + * may consists of several parts (i.e. chassis, wheels). + * To obtain the 'engine' we access the property 'part' + * using the type (i.e. engine) specified by the property + * indicated by the argument of poly(). + * This essentially is a polymorph relation, hence the name. + * In database this relation might look like this: + * + * partType | part_id + * -------------------- + * engine | 1020300 + * wheel | 4820088 + * chassis | 7823122 + * + * @param string $field field name to use for mapping + * + * @return OODBBean + */ + public function poly( $field ) + { + return $this->fetchAs( $this->$field ); + } + + /** + * Traverses a bean property with the specified function. + * Recursively iterates through the property invoking the + * function for each bean along the way passing the bean to it. + * + * Can be used together with with, withCondition, alias and fetchAs. + * + * + * $task + * ->withCondition(' priority >= ? ', [ $priority ]) + * ->traverse('ownTaskList', function( $t ) use ( &$todo ) { + * $todo[] = $t->descr; + * } ); + * + * + * In the example, we create a to-do list by traversing a + * hierarchical list of tasks while filtering out all tasks + * having a low priority. + * + * @param string $property property + * @param callable $function function + * @param integer $maxDepth maximum depth for traversal + * + * @return OODBBean + * @throws RedException + */ + public function traverse( $property, $function, $maxDepth = NULL, $depth = 1 ) + { + $this->via = NULL; + if ( strpos( $property, 'shared' ) !== FALSE ) { + throw new RedException( 'Traverse only works with (x)own-lists.' ); + } + + if ( !is_null( $maxDepth ) ) { + if ( !$maxDepth-- ) return $this; + } + + $oldFetchType = $this->fetchType; + $oldAliasName = $this->aliasName; + $oldWith = $this->withSql; + $oldBindings = $this->withParams; + + $beans = $this->$property; + + if ( $beans === NULL ) return $this; + + if ( !is_array( $beans ) ) $beans = array( $beans ); + + foreach( $beans as $bean ) { + $function( $bean, $depth ); + $bean->fetchType = $oldFetchType; + $bean->aliasName = $oldAliasName; + $bean->withSql = $oldWith; + $bean->withParams = $oldBindings; + + $bean->traverse( $property, $function, $maxDepth, $depth + 1 ); + } + + return $this; + } + + /** + * Implementation of Countable interface. Makes it possible to use + * count() function on a bean. This method gets invoked if you use + * the count() function on a bean. The count() method will return + * the number of properties of the bean, this includes the id property. + * + * Usage: + * + * + * $bean = R::dispense('bean'); + * $bean->property1 = 1; + * $bean->property2 = 2; + * echo count($bean); //prints 3 (cause id is also a property) + * + * + * The example above will print the number 3 to stdout. + * Although we have assigned values to just two properties, the + * primary key id is also a property of the bean and together + * that makes 3. Besides using the count() function, you can also + * call this method using a method notation: $bean->count(). + * + * @return integer + */ + public function count() + { + return count( $this->properties ); + } + + /** + * Checks whether a bean is empty or not. + * A bean is empty if it has no other properties than the id field OR + * if all the other properties are 'empty()' (this might + * include NULL and FALSE values). + * + * Usage: + * + * + * $newBean = R::dispense( 'bean' ); + * $newBean->isEmpty(); // TRUE + * + * + * The example above demonstrates that newly dispensed beans are + * considered 'empty'. + * + * @return boolean + */ + public function isEmpty() + { + $empty = TRUE; + foreach ( $this->properties as $key => $value ) { + if ( $key == 'id' ) { + continue; + } + if ( !empty( $value ) ) { + $empty = FALSE; + } + } + + return $empty; + } + + /** + * Chainable setter. + * This method is actually the same as just setting a value + * using a magic setter (->property = ...). The difference + * is that you can chain these setters like this: + * + * Usage: + * + * + * $book->setAttr('title', 'mybook')->setAttr('author', 'me'); + * + * + * This is the same as setting both properties $book->title and + * $book->author. Sometimes a chained notation can improve the + * readability of the code. + * + * @param string $property the property of the bean + * @param mixed $value the value you want to set + * + * @return OODBBean + */ + public function setAttr( $property, $value ) + { + $this->$property = $value; + + return $this; + } + + /** + * Convience method. + * Unsets all properties in the internal properties array. + * + * Usage: + * + * + * $bean->property = 1; + * $bean->unsetAll( array( 'property' ) ); + * $bean->property; //NULL + * + * + * In the example above the 'property' of the bean will be + * unset, resulting in the getter returning NULL instead of 1. + * + * @param array $properties properties you want to unset. + * + * @return OODBBean + */ + public function unsetAll( $properties ) + { + foreach ( $properties as $prop ) { + if ( isset( $this->properties[$prop] ) ) { + unset( $this->properties[$prop] ); + } + } + return $this; + } + + /** + * Returns original (old) value of a property. + * You can use this method to see what has changed in a + * bean. The original value of a property is the value that + * this property has had since the bean has been retrieved + * from the databases. + * + * + * $book->title = 'new title'; + * $oldTitle = $book->old('title'); + * + * + * The example shows how to use the old() method. + * Here we set the title property of the bean to 'new title', then + * we obtain the original value using old('title') and store it in + * a variable $oldTitle. + * + * @param string $property name of the property you want the old value of + * + * @return mixed + */ + public function old( $property ) + { + $old = $this->getMeta( 'sys.orig', array() ); + + if ( array_key_exists( $property, $old ) ) { + return $old[$property]; + } + + return NULL; + } + + /** + * Convenience method. + * + * Returns TRUE if the bean has been changed, or FALSE otherwise. + * Same as $bean->getMeta('tainted'); + * Note that a bean becomes tainted as soon as you retrieve a list from + * the bean. This is because the bean lists are arrays and the bean cannot + * determine whether you have made modifications to a list so RedBeanPHP + * will mark the whole bean as tainted. + * + * @return boolean + */ + public function isTainted() + { + return $this->getMeta( 'tainted' ); + } + + /** + * Returns TRUE if the value of a certain property of the bean has been changed and + * FALSE otherwise. + * + * Note that this method will return TRUE if applied to a loaded list. + * Also note that this method keeps track of the bean's history regardless whether + * it has been stored or not. Storing a bean does not undo it's history, + * to clean the history of a bean use: clearHistory(). + * + * @param string $property name of the property you want the change-status of + * + * @return boolean + */ + public function hasChanged( $property ) + { + return ( array_key_exists( $property, $this->properties ) ) ? + $this->old( $property ) != $this->properties[$property] : FALSE; + } + + /** + * Returns TRUE if the specified list exists, has been loaded + * and has been changed: + * beans have been added or deleted. + * This method will not tell you anything about + * the state of the beans in the list. + * + * Usage: + * + * + * $book->hasListChanged( 'ownPage' ); // FALSE + * array_pop( $book->ownPageList ); + * $book->hasListChanged( 'ownPage' ); // TRUE + * + * + * In the example, the first time we ask whether the + * own-page list has been changed we get FALSE. Then we pop + * a page from the list and the hasListChanged() method returns TRUE. + * + * @param string $property name of the list to check + * + * @return boolean + */ + public function hasListChanged( $property ) + { + if ( !array_key_exists( $property, $this->properties ) ) return FALSE; + $diffAdded = array_diff_assoc( $this->properties[$property], $this->__info['sys.shadow.'.$property] ); + if ( count( $diffAdded ) ) return TRUE; + $diffMissing = array_diff_assoc( $this->__info['sys.shadow.'.$property], $this->properties[$property] ); + if ( count( $diffMissing ) ) return TRUE; + return FALSE; + } + + /** + * Clears (syncs) the history of the bean. + * Resets all shadow values of the bean to their current value. + * + * Usage: + * + * + * $book->title = 'book'; + * echo $book->hasChanged( 'title' ); //TRUE + * R::store( $book ); + * echo $book->hasChanged( 'title' ); //TRUE + * $book->clearHistory(); + * echo $book->hasChanged( 'title' ); //FALSE + * + * + * Note that even after store(), the history of the bean still + * contains the act of changing the title of the book. + * Only after invoking clearHistory() will the history of the bean + * be cleared and will hasChanged() return FALSE. + * + * @return self + */ + public function clearHistory() + { + $this->__info['sys.orig'] = array(); + foreach( $this->properties as $key => $value ) { + if ( is_scalar($value) ) { + $this->__info['sys.orig'][$key] = $value; + } else { + $this->__info['sys.shadow.'.$key] = $value; + } + } + $this->__info[ 'changelist' ] = array(); + return $this; + } + + /** + * Creates a N-M relation by linking an intermediate bean. + * This method can be used to quickly connect beans using indirect + * relations. For instance, given an album and a song you can connect the two + * using a track with a number like this: + * + * Usage: + * + * + * $album->link('track', array('number'=>1))->song = $song; + * + * + * or: + * + * + * $album->link($trackBean)->song = $song; + * + * + * What this method does is adding the link bean to the own-list, in this case + * ownTrack. If the first argument is a string and the second is an array or + * a JSON string then the linking bean gets dispensed on-the-fly as seen in + * example #1. After preparing the linking bean, the bean is returned thus + * allowing the chained setter: ->song = $song. + * + * @param string|OODBBean $typeOrBean type of bean to dispense or the full bean + * @param string|array $qualification JSON string or array (optional) + * + * @return OODBBean + */ + public function link( $typeOrBean, $qualification = array() ) + { + if ( is_string( $typeOrBean ) ) { + $typeOrBean = AQueryWriter::camelsSnake( $typeOrBean ); + $bean = $this->beanHelper->getToolBox()->getRedBean()->dispense( $typeOrBean ); + if ( is_string( $qualification ) ) { + $data = json_decode( $qualification, TRUE ); + } else { + $data = $qualification; + } + foreach ( $data as $key => $value ) { + $bean->$key = $value; + } + } else { + $bean = $typeOrBean; + } + $list = 'own' . ucfirst( $bean->getMeta( 'type' ) ); + array_push( $this->$list, $bean ); + return $bean; + } + + /** + * Returns a bean of the given type with the same ID of as + * the current one. This only happens in a one-to-one relation. + * This is as far as support for 1-1 goes in RedBeanPHP. This + * method will only return a reference to the bean, changing it + * and storing the bean will not update the related one-bean. + * + * Usage: + * + * + * $author = R::load( 'author', $id ); + * $biography = $author->one( 'bio' ); + * + * + * The example loads the biography associated with the author + * using a one-to-one relation. These relations are generally not + * created (nor supported) by RedBeanPHP. + * + * @param $type type of bean to load + * + * @return OODBBean + */ + public function one( $type ) { + return $this->beanHelper + ->getToolBox() + ->getRedBean() + ->load( $type, $this->id ); + } + + /** + * Reloads the bean. + * Returns the same bean freshly loaded from the database. + * This method is equal to the following code: + * + * + * $id = $bean->id; + * $type = $bean->getMeta( 'type' ); + * $bean = R::load( $type, $id ); + * + * + * This is just a convenience method to reload beans + * quickly. + * + * Usage: + * + * + * R::exec( ...update query... ); + * $book = $book->fresh(); + * + * + * The code snippet above illustrates how to obtain changes + * caused by an UPDATE query, simply by reloading the bean using + * the fresh() method. + * + * @return OODBBean + */ + public function fresh() + { + return $this->beanHelper + ->getToolbox() + ->getRedBean() + ->load( $this->getMeta( 'type' ), $this->properties['id'] ); + } + + /** + * Registers a association renaming globally. + * Use via() and link() to associate shared beans using a + * 3rd bean that will act as an intermediate type. For instance + * consider an employee and a project. We could associate employees + * with projects using a sharedEmployeeList. But, maybe there is more + * to the relationship than just the association. Maybe we want + * to qualify the relation between a project and an employee with + * a role: 'developer', 'designer', 'tester' and so on. In that case, + * it might be better to introduce a new concept to reflect this: + * the participant. However, we still want the flexibility to + * query our employees in one go. This is where link() and via() + * can help. You can still introduce the more applicable + * concept (participant) and have your easy access to the shared beans. + * + * + * $Anna = R::dispense( 'employee' ); + * $Anna->badge = 'Anna'; + * $project = R::dispense( 'project' ); + * $project->name = 'x'; + * $Anna->link( 'participant', array( + * 'arole' => 'developer' + * ) )->project = $project; + * R::storeAll( array( $project, $Anna ) ); + * $employees = $project + * ->with(' ORDER BY badge ASC ') + * ->via( 'participant' ) + * ->sharedEmployee; + * + * + * This piece of code creates a project and an employee. + * It then associates the two using a via-relation called + * 'participant' ( employee <-> participant <-> project ). + * So, there will be a table named 'participant' instead of + * a table named 'employee_project'. Using the via() method, the + * employees associated with the project are retrieved 'via' + * the participant table (and an SQL snippet to order them by badge). + * + * @param string $via type you wish to use for shared lists + * + * @return OODBBean + */ + public function via( $via ) + { + $this->via = AQueryWriter::camelsSnake( $via ); + + return $this; + } + + /** + * Counts all own beans of type $type. + * Also works with alias(), with() and withCondition(). + * Own-beans or xOwn-beans (exclusively owned beans) are beans + * that have been associated using a one-to-many relation. They can + * be accessed through the ownXList where X is the type of the + * associated beans. + * + * Usage: + * + * + * $Bill->alias( 'author' ) + * ->countOwn( 'book' ); + * + * + * The example above counts all the books associated with 'author' + * $Bill. + * + * @param string $type the type of bean you want to count + * + * @return integer + */ + public function countOwn( $type ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $myFieldLink = $this->aliasName . '_id'; + $this->aliasName = NULL; + } else { + $myFieldLink = $this->__info['type'] . '_id'; + } + $count = 0; + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + if ( !self::$useFluidCount ) { + $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), $sql, $bindings ); + } else { + $count = $this->beanHelper->getToolbox()->getRedBean()->count( $type, $sql, $bindings ); + } + } + $this->clearModifiers(); + return (int) $count; + } + + /** + * Counts all shared beans of type $type. + * Also works with via(), with() and withCondition(). + * Shared beans are beans that have an many-to-many relation. + * They can be accessed using the sharedXList, where X the + * type of the shared bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->sharedPageList = R::dispense( 'page', 5 ); + * R::store( $book ); + * echo $book->countShared( 'page' ); + * + * + * The code snippet above will output '5', because there + * are 5 beans of type 'page' in the shared list. + * + * @param string $type type of bean you wish to count + * + * @return integer + */ + public function countShared( $type ) + { + $toolbox = $this->beanHelper->getToolbox(); + $redbean = $toolbox->getRedBean(); + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + $this->via = NULL; + } + } + $type = $this->beau( $type ); + $count = 0; + if ( $this->getID() ) { + $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams ); + } + $this->clearModifiers(); + return (integer) $count; + } + + /** + * Iterates through the specified own-list and + * fetches all properties (with their type) and + * returns the references. + * Use this method to quickly load indirectly related + * beans in an own-list. Whenever you cannot use a + * shared-list this method offers the same convenience + * by aggregating the parent beans of all children in + * the specified own-list. + * + * Example: + * + * + * $quest->aggr( 'xownQuestTarget', 'target', 'quest' ); + * + * + * Loads (in batch) and returns references to all + * quest beans residing in the $questTarget->target properties + * of each element in the xownQuestTargetList. + * + * @param string $list the list you wish to process + * @param string $property the property to load + * @param string $type the type of bean residing in this property (optional) + * + * @return array + */ + public function &aggr( $list, $property, $type = NULL ) + { + $this->via = NULL; + $ids = $beanIndex = $references = array(); + + if ( strlen( $list ) < 4 ) throw new RedException('Invalid own-list.'); + if ( strpos( $list, 'own') !== 0 ) throw new RedException('Only own-lists can be aggregated.'); + if ( !ctype_upper( substr( $list, 3, 1 ) ) ) throw new RedException('Invalid own-list.'); + + if ( is_null( $type ) ) $type = $property; + + foreach( $this->$list as $bean ) { + $field = $property . '_id'; + if ( isset( $bean->$field) ) { + $ids[] = $bean->$field; + $beanIndex[$bean->$field] = $bean; + } + } + + $beans = $this->beanHelper->getToolBox()->getRedBean()->batch( $type, $ids ); + + //now preload the beans as well + foreach( $beans as $bean ) { + $beanIndex[$bean->id]->setProperty( $property, $bean ); + } + + foreach( $beanIndex as $indexedBean ) { + $references[] = $indexedBean->$property; + } + + return $references; + } + + /** + * Tests whether the database identities of two beans are equal. + * Two beans are considered 'equal' if: + * + * a. the types of the beans match + * b. the ids of the beans match + * + * Returns TRUE if the beans are considered equal according to this + * specification and FALSE otherwise. + * + * Usage: + * + * + * $coffee->fetchAs( 'flavour' )->taste->equals( + * R::enum('flavour:mocca') + * ); + * + * + * The example above compares the flavour label 'mocca' with + * the flavour label attachec to the $coffee bean. This illustrates + * how to use equals() with RedBeanPHP-style enums. + * + * @param OODBBean $bean other bean + * + * @return boolean + */ + public function equals(OODBBean $bean) + { + return (bool) ( + ( (string) $this->properties['id'] === (string) $bean->properties['id'] ) + && ( (string) $this->__info['type'] === (string) $bean->__info['type'] ) + ); + } + + /** + * Magic method jsonSerialize, + * implementation for the \JsonSerializable interface, + * this method gets called by json_encode and + * facilitates a better JSON representation + * of the bean. Exports the bean on JSON serialization, + * for the JSON fans. + * + * Models can override jsonSerialize (issue #651) by + * implementing a __jsonSerialize method which should return + * an array. The __jsonSerialize override gets called with + * the @ modifier to prevent errors or warnings. + * + * @see http://php.net/manual/en/class.jsonserializable.php + * + * @return array + */ + public function jsonSerialize() + { + $json = $this->__call( '@__jsonSerialize', array( ) ); + + if ( $json !== NULL ) { + return $json; + } + + return $this->export(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Observable.php b/vendor/gabordemooij/redbean/RedBeanPHP/Observable.php new file mode 100644 index 0000000..569c4d4 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Observable.php @@ -0,0 +1,73 @@ +observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + if ( in_array( $observer, $this->observers[$eventname] ) ) { + return; + } + + $this->observers[$eventname][] = $observer; + } + + /** + * Notifies listeners. + * Sends the signal $eventname, the event identifier and a message object + * to all observers that have been registered to receive notification for + * this event. Part of the observer pattern implementation in RedBeanPHP. + * + * @param string $eventname event you want signal + * @param mixed $info message object to send along + * + * @return void + */ + public function signal( $eventname, $info ) + { + if ( !isset( $this->observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + foreach ( $this->observers[$eventname] as $observer ) { + $observer->onEvent( $eventname, $info ); + } + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Observer.php b/vendor/gabordemooij/redbean/RedBeanPHP/Observer.php new file mode 100644 index 0000000..29a0700 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Observer.php @@ -0,0 +1,35 @@ +toolbox; + return array( $toolbox->getRedbean(), $toolbox->getDatabaseAdapter(), $toolbox->getWriter(), $toolbox ); + } + + /** + * Constructor + * + * Creates a new instance of the NonStaticBeanHelper. + * The NonStaticBeanHelper is used by the database pool class PoolDB. + */ + public function __construct($toolbox) + { + $this->toolbox = $toolbox; + } +} + +/** + * PoolDB + * + * Represents a pool of databases that will have persisting + * associations with the beans they dispense. Saving a bean from + * a pooled database will make sure that the bean will be stored + * in the database it originated from instead of the currently + * selected database. + * + * @experimental + * This is an experimental plugin, added for testing purposes + * only. + * + * Usage: + * + * + * // Let's add some databases + * R::addPoolDatabase( 'db1', 'sqlite:/tmp/db1.txt' ); + * R::nuke(); + * R::addPoolDatabase( 'db2', 'sqlite:/tmp/db2.txt' ); + * R::nuke(); + * R::addPoolDatabase( 'db3', 'sqlite:/tmp/db3.txt' ); + * R::nuke(); + * + * // create a book and page in db1 + * R::selectDatabase('db1'); + * $book = R::dispense(array( + * '_type' => 'book', + * 'title' => 'Databases for Beans', + * 'ownPageList' => array( + * 0 => array( + * '_type' => 'page', + * 'content' => 'Lorem Ipsum' + * ) + * ) + * )); + * R::store($book); + * + * //switch to db2 + * R::selectDatabase( 'db2' ); + * //obtain pages (from db1) + * $pages = count($book->ownPageList); + * echo "I found {$pages} pages in db1.\n"; + * + * $pages = R::count('page'); + * echo "There are {$pages} pages in db2.\n"; + * + * // create pizza in db 2 + * $pizza = R::dispense('pizza'); + * + * // switch to db3 + * R::selectDatabase( 'db3' ); + * + * // store pizza in db2 + * $pizza->pepperoni = true; + * R::store($pizza); + * + * $pizzas = R::count('pizza'); + * echo "There are {$pizzas} in pizzas db3.\n"; + * R::selectDatabase('db2'); + * $pizzas = R::count('pizza'); + * echo "There are {$pizzas} pizzas in db2.\n"; + * + * + * @file RedBeanPHP/Plugin/Pool.php + * @author RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class PoolDB extends \RedBeanPHP\OODB { + + /** + * @var array + */ + private static $pool = array(); + + /** + * @var ToolBox + */ + private $toolbox; + + /** + * @var OODB + */ + private $oodb; + + /** + * @var string + */ + private $key; + + /** + * @var NonStaticBeanHelper + */ + private $beanHelper; + + /** + * Constructor + * + * creates a new instance of the database pool. + * + * @param string $key key + * @param OODB $oodb oodb instance + */ + public function __construct( $key, $oodb) + { + self::$pool[$key] = $oodb; + $this->oodb = $oodb; + $this->key = $key; + parent::__construct( $oodb->writer, $oodb->isFrozen ); + } + + /** + * Sets the toolbox to be used by the database pool. + * + * @param ToolBox $toolbox toolbox + * + * @return void + */ + public function setToolBox( $toolbox ) + { + $this->toolbox = $toolbox; + $this->beanHelper = new NonStaticBeanHelper( $this->toolbox ); + $this->beanHelper->key = $this->key; + $this->oodb->setBeanHelper( $this->beanHelper ); + } + + /** + * Returns the bean helper of the database pool. + * + * @return BeanHelper + */ + public function getBeanHelper() + { + return $this->beanHelper; + } + + /** + * Implements the find operation. + * + * @see OODB::find + */ + public function find( $type, $conditions=array(), $sql=NULL, $bindings=array()) + { + return parent::find($type, $conditions, $sql, $bindings); + } + + /** + * Dispenses a new bean from the database pool. + * A bean that has been dispensed by the pool will have a special + * meta attribute called sys.source containing the key identifying + * the database in the pool it originated from. + * + * @see OODB::dispense + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + $bean = $this->oodb->dispense( $type, $number, $alwaysReturnArray ); + foreach( self::$pool as $key => $db ) { + if ( $this->oodb === $db ) { + $bean->setMeta( 'sys.source',$key ); + } + } + return $bean; + } + + /** + * Stores the specified bean in the database in the pool + * it originated from by looking up the sys.source attribute. + * + * @see OODB::store + */ + public function store( $bean ) + { + $dataSource = $bean->getMeta('sys.source'); + if ( !is_null( $dataSource ) ) { + $result = self::$pool[$dataSource]->store( $bean ); + } else { + $result = parent::store( $bean ); + } + return $result; + } +} + + +R::ext( 'addPoolDatabase', function( $dbName, $dsn, $user=NULL, $pass=NULL ) { + R::addDatabase( $dbName, $dsn, $user, $pass ); + R::selectDatabase( $dbName ); + list($oodb, $adapter, $writer, ) = R::getExtractedToolbox(); + $poolDB = new PoolDB( $dbName, $oodb ); + $toolbox = new ToolBox( $poolDB, $adapter, $writer ); + $poolDB->setToolBox( $toolbox ); + R::$toolboxes[$dbName]=$toolbox; + R::selectDatabase( $dbName, TRUE ); +} ); + diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/SQN.php b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/SQN.php new file mode 100644 index 0000000..4244830 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/SQN.php @@ -0,0 +1,86 @@ + 'INNER JOIN', + '||' => 'INNER JOIN', + '>' => 'RIGHT JOIN', + '>>' => 'RIGHT JOIN', + '<' => 'LEFT JOIN', + '<<' => 'LEFT JOIN', + ]; + $select = []; + $from = ''; + $joins = []; + $prev = ''; + $ents = preg_split( '/[^\w_]+/', $query ); + $tokens = preg_split( '/[\w_]+/', $query ); + array_pop($tokens); + foreach( $ents as $i => $ent ) { + $select[] = " {$q}{$ent}{$q}.* "; + if (!$i) { + $from = $ent; + $prev = $ent; + continue; + } + if ( $tokens[$i] == '<' || $tokens[$i] == '>' || $tokens[$i] == '|') { + $join[] = " {$map[$tokens[$i]]} {$q}{$ent}{$q} ON {$q}{$ent}{$q}.{$prev}_id = {$q}{$prev}{$q}.id "; + } + if ( $tokens[$i] == '<<' || $tokens[$i] == '>>' || $tokens[$i] == '||') { + $combi = [$prev, $ent]; + sort( $combi ); + $combi = implode( '_', $combi ); + $select[] = " {$q}{$combi}{$q}.* "; + $join[] = " {$map[$tokens[$i]]} {$q}{$combi}{$q} ON {$q}{$combi}{$q}.{$prev}_id = {$q}{$prev}{$q}.id "; + $join[] = " {$map[$tokens[$i]]} {$q}{$ent}{$q} ON {$q}{$combi}{$q}.{$ent}_id = {$q}{$ent}{$q}.id "; + } + $prev = $ent; + } + if (!is_null($aliases)) { + $aliases = explode(';', $aliases); + foreach($aliases as $alias) { + list($table, $cols) = explode('/', $alias); + $cols = explode(',', $cols); + foreach($cols as $col) { + $select[] = " {$q}{$table}{$q}.{$q}{$col}{$q} AS {$q}{$table}_{$col}{$q} "; + } + } + } + $selectSQL = implode( ",\n", $select ); + $joinSQL = implode( "\n", $join ); + return "SELECT{$selectSQL}\n FROM {$q}{$from}{$q}\n{$joinSQL}"; +}); + diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/TinyQueryBuilder.php b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/TinyQueryBuilder.php new file mode 100644 index 0000000..49d580e --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/TinyQueryBuilder.php @@ -0,0 +1,56 @@ + + * $sql = build_query([ + * [ 'SELECT * FROM book'], + * [$title ,'WHERE','title = ?'], + * [$price ,'AND','price < ?'], + * [$order ,'ORDER BY ? ASC'], + * [$limit ,'LIMIT ?'] + * ]); + * + * + * Now, if we have a $title and a $price the query will be: + * 'SELECT * FROM book WHERE title = ? AND price < ? ' + * If we only have a $price and a $limit: + * 'SELECT * FROM book WHERE price < ? LIMIT ?' + * The Query Builder works very easy, it simply loops through the array, + * each element is another array inside this main array, + * let's call this inner array a 'piece'. + * A piece can have one, two or three elements. + * If it has one element, the element is simply concatenated to the final query. + * If a piece has two elements, the second element will be + * concatenated only if the first evaluates to TRUE. + * Finally a piece having three elements works the same as a piece with two elements, + * except that it will use the glue provided in the second element + * to concat the value of the third element. The glue acts as a little tube of glue. + * If there is still glue left in the tube (WHERE) it will preserve this + * until it can be applied (so the first AND will be ignored in case of a WHERE condition). + */ +R::ext('buildQuery', function($pieces) { + $sql = ''; + $glue = NULL; + foreach( $pieces as $piece ) { + $n = count( $piece ); + switch( $n ) { + case 1: + $sql .= " {$piece[0]} "; + break; + case 2: + $glue = NULL; + if (!is_null($piece[0])) $sql .= " {$piece[1]} "; + break; + case 3: + $glue = ( is_null( $glue ) ) ? $piece[1] : $glue; + if (!is_null($piece[0])) { + $sql .= " {$glue} {$piece[2]} "; + $glue = NULL; + } + break; + } + } + return $sql; +}); diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/put_your_plugins_here.txt b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/put_your_plugins_here.txt new file mode 100644 index 0000000..bb5ecef --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Plugin/put_your_plugins_here.txt @@ -0,0 +1,2 @@ +This folder has been reserved for your plugins + diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter.php new file mode 100644 index 0000000..9c4c6db --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter.php @@ -0,0 +1,559 @@ + $type where $field is the name of the column and $type + * is a database specific description of the datatype. + * + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type of bean you want to obtain a column list of + * + * @return array + */ + public function getColumns( $type ); + + /** + * Returns the Column Type Code (integer) that corresponds + * to the given value type. This method is used to determine the minimum + * column type required to represent the given value. There are two modes of + * operation: with or without special types. Scanning without special types + * requires the second parameter to be set to FALSE. This is useful when the + * column has already been created and prevents it from being modified to + * an incompatible type leading to data loss. Special types will be taken + * into account when a column does not exist yet (parameter is then set to TRUE). + * + * Special column types are determines by the AQueryWriter constant + * C_DATA_TYPE_ONLY_IF_NOT_EXISTS (usually 80). Another 'very special' type is type + * C_DATA_TYPE_MANUAL (usually 99) which represents a user specified type. Although + * no special treatment has been associated with the latter for now. + * + * @param string $value value + * @param boolean $alsoScanSpecialForTypes take special types into account + * + * @return integer + */ + public function scanType( $value, $alsoScanSpecialForTypes = FALSE ); + + /** + * This method will add a column to a table. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table + * @param string $column name of the column + * @param integer $field data type for field + * + * @return void + */ + public function addColumn( $type, $column, $field ); + + /** + * Returns the Type Code for a Column Description. + * Given an SQL column description this method will return the corresponding + * code for the writer. If the include specials flag is set it will also + * return codes for special columns. Otherwise special columns will be identified + * as specified columns. + * + * @param string $typedescription description + * @param boolean $includeSpecials whether you want to get codes for special columns as well + * + * @return integer + */ + public function code( $typedescription, $includeSpecials = FALSE ); + + /** + * This method will widen the column to the specified data type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type / table that needs to be adjusted + * @param string $column column that needs to be altered + * @param integer $datatype target data type + * + * @return void + */ + public function widenColumn( $type, $column, $datatype ); + + /** + * Selects records from the database. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return array + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Selects records from the database and returns a cursor. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return Cursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ); + + /** + * Returns records through an intermediate type. This method is used to obtain records using a link table and + * allows the SQL snippets to reference columns in the link table for additional filtering or ordering. + * + * @param string $sourceType source type, the reference type you want to use to fetch related items on the other side + * @param string $destType destination type, the target type you want to get beans of + * @param mixed $linkID ID to use for the link table + * @param string $addSql Additional SQL snippet + * @param array $bindings Bindings for SQL snippet + * + * @return array + */ + public function queryRecordRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ); + + /** + * Returns the row that links $sourceType $sourcID to $destType $destID in an N-M relation. + * + * @param string $sourceType source type, the first part of the link you're looking for + * @param string $destType destination type, the second part of the link you're looking for + * @param string $sourceID ID for the source + * @param string $destID ID for the destination + * + * @return array|null + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ); + + /** + * Counts the number of records in the database that match the + * conditions and additional SQL. + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Returns the number of records linked through $linkType and satisfying the SQL in $addSQL/$bindings. + * + * @param string $sourceType source type + * @param string $targetType the thing you want to count + * @param mixed $linkID the of the source type + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCountRelated( $sourceType, $targetType, $linkID, $addSQL = '', $bindings = array() ); + + /** + * Returns all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return array + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Like queryTagged but only counts. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return integer + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Returns all parent rows or child rows of a specified row. + * Given a type specifier and a primary key id, this method returns either all child rows + * as defined by having _id = id or all parent rows as defined per id = _id + * taking into account an optional SQL snippet along with parameters. + * + * The $select parameter can be used to adjust the select snippet of the query. + * Possible values are: C_CTE_SELECT_NORMAL (just select all columns, default), C_CTE_SELECT_COUNT + * (count rows) used for countParents and countChildren functions - or you can specify a + * string yourself like 'count(distinct brand)'. + * + * @param string $type the bean type you want to query rows for + * @param integer $id id of the reference row + * @param boolean $up TRUE to query parent rows, FALSE to query child rows + * @param string $addSql optional SQL snippet to embed in the query + * @param array $bindings parameter bindings for additional SQL snippet + * @param mixed $select Select Snippet to use when querying (optional) + * + * @return array + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_NORMAL ); + + /** + * This method should update (or insert a record), it takes + * a table name, a list of update values ( $field => $value ) and an + * primary key ID (optional). If no primary key ID is provided, an + * INSERT will take place. + * Returns the new ID. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table to update + * @param array $updatevalues list of update values + * @param integer $id optional primary key ID value + * + * @return integer + */ + public function updateRecord( $type, $updatevalues, $id = NULL ); + + /** + * Deletes records from the database. + * @note $addSql is always prefixed with ' WHERE ' or ' AND .' + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL + * @param array $bindings bindings + * + * @return void + */ + public function deleteRecord( $type, $conditions = array(), $addSql = '', $bindings = array() ); + + /** + * Deletes all links between $sourceType and $destType in an N-M relation. + * + * @param string $sourceType source type + * @param string $destType destination type + * @param string $sourceID source ID + * + * @return void + */ + public function deleteRelations( $sourceType, $destType, $sourceID ); + + /** + * @see QueryWriter::addUniqueConstaint + */ + public function addUniqueIndex( $type, $columns ); + + /** + * This method will add a UNIQUE constraint index to a table on columns $columns. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type target bean type + * @param array $columnsPartOfIndex columns to include in index + * + * @return void + */ + public function addUniqueConstraint( $type, $columns ); + + /** + * This method will check whether the SQL state is in the list of specified states + * and returns TRUE if it does appear in this list or FALSE if it + * does not. The purpose of this method is to translate the database specific state to + * a one of the constants defined in this class and then check whether it is in the list + * of standard states provided. + * + * @param string $state SQL state to consider + * @param array $list list of standardized SQL state constants to check against + * @param array $extraDriverDetails Some databases communicate state information in a driver-specific format + * rather than through the main sqlState code. For those databases, this extra + * information can be used to determine the standardized state + * + * @return boolean + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ); + + /** + * This method will remove all beans of a certain type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type bean type + * + * @return void + */ + public function wipe( $type ); + + /** + * This method will add a foreign key from type and field to + * target type and target field. + * The foreign key is created without an action. On delete/update + * no action will be triggered. The FK is only used to allow database + * tools to generate pretty diagrams and to make it easy to add actions + * later on. + * This methods accepts a type and infers the corresponding table name. + * + * + * @param string $type type that will have a foreign key field + * @param string $targetType points to this type + * @param string $property field that contains the foreign key value + * @param string $targetProperty field where the fk points to + * @param string $isDep whether target is dependent and should cascade on update/delete + * + * @return void + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ); + + /** + * This method will add an index to a type and field with name + * $name. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type to add index to + * @param string $name name of the new index + * @param string $property field to index + * + * @return void + */ + public function addIndex( $type, $name, $property ); + + /** + * Checks and filters a database structure element like a table of column + * for safe use in a query. A database structure has to conform to the + * RedBeanPHP DB security policy which basically means only alphanumeric + * symbols are allowed. This security policy is more strict than conventional + * SQL policies and does therefore not require database specific escaping rules. + * + * @param string $databaseStructure name of the column/table to check + * @param boolean $noQuotes TRUE to NOT put backticks or quotes around the string + * + * @return string + */ + public function esc( $databaseStructure, $dontQuote = FALSE ); + + /** + * Removes all tables and views from the database. + * + * @return void + */ + public function wipeAll(); + + /** + * Renames an association. For instance if you would like to refer to + * album_song as: track you can specify this by calling this method like: + * + * + * renameAssociation('album_song','track') + * + * + * This allows: + * + * + * $album->sharedSong + * + * + * to add/retrieve beans from track instead of album_song. + * Also works for exportAll(). + * + * This method also accepts a single associative array as + * its first argument. + * + * @param string|array $fromType original type name, or array + * @param string $toType new type name (only if 1st argument is string) + * + * @return void + */ + public function renameAssocTable( $fromType, $toType = NULL ); + + /** + * Returns the format for link tables. + * Given an array containing two type names this method returns the + * name of the link table to be used to store and retrieve + * association records. For instance, given two types: person and + * project, the corresponding link table might be: 'person_project'. + * + * @param array $types two types array($type1, $type2) + * + * @return string + */ + public function getAssocTable( $types ); + +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/AQueryWriter.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/AQueryWriter.php new file mode 100644 index 0000000..34abe93 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/AQueryWriter.php @@ -0,0 +1,1667 @@ +getDDLTemplate( 'createTable', '*' ); + * $writer->setDDLTemplate( 'createTable', '*', $sql . ' ROW_FORMAT=DYNAMIC ' ); + * + * For property-specific templates set $beanType to: + * account.username -- then the template will only be applied to SQL statements relating + * to that column/property. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $template SQL template, contains %s for slots + * + * @return void + */ + public function setDDLTemplate( $type, $beanType, $template ) + { + $this->DDLTemplates[ $type ][ $beanType ] = $template; + } + + /** + * Returns the specified data definition template. + * If no template can be found for the specified type, the template for + * '*' will be returned instead. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $property specify if you're looking for a property-specific template + * + * @return string + */ + public function getDDLTemplate( $type, $beanType = '*', $property = NULL ) + { + $key = ( $property ) ? "{$beanType}.{$property}" : $beanType; + if ( isset( $this->DDLTemplates[ $type ][ $key ] ) ) { + return $this->DDLTemplates[ $type ][ $key ]; + } + if ( isset( $this->DDLTemplates[ $type ][ $beanType ] ) ) { + return $this->DDLTemplates[ $type ][ $beanType ]; + } + return $this->DDLTemplates[ $type ][ '*' ]; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $flag ) + { + $old = self::$enableISNULLConditions; + self::$enableISNULLConditions = $flag; + return $old; + } + + /** + * Toggles support for automatic generation of JSON columns. + * Using JSON columns means that strings containing JSON will + * cause the column to be created (not modified) as a JSON column. + * However it might also trigger exceptions if this means the DB attempts to + * convert a non-json column to a JSON column. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useJSONColumns( $flag ) + { + $old = self::$flagUseJSONColumns; + self::$flagUseJSONColumns = $flag; + return $old; + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function forbidNuke( $flag ) { + $old = self::$noNuke; + self::$noNuke = (bool) $flag; + return $old; + } + + /** + * Checks whether a number can be treated like an int. + * + * @param string $value string representation of a certain value + * + * @return boolean + */ + public static function canBeTreatedAsInt( $value ) + { + return (bool) ( strval( $value ) === strval( intval( $value ) ) ); + } + + /** + * @see QueryWriter::getAssocTableFormat + */ + public static function getAssocTableFormat( $types ) + { + sort( $types ); + + $assoc = implode( '_', $types ); + + return ( isset( self::$renames[$assoc] ) ) ? self::$renames[$assoc] : $assoc; + } + + /** + * @see QueryWriter::renameAssociation + */ + public static function renameAssociation( $from, $to = NULL ) + { + if ( is_array( $from ) ) { + foreach ( $from as $key => $value ) self::$renames[$key] = $value; + + return; + } + + self::$renames[$from] = $to; + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a camel cased string to a snake cased string. + * + * @param string $camel camelCased string to converty to snake case + * + * @return string + */ + public static function camelsSnake( $camel ) + { + return strtolower( preg_replace( '/(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])/', '_$1$2', $camel ) ); + } + + /** + * Clears renames. + * + * @return void + */ + public static function clearRenames() + { + self::$renames = array(); + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * SELECT {table}.* + * + * instead of + * + * SELECT * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $narrowField ) + { + self::$flagNarrowFieldMode = (boolean) $narrowField; + } + + /** + * Sets SQL filters. + * This is a lowlevel method to set the SQL filter array. + * The format of this array is: + * + * + * array( + * '' => array( + * '' => array( + * '' => '' + * ) + * ) + * ) + * + * + * Example: + * + * + * array( + * QueryWriter::C_SQLFILTER_READ => array( + * 'book' => array( + * 'title' => ' LOWER(book.title) ' + * ) + * ) + * + * + * Note that you can use constants instead of magical chars + * as keys for the uppermost array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @param array list of filters to set + * + * @return void + */ + public static function setSQLFilters( $sqlFilters, $safeMode = FALSE ) + { + self::$flagSQLFilterSafeMode = (boolean) $safeMode; + self::$sqlFilters = $sqlFilters; + } + + /** + * Returns current SQL Filters. + * This method returns the raw SQL filter array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @return array + */ + public static function getSQLFilters() + { + return self::$sqlFilters; + } + + /** + * Returns a cache key for the cache values passed. + * This method returns a fingerprint string to be used as a key to store + * data in the writer cache. + * + * @param array $keyValues key-value to generate key for + * + * @return string + */ + private function getCacheKey( $keyValues ) + { + return json_encode( $keyValues ); + } + + /** + * Returns the values associated with the provided cache tag and key. + * + * @param string $cacheTag cache tag to use for lookup + * @param string $key key to use for lookup + * + * @return mixed + */ + private function getCached( $cacheTag, $key ) + { + $sql = $this->adapter->getSQL(); + if ($this->updateCache()) { + if ( isset( $this->cache[$cacheTag][$key] ) ) { + return $this->cache[$cacheTag][$key]; + } + } + + return NULL; + } + + /** + * Checks if the previous query had a keep-cache tag. + * If so, the cache will persist, otherwise the cache will be flushed. + * + * Returns TRUE if the cache will remain and FALSE if a flush has + * been performed. + * + * @return boolean + */ + private function updateCache() + { + $sql = $this->adapter->getSQL(); + if ( strpos( $sql, '-- keep-cache' ) !== strlen( $sql ) - 13 ) { + // If SQL has been taken place outside of this method then something else then + // a select query might have happened! (or instruct to keep cache) + $this->cache = array(); + return FALSE; + } + return TRUE; + } + + /** + * Stores data from the writer in the cache under a specific key and cache tag. + * A cache tag is used to make sure the cache remains consistent. In most cases the cache tag + * will be the bean type, this makes sure queries associated with a certain reference type will + * never contain conflicting data. + * Why not use the cache tag as a key? Well + * we need to make sure the cache contents fits the key (and key is based on the cache values). + * Otherwise it would be possible to store two different result sets under the same key (the cache tag). + * + * In previous versions you could only store one key-entry, I have changed this to + * improve caching efficiency (issue #400). + * + * @param string $cacheTag cache tag (secondary key) + * @param string $key key to store values under + * @param array $values content to be stored + * + * @return void + */ + private function putResultInCache( $cacheTag, $key, $values ) + { + if ( isset( $this->cache[$cacheTag] ) ) { + if ( count( $this->cache[$cacheTag] ) > $this->maxCacheSizePerType ) array_shift( $this->cache[$cacheTag] ); + } else { + $this->cache[$cacheTag] = array(); + } + $this->cache[$cacheTag][$key] = $values; + } + + /** + * Creates an SQL snippet from a list of conditions of format: + * + * + * array( + * key => array( + * value1, value2, value3 .... + * ) + * ) + * + * + * @param array $conditions list of conditions + * @param array $bindings parameter bindings for SQL snippet + * @param string $addSql additional SQL snippet to append to result + * + * @return string + */ + private function makeSQLFromConditions( $conditions, &$bindings, $addSql = '' ) + { + reset( $bindings ); + $firstKey = key( $bindings ); + $paramTypeIsNum = ( is_numeric( $firstKey ) ); + $counter = 0; + + $sqlConditions = array(); + foreach ( $conditions as $column => $values ) { + if ( $values === NULL ) { + if ( self::$enableISNULLConditions ) { + $sqlConditions[] = $this->esc( $column ) . ' IS NULL'; + } + continue; + } + + if ( is_array( $values ) ) { + if ( empty( $values ) ) continue; + } else { + $values = array( $values ); + } + + $checkOODB = reset( $values ); + if ( $checkOODB instanceof OODBBean && $checkOODB->getMeta( 'type' ) === $column && substr( $column, -3 ) != '_id' ) + $column = $column . '_id'; + + + $sql = $this->esc( $column ); + $sql .= ' IN ( '; + + if ( $paramTypeIsNum ) { + $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) '; + + array_unshift($sqlConditions, $sql); + + foreach ( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $values[$k] = strval( $v ); + + array_unshift( $bindings, $v ); + } + } else { + + $slots = array(); + + foreach( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $slot = ':slot'.$counter++; + $slots[] = $slot; + $bindings[$slot] = strval( $v ); + } + + $sql .= implode( ',', $slots ).' ) '; + $sqlConditions[] = $sql; + } + } + + $sql = ''; + if ( !empty( $sqlConditions ) ) { + $sql .= " WHERE ( " . implode( ' AND ', $sqlConditions ) . ") "; + } + + $addSql = $this->glueSQLCondition( $addSql, !empty( $sqlConditions ) ? QueryWriter::C_GLUE_AND : NULL ); + if ( $addSql ) $sql .= $addSql; + + return $sql; + } + + /** + * Returns the table names and column names for a relational query. + * + * @param string $sourceType type of the source bean + * @param string $destType type of the bean you want to obtain using the relation + * @param boolean $noQuote TRUE if you want to omit quotes + * + * @return array + */ + private function getRelationalTablesAndColumns( $sourceType, $destType, $noQuote = FALSE ) + { + $linkTable = $this->esc( $this->getAssocTable( array( $sourceType, $destType ) ), $noQuote ); + $sourceCol = $this->esc( $sourceType . '_id', $noQuote ); + + if ( $sourceType === $destType ) { + $destCol = $this->esc( $destType . '2_id', $noQuote ); + } else { + $destCol = $this->esc( $destType . '_id', $noQuote ); + } + + $sourceTable = $this->esc( $sourceType, $noQuote ); + $destTable = $this->esc( $destType, $noQuote ); + + return array( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ); + } + + /** + * Determines whether a string can be considered JSON or not. + * This is used by writers that support JSON columns. However + * we dont want that code duplicated over all JSON supporting + * Query Writers. + * + * @param string $value value to determine 'JSONness' of. + * + * @return boolean + */ + protected function isJSON( $value ) + { + return ( + is_string($value) && + is_array(json_decode($value, TRUE)) && + (json_last_error() == JSON_ERROR_NONE) + ); + } + + /** + * Given a type and a property name this method + * returns the foreign key map section associated with this pair. + * + * @param string $type name of the type + * @param string $property name of the property + * + * @return array|NULL + */ + protected function getForeignKeyForTypeProperty( $type, $property ) + { + $property = $this->esc( $property, TRUE ); + + try { + $map = $this->getKeyMapForType( $type ); + } catch ( SQLException $e ) { + return NULL; + } + + foreach( $map as $key ) { + if ( $key['from'] === $property ) return $key; + } + return NULL; + } + + /** + * Returns the foreign key map (FKM) for a type. + * A foreign key map describes the foreign keys in a table. + * A FKM always has the same structure: + * + * + * array( + * 'name' => + * 'from' => + * 'table' => + * 'to' => (most of the time 'id') + * 'on_update' => + * 'on_delete' => + * ) + * + * + * @note the keys in the result array are FKDLs, i.e. descriptive unique + * keys per source table. Also see: AQueryWriter::makeFKLabel for details. + * + * @param string $type the bean type you wish to obtain a key map of + * + * @return array + */ + protected function getKeyMapForType( $type ) + { + return array(); + } + + /** + * This method makes a key for a foreign key description array. + * This key is a readable string unique for every source table. + * This uniform key is called the FKDL Foreign Key Description Label. + * Note that the source table is not part of the FKDL because + * this key is supposed to be 'per source table'. If you wish to + * include a source table, prefix the key with 'on_table__'. + * + * @param string $from the column of the key in the source table + * @param string $type the type (table) where the key points to + * @param string $to the target column of the foreign key (mostly just 'id') + * + * @return string + */ + protected function makeFKLabel($from, $type, $to) + { + return "from_{$from}_to_table_{$type}_col_{$to}"; + } + + /** + * Returns an SQL Filter snippet for reading. + * + * @param string $type type of bean + * + * @return string + */ + protected function getSQLFilterSnippet( $type ) + { + $existingCols = array(); + if (self::$flagSQLFilterSafeMode) { + $existingCols = $this->getColumns( $type ); + } + + $sqlFilters = array(); + if ( isset( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] ) ) { + foreach( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] as $property => $sqlFilter ) { + if ( !self::$flagSQLFilterSafeMode || isset( $existingCols[$property] ) ) { + $sqlFilters[] = $sqlFilter.' AS '.$property.' '; + } + } + } + $sqlFilterStr = ( count($sqlFilters) ) ? ( ','.implode( ',', $sqlFilters ) ) : ''; + return $sqlFilterStr; + } + + /** + * Generates a list of parameters (slots) for an SQL snippet. + * This method calculates the correct number of slots to insert in the + * SQL snippet and determines the correct type of slot. If the bindings + * array contains named parameters this method will return named ones and + * update the keys in the value list accordingly (that's why we use the &). + * + * If you pass an offset the bindings will be re-added to the value list. + * Some databases cant handle duplicate parameter names in queries. + * + * @param array &$valueList list of values to generate slots for (gets modified if needed) + * @param array $otherBindings list of additional bindings + * @param integer $offset start counter at... + * + * @return string + */ + protected function getParametersForInClause( &$valueList, $otherBindings, $offset = 0 ) + { + if ( is_array( $otherBindings ) && count( $otherBindings ) > 0 ) { + reset( $otherBindings ); + + $key = key( $otherBindings ); + + if ( !is_numeric($key) ) { + $filler = array(); + $newList = (!$offset) ? array() : $valueList; + $counter = $offset; + + foreach( $valueList as $value ) { + $slot = ':slot' . ( $counter++ ); + $filler[] = $slot; + $newList[$slot] = $value; + } + + // Change the keys! + $valueList = $newList; + + return implode( ',', $filler ); + } + } + + return implode( ',', array_fill( 0, count( $valueList ), '?' ) ); + } + + /** + * Adds a data type to the list of data types. + * Use this method to add a new column type definition to the writer. + * Used for UUID support. + * + * @param integer $dataTypeID magic number constant assigned to this data type + * @param string $SQLDefinition SQL column definition (i.e. INT(11)) + * + * @return self + */ + protected function addDataType( $dataTypeID, $SQLDefinition ) + { + $this->typeno_sqltype[ $dataTypeID ] = $SQLDefinition; + $this->sqltype_typeno[ $SQLDefinition ] = $dataTypeID; + return $this; + } + + /** + * Returns the sql that should follow an insert statement. + * + * @param string $table name + * + * @return string + */ + protected function getInsertSuffix( $table ) + { + return ''; + } + + /** + * Checks whether a value starts with zeros. In this case + * the value should probably be stored using a text datatype instead of a + * numerical type in order to preserve the zeros. + * + * @param string $value value to be checked. + * + * @return boolean + */ + protected function startsWithZeros( $value ) + { + $value = strval( $value ); + + if ( strlen( $value ) > 1 && strpos( $value, '0' ) === 0 && strpos( $value, '0.' ) !== 0 ) { + return TRUE; + } else { + return FALSE; + } + } + + /** + * Inserts a record into the database using a series of insert columns + * and corresponding insertvalues. Returns the insert id. + * + * @param string $table table to perform query on + * @param array $insertcolumns columns to be inserted + * @param array $insertvalues values to be inserted + * + * @return integer + */ + protected function insertRecord( $type, $insertcolumns, $insertvalues ) + { + $default = $this->defaultValue; + $suffix = $this->getInsertSuffix( $type ); + $table = $this->esc( $type ); + + if ( count( $insertvalues ) > 0 && is_array( $insertvalues[0] ) && count( $insertvalues[0] ) > 0 ) { + + $insertSlots = array(); + foreach ( $insertcolumns as $k => $v ) { + $insertcolumns[$k] = $this->esc( $v ); + + if (isset(self::$sqlFilters['w'][$type][$v])) { + $insertSlots[] = self::$sqlFilters['w'][$type][$v]; + } else { + $insertSlots[] = '?'; + } + } + + $insertSQL = "INSERT INTO $table ( id, " . implode( ',', $insertcolumns ) . " ) VALUES + ( $default, " . implode( ',', $insertSlots ) . " ) $suffix"; + + $ids = array(); + foreach ( $insertvalues as $i => $insertvalue ) { + $ids[] = $this->adapter->getCell( $insertSQL, $insertvalue, $i ); + } + + $result = count( $ids ) === 1 ? array_pop( $ids ) : $ids; + } else { + $result = $this->adapter->getCell( "INSERT INTO $table (id) VALUES($default) $suffix" ); + } + + if ( $suffix ) return $result; + + $last_id = $this->adapter->getInsertID(); + + return $last_id; + } + + /** + * Checks table name or column name. + * + * @param string $table table string + * + * @return string + */ + protected function check( $struct ) + { + if ( !is_string( $struct ) || !preg_match( '/^[a-zA-Z0-9_]+$/', $struct ) ) { + throw new RedException( 'Identifier does not conform to RedBeanPHP security policies.' ); + } + + return $struct; + } + + /** + * Checks whether the specified type (i.e. table) already exists in the database. + * Not part of the Object Database interface! + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + $tables = $this->getTables(); + + return in_array( $table, $tables ); + } + + /** + * @see QueryWriter::glueSQLCondition + */ + public function glueSQLCondition( $sql, $glue = NULL ) + { + static $snippetCache = array(); + + if ( trim( $sql ) === '' ) { + return $sql; + } + + $key = $glue . '|' . $sql; + + if ( isset( $snippetCache[$key] ) ) { + return $snippetCache[$key]; + } + + $lsql = ltrim( $sql ); + + if ( preg_match( '/^(INNER|LEFT|RIGHT|JOIN|AND|OR|WHERE|ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', $lsql ) ) { + if ( $glue === QueryWriter::C_GLUE_WHERE && stripos( $lsql, 'AND' ) === 0 ) { + $snippetCache[$key] = ' WHERE ' . substr( $lsql, 3 ); + } else { + $snippetCache[$key] = $sql; + } + } else { + $snippetCache[$key] = ( ( $glue === QueryWriter::C_GLUE_AND ) ? ' AND ' : ' WHERE ') . $sql; + } + + return $snippetCache[$key]; + } + + /** + * @see QueryWriter::glueLimitOne + */ + public function glueLimitOne( $sql = '') + { + return ( strpos( strtoupper( ' ' . $sql ), ' LIMIT ' ) === FALSE ) ? ( $sql . ' LIMIT 1 ' ) : $sql; + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $dontQuote = FALSE ) + { + $this->check( $dbStructure ); + + return ( $dontQuote ) ? $dbStructure : $this->quoteCharacter . $dbStructure . $this->quoteCharacter; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $beanType, $column, $field ) + { + $table = $beanType; + $type = $field; + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = ( isset( $this->typeno_sqltype[$type] ) ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( sprintf( $this->getDDLTemplate('addColumn', $beanType, $column ), $table, $column, $type ) ); + } + + /** + * @see QueryWriter::updateRecord + */ + public function updateRecord( $type, $updatevalues, $id = NULL ) + { + $table = $type; + + if ( !$id ) { + $insertcolumns = $insertvalues = array(); + + foreach ( $updatevalues as $pair ) { + $insertcolumns[] = $pair['property']; + $insertvalues[] = $pair['value']; + } + + //Otherwise psql returns string while MySQL/SQLite return numeric causing problems with additions (array_diff) + return (string) $this->insertRecord( $table, $insertcolumns, array( $insertvalues ) ); + } + + if ( $id && !count( $updatevalues ) ) { + return $id; + } + + $table = $this->esc( $table ); + $sql = "UPDATE $table SET "; + + $p = $v = array(); + + foreach ( $updatevalues as $uv ) { + + if ( isset( self::$sqlFilters['w'][$type][$uv['property']] ) ) { + $p[] = " {$this->esc( $uv["property"] )} = ". self::$sqlFilters['w'][$type][$uv['property']]; + } else { + $p[] = " {$this->esc( $uv["property"] )} = ? "; + } + + $v[] = $uv['value']; + } + + $sql .= implode( ',', $p ) . ' WHERE id = ? '; + + $v[] = $id; + + $this->adapter->exec( $sql, $v ); + + return $id; + } + + /** + * @see QueryWriter::parseJoin + */ + public function parseJoin( $type, $sql, $cteType = NULL ) + { + if ( strpos( $sql, '@' ) === FALSE ) { + return $sql; + } + + $sql = ' ' . $sql; + $joins = array(); + $joinSql = ''; + + if ( !preg_match_all( '#@((shared|own|joined)\.[^\s(,=!?]+)#', $sql, $matches ) ) + return $sql; + + $expressions = $matches[1]; + // Sort to make the joins from the longest to the shortest + uasort( $expressions, function($a, $b) { + return substr_count( $b, '.' ) - substr_count( $a, '.' ); + }); + + $nsuffix = 1; + foreach ( $expressions as $exp ) { + $explosion = explode( '.', $exp ); + $joinTable = $type; + $joinType = array_shift( $explosion ); + $lastPart = array_pop( $explosion ); + $lastJoin = end($explosion); + if ( ( $index = strpos( $lastJoin, '[' ) ) !== FALSE ) { + $lastJoin = substr( $lastJoin, 0, $index); + } + reset($explosion); + + // Let's check if we already joined that chain + // If that's the case we skip this + $joinKey = implode( '.', $explosion ); + foreach ( $joins as $chain => $suffix ) { + if ( strpos ( $chain, $joinKey ) === 0 ) { + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$suffix}.{$lastPart}", $sql ); + continue 2; + } + } + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$nsuffix}.{$lastPart}", $sql ); + $joins[$joinKey] = $nsuffix; + + // We loop on the elements of the join + $i = 0; + while ( TRUE ) { + $joinInfo = $explosion[$i]; + if ( $i ) { + $joinType = $explosion[$i-1]; + $joinTable = $explosion[$i-2]; + } + + $aliases = array(); + if ( ( $index = strpos( $joinInfo, '[' ) ) !== FALSE ) { + if ( preg_match_all( '#(([^\s:/\][]+)[/\]])#', $joinInfo, $matches ) ) { + $aliases = $matches[2]; + $joinInfo = substr( $joinInfo, 0, $index); + } + } + if ( ( $index = strpos( $joinTable, '[' ) ) !== FALSE ) { + $joinTable = substr( $joinTable, 0, $index); + } + + if ( $i ) { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'INNER', $joinType, FALSE, "__rb{$nsuffix}", $aliases, NULL ); + } else { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'LEFT', $joinType, TRUE, "__rb{$nsuffix}", $aliases, $cteType ); + } + + $i += 2; + if ( !isset( $explosion[$i] ) ) { + break; + } + } + $nsuffix++; + } + + $sql = str_ireplace( ' where ', ' WHERE ', $sql ); + if ( strpos( $sql, ' WHERE ') === FALSE ) { + if ( preg_match( '/^(ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', trim($sql) ) ) { + $sql = "{$joinSql} {$sql}"; + } else { + $sql = "{$joinSql} WHERE {$sql}"; + } + } else { + $sqlParts = explode( ' WHERE ', $sql, 2 ); + $sql = "{$sqlParts[0]} {$joinSql} WHERE {$sqlParts[1]}"; + } + + return $sql; + } + + /** + * @see QueryWriter::writeJoin + */ + public function writeJoin( $type, $targetType, $leftRight = 'LEFT', $joinType = 'parent', $firstOfChain = TRUE, $suffix = '', $aliases = array(), $cteType = NULL ) + { + if ( $leftRight !== 'LEFT' && $leftRight !== 'RIGHT' && $leftRight !== 'INNER' ) + throw new RedException( 'Invalid JOIN.' ); + + $globalAliases = OODBBean::getAliases(); + if ( isset( $globalAliases[$targetType] ) ) { + $destType = $globalAliases[$targetType]; + $asTargetTable = $this->esc( $targetType.$suffix ); + } else { + $destType = $targetType; + $asTargetTable = $this->esc( $destType.$suffix ); + } + + if ( $firstOfChain ) { + $table = $this->esc( $type ); + } else { + $table = $this->esc( $type.$suffix ); + } + $targetTable = $this->esc( $destType ); + + if ( $joinType == 'shared' ) { + + if ( isset( $globalAliases[$type] ) ) { + $field = $this->esc( $globalAliases[$type], TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $globalAliases[$type], $destType ) ); + } + } else { + $field = $this->esc( $type, TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $type, $destType ) ); + } + } + $linkTable = $this->esc( $assocTable ); + $asLinkTable = $this->esc( $assocTable.$suffix ); + $leftField = "id"; + $rightField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $linkField = $this->esc( $destType, TRUE ); + $linkLeftField = "id"; + $linkRightField = "{$linkField}_id"; + + $joinSql = " {$leftRight} JOIN {$linkTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asLinkTable}"; + } + $joinSql .= " ON {$table}.{$leftField} = {$asLinkTable}.{$rightField}"; + $joinSql .= " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + $joinSql .= " ON {$asTargetTable}.{$linkLeftField} = {$asLinkTable}.{$linkRightField}"; + + } elseif ( $joinType == 'own' ) { + + $field = $this->esc( $type, TRUE ); + $rightField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$alias}_id = {$table}.{$rightField}"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $leftField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } else { + + $field = $this->esc( $targetType, TRUE ); + $leftField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$leftField} = {$table}.{$alias}_id"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $rightField = "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } + + return $joinSql; + } + + /** + * Sets an SQL snippet to be used for the next queryRecord() operation. + * A select snippet will be inserted at the end of the SQL select statement and + * can be used to modify SQL-select commands to enable locking, for instance + * using the 'FOR UPDATE' snippet (this will generate an SQL query like: + * 'SELECT * FROM ... FOR UPDATE'. After the query has been executed the + * SQL snippet will be erased. Note that only the first upcoming direct or + * indirect invocation of queryRecord() through batch(), find() or load() + * will be affected. The SQL snippet will be cached. + * + * @param string $sql SQL snippet to use in SELECT statement. + * + * return self + */ + public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { + $this->sqlSelectSnippet = $sqlSelectSnippet; + return $this; + } + + /** + * @see QueryWriter::queryRecord + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache && $this->sqlSelectSnippet != self::C_SELECT_SNIPPET_FOR_UPDATE ) { + $key = $this->getCacheKey( array( $conditions, trim("$addSql {$this->sqlSelectSnippet}"), $bindings, 'select' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} {$this->sqlSelectSnippet} -- keep-cache"; + $this->sqlSelectSnippet = ''; + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache && !empty( $key ) ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordWithCursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + $sql = $this->glueSQLCondition( $addSql, NULL ); + + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} -- keep-cache"; + + return $this->adapter->getCursor( $sql, $bindings ); + } + + /** + * @see QueryWriter::queryRecordRelated + */ + public function queryRecordRelated( $sourceType, $destType, $linkIDs, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, implode( ',', $linkIDs ), trim($addSql), $bindings, 'selectrelated' ) ); + if ( $cached = $this->getCached( $destType, $key ) ) { + return $cached; + } + } + + $addSql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $inClause = $this->getParametersForInClause( $linkIDs, $bindings ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $destType ); + } + + if ( $sourceType === $destType ) { + $inClause2 = $this->getParametersForInClause( $linkIDs, $bindings, count( $bindings ) ); //for some databases + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr} , + COALESCE( + NULLIF({$linkTable}.{$sourceCol}, {$destTable}.id), + NULLIF({$linkTable}.{$destCol}, {$destTable}.id)) AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} IN ($inClause2) ) + {$addSql} + -- keep-cache"; + + $linkIDs = array_merge( $linkIDs, $linkIDs ); + } else { + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr}, + {$linkTable}.{$sourceCol} AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) + {$addSql} + -- keep-cache"; + } + + $bindings = array_merge( $linkIDs, $bindings ); + + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $destType, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordLink + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, $destType, $sourceID, $destID, 'selectlink' ) ); + if ( $cached = $this->getCached( $linkTable, $key ) ) { + return $cached; + } + } + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $linkType = $this->getAssocTable( array( $sourceType, $destType ) ); + $sqlFilterStr = $this->getSQLFilterSnippet( "{$linkType}" ); + } + + if ( $sourceTable === $destTable ) { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE ( {$sourceCol} = ? AND {$destCol} = ? ) OR + ( {$destCol} = ? AND {$sourceCol} = ? ) -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID, $sourceID, $destID ) ); + } else { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE {$sourceCol} = ? AND {$destCol} = ? -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID ) ); + } + + if ( $this->flagUseCache ) { + $this->putResultInCache( $linkTable, $key, $row ); + } + + return $row; + } + + /** + * Returns or counts all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * @param string $wrap SQL wrapper string (use %s for subquery) + * + * @return array + */ + private function queryTaggedGeneric( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array(), $wrap = '%s' ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( implode( ',', $tagList ), $all, trim($addSql), $bindings, 'selectTagged' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $assocType = $this->getAssocTable( array( $type, 'tag' ) ); + $assocTable = $this->esc( $assocType ); + $assocField = $type . '_id'; + $table = $this->esc( $type ); + $slots = implode( ',', array_fill( 0, count( $tagList ), '?' ) ); + $score = ( $all ) ? count( $tagList ) : 1; + + $sql = " + SELECT {$table}.* FROM {$table} + INNER JOIN {$assocTable} ON {$assocField} = {$table}.id + INNER JOIN tag ON {$assocTable}.tag_id = tag.id + WHERE tag.title IN ({$slots}) + GROUP BY {$table}.id + HAVING count({$table}.id) >= ? + {$addSql} + -- keep-cache + "; + $sql = sprintf($wrap,$sql); + + $bindings = array_merge( $tagList, array( $score ), $bindings ); + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryTagged + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + return $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings ); + } + + /** + * @see QueryWriter::queryCountTagged + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + $rows = $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings, 'SELECT COUNT(*) AS counted FROM (%s) AS counting' ); + return intval($rows[0]['counted']); + } + + /** + * @see QueryWriter::queryRecordCount + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $conditions, trim($addSql), $bindings, 'count' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = $this->parseJoin( $type, $sql ); + + $sql = "SELECT COUNT(*) FROM {$table} {$sql} -- keep-cache"; + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecordCountRelated + */ + public function queryRecordCountRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $cacheType = "#{$sourceType}/{$destType}"; + $key = $this->getCacheKey( array( $sourceType, $destType, $linkID, trim($addSql), $bindings, 'countrelated' ) ); + if ( $cached = $this->getCached( $cacheType, $key ) ) { + return $cached; + } + } + + if ( $sourceType === $destType ) { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID, $linkID ), $bindings ); + } else { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID ), $bindings ); + } + + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $cacheType, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecursiveCommonTableExpression + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $selectForm = FALSE ) + { + if ($selectForm === QueryWriter::C_CTE_SELECT_COUNT) { + $selectForm = "count(redbeantree.*)"; + } elseif ( $selectForm === QueryWriter::C_CTE_SELECT_NORMAL ) { + $selectForm = "redbeantree.*"; + } + $alias = $up ? 'parent' : 'child'; + $direction = $up ? " {$alias}.{$type}_id = {$type}.id " : " {$alias}.id = {$type}.{$type}_id "; + /* allow numeric and named param bindings, if '0' exists then numeric */ + if ( array_key_exists( 0,$bindings ) ) { + array_unshift( $bindings, $id ); + $idSlot = '?'; + } else { + $idSlot = ':slot0'; + $bindings[$idSlot] = $id; + } + $sql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $sql = $this->parseJoin( 'redbeantree', $sql, $type ); + $rows = $this->adapter->get(" + WITH RECURSIVE redbeantree AS + ( + SELECT * + FROM {$type} WHERE {$type}.id = {$idSlot} + UNION ALL + SELECT {$type}.* FROM {$type} + INNER JOIN redbeantree {$alias} ON {$direction} + ) + SELECT {$selectForm} FROM redbeantree {$sql};", + $bindings + ); + return $rows; + } + + /** + * @see QueryWriter::deleteRecord + */ + public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = "DELETE FROM {$table} {$sql}"; + + return $this->adapter->exec( $sql, $bindings ); + } + + /** + * @see QueryWriter::deleteRelations + */ + public function deleteRelations( $sourceType, $destType, $sourceID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $sourceTable === $destTable ) { + $sql = "DELETE FROM {$linkTable} + WHERE ( {$sourceCol} = ? ) OR + ( {$destCol} = ? ) + "; + + $this->adapter->exec( $sql, array( $sourceID, $sourceID ) ); + } else { + $sql = "DELETE FROM {$linkTable} + WHERE {$sourceCol} = ? "; + + $this->adapter->exec( $sql, array( $sourceID ) ); + } + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $property, $dataType ) + { + if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE; + + $table = $this->esc( $type ); + $column = $this->esc( $property ); + + $newType = $this->typeno_sqltype[$dataType]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'widenColumn', $type, $column ), $type, $column, $column, $newType ) ); + + return TRUE; + } + + /** + * @see QueryWriter::wipe + */ + public function wipe( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( "TRUNCATE $table " ); + } + + /** + * @see QueryWriter::renameAssocTable + */ + public function renameAssocTable( $from, $to = NULL ) + { + self::renameAssociation( $from, $to ); + } + + /** + * @see QueryWriter::getAssocTable + */ + public function getAssocTable( $types ) + { + return self::getAssocTableFormat( $types ); + } + + /** + * Turns caching on or off. Default: off. + * If caching is turned on retrieval queries fired after eachother will + * use a result row cache. + * + * @param boolean + * + * @return void + */ + public function setUseCache( $yesNo ) + { + $this->flushCache(); + + $this->flagUseCache = (bool) $yesNo; + } + + /** + * Flushes the Query Writer Cache. + * Clears the internal query cache array and returns its overall + * size. + * + * @return mixed + */ + public function flushCache( $newMaxCacheSizePerType = NULL, $countCache = TRUE ) + { + if ( !is_null( $newMaxCacheSizePerType ) && $newMaxCacheSizePerType > 0 ) { + $this->maxCacheSizePerType = $newMaxCacheSizePerType; + } + $count = $countCache ? count( $this->cache, COUNT_RECURSIVE ) : NULL; + $this->cache = array(); + return $count; + } + + /** + * @deprecated Use esc() instead. + * + * @param string $column column to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeColumn( $column, $noQuotes = FALSE ) + { + return $this->esc( $column, $noQuotes ); + } + + /** + * @deprecated Use esc() instead. + * + * @param string $table table to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeTable( $table, $noQuotes = FALSE ) + { + return $this->esc( $table, $noQuotes ); + } + + /** + * @see QueryWriter::addUniqueConstraint + */ + public function addUniqueIndex( $type, $properties ) + { + return $this->addUniqueConstraint( $type, $properties ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/CUBRID.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/CUBRID.php new file mode 100644 index 0000000..92ce96a --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/CUBRID.php @@ -0,0 +1,364 @@ +esc( $type ); + $tableNoQ = $this->esc( $type, TRUE ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $column = $this->esc( $property ); + $columnNoQ = $this->esc( $property, TRUE ); + $targetColumn = $this->esc( $targetProperty ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $columnNoQ ) ) ) return FALSE; + $needsToDropFK = FALSE; + $casc = ( $isDep ? 'CASCADE' : 'SET NULL' ); + $sql = "ALTER TABLE $table ADD CONSTRAINT FOREIGN KEY($column) REFERENCES $targetTable($targetColumn) ON DELETE $casc "; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $sqlCode = $this->adapter->get("SHOW CREATE TABLE `{$type}`"); + if (!isset($sqlCode[0])) return array(); + $matches = array(); + preg_match_all( '/CONSTRAINT\s+\[([\w_]+)\]\s+FOREIGN\s+KEY\s+\(\[([\w_]+)\]\)\s+REFERENCES\s+\[([\w_]+)\](\s+ON\s+DELETE\s+(CASCADE|SET\sNULL|RESTRICT|NO\sACTION)\s+ON\s+UPDATE\s+(SET\sNULL|RESTRICT|NO\sACTION))?/', $sqlCode[0]['CREATE TABLE'], $matches ); + $list = array(); + if (!isset($matches[0])) return $list; + $max = count($matches[0]); + for($i = 0; $i < $max; $i++) { + $label = $this->makeFKLabel( $matches[2][$i], $matches[3][$i], 'id' ); + $list[ $label ] = array( + 'name' => $matches[1][$i], + 'from' => $matches[2][$i], + 'table' => $matches[3][$i], + 'to' => 'id', + 'on_update' => $matches[6][$i], + 'on_delete' => $matches[5][$i] + ); + } + return $list; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + CUBRID::C_DATATYPE_INTEGER => ' INTEGER ', + CUBRID::C_DATATYPE_DOUBLE => ' DOUBLE ', + CUBRID::C_DATATYPE_STRING => ' STRING ', + CUBRID::C_DATATYPE_SPECIAL_DATE => ' DATE ', + CUBRID::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( ( $v ) )] = $k; + } + + $this->sqltype_typeno['STRING(1073741823)'] = self::C_DATATYPE_STRING; + + $this->adapter = $adapter; + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + $rows = $this->adapter->getCol( "SELECT class_name FROM db_class WHERE is_system_class = 'NO';" ); + + return $rows; + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $table ) + { + $sql = 'CREATE TABLE ' + . $this->esc( $table ) + . ' ("id" integer AUTO_INCREMENT, CONSTRAINT "pk_' + . $this->esc( $table, TRUE ) + . '_id" PRIMARY KEY("id"))'; + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table ); + + $columnsRaw = $this->adapter->get( "SHOW COLUMNS FROM $table" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['Field']] = $r['Type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( is_null( $value ) ) { + return self::C_DATATYPE_INTEGER; + } + + if ( $flagSpecial ) { + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATE; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATETIME; + } + } + + $value = strval( $value ); + + if ( !$this->startsWithZeros( $value ) ) { + if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= -2147483647 && $value <= 2147483647 ) { + return self::C_DATATYPE_INTEGER; + } + if ( is_numeric( $value ) ) { + return self::C_DATATYPE_DOUBLE; + } + } + + return self::C_DATATYPE_STRING; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : self::C_DATATYPE_SPECIFIED ); + + if ( $includeSpecials ) { + return $r; + } + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $type, $column, $field ) + { + $table = $type; + $type = $field; + + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = array_key_exists( $type, $this->typeno_sqltype ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( "ALTER TABLE $table ADD COLUMN $column $type " ); + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // else we get multiple indexes due to order-effects + $name = 'UQ_' . sha1( implode( ',', $columns ) ); + $sql = "ALTER TABLE $table ADD CONSTRAINT UNIQUE $name (" . implode( ',', $columns ) . ")"; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + return ( $state == 'HY000' ) ? ( count( array_diff( array( + QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), $list ) ) !== 3 ) : FALSE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $column ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $column ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDependent ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + foreach ( $this->getTables() as $t ) { + foreach ( $this->getKeyMapForType( $t ) as $k ) { + $this->adapter->exec( "ALTER TABLE \"$t\" DROP FOREIGN KEY \"{$k['name']}\"" ); + } + } + foreach ( $this->getTables() as $t ) { + $this->adapter->exec( "DROP TABLE \"$t\"" ); + } + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $noQuotes = FALSE ) + { + return parent::esc( strtolower( $dbStructure ), $noQuotes ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/Firebird.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/Firebird.php new file mode 100644 index 0000000..fd816e0 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/Firebird.php @@ -0,0 +1,352 @@ +esc( $type, TRUE ); + $keys = $this->adapter->get(' + SELECT + information_schema.key_column_usage.constraint_name AS `name`, + information_schema.key_column_usage.referenced_table_name AS `table`, + information_schema.key_column_usage.column_name AS `from`, + information_schema.key_column_usage.referenced_column_name AS `to`, + information_schema.referential_constraints.update_rule AS `on_update`, + information_schema.referential_constraints.delete_rule AS `on_delete` + FROM information_schema.key_column_usage + INNER JOIN information_schema.referential_constraints + ON ( + information_schema.referential_constraints.constraint_name = information_schema.key_column_usage.constraint_name + AND information_schema.referential_constraints.constraint_schema = information_schema.key_column_usage.constraint_schema + AND information_schema.referential_constraints.constraint_catalog = information_schema.key_column_usage.constraint_catalog + ) + WHERE + information_schema.key_column_usage.table_schema IN ( SELECT DATABASE() ) + AND information_schema.key_column_usage.table_name = ? + AND information_schema.key_column_usage.constraint_name != \'PRIMARY\' + AND information_schema.key_column_usage.referenced_table_name IS NOT NULL + ', array($table)); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $k['name'], + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + Firebird::C_DATATYPE_INTEGER => 'INTEGER', + Firebird::C_DATATYPE_FLOAT => 'FLOAT', + Firebird::C_DATATYPE_TEXT => 'VARCHAR(8190)', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtoupper( $v ) )] = $k; + } + + $this->adapter = $adapter; + $this->encoding = $this->adapter->getDatabase()->getMysqlEncoding(); + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer $const data type to be used for IDS. + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + return $this->adapter->getCol( 'SELECT RDB$RELATION_NAME FROM RDB$RELATIONS + WHERE RDB$VIEW_BLR IS NULL AND + (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0)'); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $table ) + { + $tableNoQ = $this->esc( $table ); + $tableSQL = "CREATE TABLE \"{$table}\" ( id INT )"; + $dropGeneratorSQL = "DROP GENERATOR gen{$table}"; + $generatorSQL = "CREATE GENERATOR gen{$table}"; + $generatorSQL2 = "SET GENERATOR gen{$table} TO 0"; + $triggerSQL = " + CREATE TRIGGER ai{$table} FOR \"{$table}\" + ACTIVE BEFORE INSERT POSITION 0 + AS + BEGIN + if (NEW.id is NULL) then NEW.id = GEN_ID(gen{$table}, 1); + END + "; + + try { $this->adapter->exec( $dropGeneratorSQL ); }catch( SQLException $e ) {}; + $this->adapter->exec( $tableSQL ); + $this->adapter->exec( $generatorSQL ); + $this->adapter->exec( $generatorSQL2 ); + $this->adapter->exec( $triggerSQL ); + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $property, $dataType ) + { + if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE; + + $table = $this->esc( $type ); + $column = $this->esc( $property ); + + $newType = $this->typeno_sqltype[$dataType]; + + $this->adapter->exec( "ALTER TABLE $table ALTER COLUMN $column TYPE $newType " ); + + return TRUE; + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $columnsRaw = $this->adapter->getAssoc( ' + SELECT + RDB$RELATION_FIELDS.RDB$FIELD_NAME, + CASE RDB$FIELDS.RDB$FIELD_TYPE + WHEN 10 THEN \'FLOAT\' + WHEN 8 THEN \'INTEGER\' + WHEN 37 THEN \'VARCHAR\' + ELSE RDB$FIELDS.RDB$FIELD_TYPE + END AS FTYPE + FROM RDB$RELATION_FIELDS + LEFT JOIN RDB$FIELDS ON RDB$RELATION_FIELDS.RDB$FIELD_SOURCE = RDB$FIELDS.RDB$FIELD_NAME + WHERE RDB$RELATION_FIELDS.RDB$RELATION_NAME = \''.($this->esc($table, true)).'\' + ORDER BY RDB$RELATION_FIELDS.RDB$FIELD_POSITION + '); + $columns = array(); + foreach( $columnsRaw as $rawKey => $columnRaw ) { + $columns[ trim( $rawKey ) ] = trim( $columnRaw ); + } + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + if ( AQueryWriter::canBeTreatedAsInt( $value ) ) { + return FireBird::C_DATATYPE_INTEGER; + } + if ( !$this->startsWithZeros( $value ) && is_numeric( $value ) ) { + return FireBird::C_DATATYPE_DOUBLE; + } + return FireBird::C_DATATYPE_TEXT; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + if ( isset( $this->sqltype_typeno[$typedescription] ) ) { + return $this->sqltype_typeno[$typedescription]; + } else { + return self::C_DATATYPE_SPECIFIED; + } + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // Else we get multiple indexes due to order-effects + $name = 'UQ_'.substr( sha1( implode( ',', $columns ) ), 0, 28); + try { + $sql = "ALTER TABLE $table + ADD CONSTRAINT $name UNIQUE (" . implode( ',', $columns ) . ")"; + $this->adapter->exec( $sql ); + } catch ( SQLException $e ) { + //do nothing, dont use alter table ignore, this will delete duplicate records in 3-ways! + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $property ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $property ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + $table = $this->esc( $type ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $field = $this->esc( $property ); + $fieldNoQ = $this->esc( $property, TRUE ); + $targetField = $this->esc( $targetProperty ); + $targetFieldNoQ = $this->esc( $targetProperty, TRUE ); + $tableNoQ = $this->esc( $type, TRUE ); + $fieldNoQ = $this->esc( $property, TRUE ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; + + //Widen the column if it's incapable of representing a foreign key (at least INT). + $columns = $this->getColumns( $tableNoQ ); + $idType = $this->getTypeForID(); + if ( $this->code( $columns[$fieldNoQ] ) !== $idType ) { + $this->widenColumn( $type, $property, $idType ); + } + + $fkName = 'fk_'.($tableNoQ.'_'.$fieldNoQ); + $cName = 'c_'.$fkName; + try { + $this->adapter->exec( " + ALTER TABLE {$table} + ADD CONSTRAINT $cName + FOREIGN KEY $fkName ( {$fieldNoQ} ) REFERENCES {$targetTableNoQ} + ({$targetFieldNoQ}) ON DELETE " . ( $isDependent ? 'CASCADE' : 'SET NULL' ) . ' ON UPDATE '.( $isDependent ? 'CASCADE' : 'SET NULL' ).';'); + } catch ( SQLException $e ) { + // Failure of fk-constraints is not a problem + } + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '42S02' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + '42S22' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION + ); + + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $tables = $this->getTables(); + foreach( $tables as $table ) { + $table = trim( $table ); + $this->adapter->exec( "DROP TABLE \"{$table}\" " ); + } + } +} + diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/MySQL.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/MySQL.php new file mode 100644 index 0000000..1e274ac --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/MySQL.php @@ -0,0 +1,460 @@ + array( + '*' => 'ALTER TABLE %s ADD %s %s ' + ), + 'createTable' => array( + '*' => 'CREATE TABLE %s (id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY ( id )) ENGINE = InnoDB DEFAULT CHARSET=%s COLLATE=%s ' + ), + 'widenColumn' => array( + '*' => 'ALTER TABLE `%s` CHANGE %s %s %s ' + ) + ); + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $databaseName = $this->adapter->getCell('SELECT DATABASE()'); + $table = $this->esc( $type, TRUE ); + $keys = $this->adapter->get(' + SELECT + information_schema.key_column_usage.constraint_name AS `name`, + information_schema.key_column_usage.referenced_table_name AS `table`, + information_schema.key_column_usage.column_name AS `from`, + information_schema.key_column_usage.referenced_column_name AS `to`, + information_schema.referential_constraints.update_rule AS `on_update`, + information_schema.referential_constraints.delete_rule AS `on_delete` + FROM information_schema.key_column_usage + INNER JOIN information_schema.referential_constraints + ON information_schema.referential_constraints.constraint_name = information_schema.key_column_usage.constraint_name + WHERE + information_schema.key_column_usage.table_schema = :database + AND information_schema.referential_constraints.constraint_schema = :database + AND information_schema.key_column_usage.constraint_schema = :database + AND information_schema.key_column_usage.table_name = :table + AND information_schema.key_column_usage.constraint_name != \'PRIMARY\' + AND information_schema.key_column_usage.referenced_table_name IS NOT NULL + ', array( ':database' => $databaseName, ':table' => $table ) ); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $k['name'], + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + * @param array $options options array + */ + public function __construct( Adapter $adapter, $options = array() ) + { + $this->typeno_sqltype = array( + MySQL::C_DATATYPE_BOOL => ' TINYINT(1) UNSIGNED ', + MySQL::C_DATATYPE_UINT32 => ' INT(11) UNSIGNED ', + MySQL::C_DATATYPE_DOUBLE => ' DOUBLE ', + MySQL::C_DATATYPE_TEXT7 => ' VARCHAR(191) ', + MYSQL::C_DATATYPE_TEXT8 => ' VARCHAR(255) ', + MySQL::C_DATATYPE_TEXT16 => ' TEXT ', + MySQL::C_DATATYPE_TEXT32 => ' LONGTEXT ', + MySQL::C_DATATYPE_SPECIAL_DATE => ' DATE ', + MySQL::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', + MySQL::C_DATATYPE_SPECIAL_TIME => ' TIME ', + MySQL::C_DATATYPE_SPECIAL_POINT => ' POINT ', + MySQL::C_DATATYPE_SPECIAL_LINESTRING => ' LINESTRING ', + MySQL::C_DATATYPE_SPECIAL_POLYGON => ' POLYGON ', + MySQL::C_DATATYPE_SPECIAL_MONEY => ' DECIMAL(10,2) ', + MYSQL::C_DATATYPE_SPECIAL_JSON => ' JSON ' + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; + } + + $this->adapter = $adapter; + $this->encoding = $this->adapter->getDatabase()->getMysqlEncoding(); + $me = $this; + if (!isset($options['noInitcode'])) + $this->adapter->setInitCode(function($version) use(&$me) { + try { + if (strpos($version, 'maria')===FALSE && intval($version)>=8) { + $me->useFeature('ignoreDisplayWidth'); + } + } catch( \Exception $e ){} + }); + } + + /** + * Enables certain features/dialects. + * + * - ignoreDisplayWidth required for MySQL8+ + * (automatically set by setup() if you pass dsn instead of PDO object) + * + * @param string $name feature ID + * + * @return void + */ + public function useFeature($name) { + if ($name == 'ignoreDisplayWidth') { + $this->typeno_sqltype[MySQL::C_DATATYPE_BOOL] = ' TINYINT UNSIGNED '; + $this->typeno_sqltype[MySQL::C_DATATYPE_UINT32] = ' INT UNSIGNED '; + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; + } + } + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_UINT32; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + return $this->adapter->getCol( 'show tables' ); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $type ) + { + $table = $this->esc( $type ); + + $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); + $charset = $charset_collate['charset']; + $collate = $charset_collate['collate']; + + $sql = sprintf( $this->getDDLTemplate( 'createTable', $type ), $table, $charset, $collate ); + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $columnsRaw = $this->adapter->get( "DESCRIBE " . $this->esc( $table ) ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['Field']] = $r['Type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( is_null( $value ) ) return MySQL::C_DATATYPE_BOOL; + if ( $value === INF ) return MySQL::C_DATATYPE_TEXT7; + + if ( $flagSpecial ) { + if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_MONEY; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_DATE; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_DATETIME; + } + if ( preg_match( '/^POINT\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_POINT; + } + if ( preg_match( '/^LINESTRING\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_LINESTRING; + } + if ( preg_match( '/^POLYGON\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_POLYGON; + } + if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) { + return self::C_DATATYPE_SPECIAL_JSON; + } + } + + //setter turns TRUE FALSE into 0 and 1 because database has no real bools (TRUE and FALSE only for test?). + if ( $value === FALSE || $value === TRUE || $value === '0' || $value === '1' || $value === 0 || $value === 1 ) { + return MySQL::C_DATATYPE_BOOL; + } + + if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE; + + if ( !$this->startsWithZeros( $value ) ) { + + if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= 0 && $value <= 4294967295 ) { + return MySQL::C_DATATYPE_UINT32; + } + + if ( is_numeric( $value ) ) { + return MySQL::C_DATATYPE_DOUBLE; + } + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 191 ) { + return MySQL::C_DATATYPE_TEXT7; + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 255 ) { + return MySQL::C_DATATYPE_TEXT8; + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 65535 ) { + return MySQL::C_DATATYPE_TEXT16; + } + + return MySQL::C_DATATYPE_TEXT32; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + if ( isset( $this->sqltype_typeno[$typedescription] ) ) { + $r = $this->sqltype_typeno[$typedescription]; + } else { + $r = self::C_DATATYPE_SPECIFIED; + } + + if ( $includeSpecials ) { + return $r; + } + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // Else we get multiple indexes due to order-effects + $name = 'UQ_' . sha1( implode( ',', $columns ) ); + try { + $sql = "ALTER TABLE $table + ADD UNIQUE INDEX $name (" . implode( ',', $columns ) . ")"; + $this->adapter->exec( $sql ); + } catch ( SQLException $e ) { + //do nothing, dont use alter table ignore, this will delete duplicate records in 3-ways! + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $property ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $property ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + * @return bool + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + $table = $this->esc( $type ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $field = $this->esc( $property ); + $fieldNoQ = $this->esc( $property, TRUE ); + $targetField = $this->esc( $targetProperty ); + $targetFieldNoQ = $this->esc( $targetProperty, TRUE ); + $tableNoQ = $this->esc( $type, TRUE ); + $fieldNoQ = $this->esc( $property, TRUE ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; + + //Widen the column if it's incapable of representing a foreign key (at least INT). + $columns = $this->getColumns( $tableNoQ ); + $idType = $this->getTypeForID(); + if ( $this->code( $columns[$fieldNoQ] ) !== $idType ) { + $this->widenColumn( $type, $property, $idType ); + } + + $fkName = 'fk_'.($tableNoQ.'_'.$fieldNoQ); + $cName = 'c_'.$fkName; + try { + $this->adapter->exec( " + ALTER TABLE {$table} + ADD CONSTRAINT $cName + FOREIGN KEY $fkName ( `{$fieldNoQ}` ) REFERENCES `{$targetTableNoQ}` + (`{$targetFieldNoQ}`) ON DELETE " . ( $isDependent ? 'CASCADE' : 'SET NULL' ) . ' ON UPDATE '.( $isDependent ? 'CASCADE' : 'SET NULL' ).';'); + } catch ( SQLException $e ) { + // Failure of fk-constraints is not a problem + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '42S02' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + '42S22' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + ); + + if ( $state == 'HY000' && !empty( $extraDriverDetails[1] ) ) { + $driverCode = $extraDriverDetails[1]; + + if ( $driverCode == '1205' && in_array( QueryWriter::C_SQLSTATE_LOCK_TIMEOUT, $list ) ) { + return TRUE; + } + } + + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 0;' ); + + foreach ( $this->getTables() as $t ) { + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + try { $this->adapter->exec( "DROP VIEW IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + } + + $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 1;' ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/PostgreSQL.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/PostgreSQL.php new file mode 100644 index 0000000..9f4e681 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/PostgreSQL.php @@ -0,0 +1,436 @@ + array( + '*' => 'ALTER TABLE %s ADD %s %s ' + ), + 'createTable' => array( + '*' => 'CREATE TABLE %s (id SERIAL PRIMARY KEY) ' + ), + 'widenColumn' => array( + '*' => 'ALTER TABLE %s ALTER COLUMN %s TYPE %s' + ) + ); + + /** + * Returns the insert suffix SQL Snippet + * + * @param string $table table + * + * @return string $sql SQL Snippet + */ + protected function getInsertSuffix( $table ) + { + return 'RETURNING id '; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $table = $this->esc( $type, TRUE ); + $keys = $this->adapter->get( ' + SELECT + information_schema.key_column_usage.constraint_name AS "name", + information_schema.key_column_usage.column_name AS "from", + information_schema.constraint_table_usage.table_name AS "table", + information_schema.constraint_column_usage.column_name AS "to", + information_schema.referential_constraints.update_rule AS "on_update", + information_schema.referential_constraints.delete_rule AS "on_delete" + FROM information_schema.key_column_usage + INNER JOIN information_schema.constraint_table_usage + ON ( + information_schema.key_column_usage.constraint_name = information_schema.constraint_table_usage.constraint_name + AND information_schema.key_column_usage.constraint_schema = information_schema.constraint_table_usage.constraint_schema + AND information_schema.key_column_usage.constraint_catalog = information_schema.constraint_table_usage.constraint_catalog + ) + INNER JOIN information_schema.constraint_column_usage + ON ( + information_schema.key_column_usage.constraint_name = information_schema.constraint_column_usage.constraint_name + AND information_schema.key_column_usage.constraint_schema = information_schema.constraint_column_usage.constraint_schema + AND information_schema.key_column_usage.constraint_catalog = information_schema.constraint_column_usage.constraint_catalog + ) + INNER JOIN information_schema.referential_constraints + ON ( + information_schema.key_column_usage.constraint_name = information_schema.referential_constraints.constraint_name + AND information_schema.key_column_usage.constraint_schema = information_schema.referential_constraints.constraint_schema + AND information_schema.key_column_usage.constraint_catalog = information_schema.referential_constraints.constraint_catalog + ) + WHERE + information_schema.key_column_usage.table_catalog = current_database() + AND information_schema.key_column_usage.table_schema = ANY( current_schemas( FALSE ) ) + AND information_schema.key_column_usage.table_name = ? + ', array( $type ) ); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $k['name'], + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + self::C_DATATYPE_INTEGER => ' integer ', + self::C_DATATYPE_DOUBLE => ' double precision ', + self::C_DATATYPE_TEXT => ' text ', + self::C_DATATYPE_SPECIAL_DATE => ' date ', + self::C_DATATYPE_SPECIAL_TIME => ' time ', + self::C_DATATYPE_SPECIAL_TIMEZ => ' time with time zone ', + self::C_DATATYPE_SPECIAL_DATETIME => ' timestamp without time zone ', + self::C_DATATYPE_SPECIAL_POINT => ' point ', + self::C_DATATYPE_SPECIAL_LSEG => ' lseg ', + self::C_DATATYPE_SPECIAL_CIRCLE => ' circle ', + self::C_DATATYPE_SPECIAL_MONEY => ' money ', + self::C_DATATYPE_SPECIAL_MONEY2 => ' numeric(10,2) ', + self::C_DATATYPE_SPECIAL_POLYGON => ' polygon ', + self::C_DATATYPE_SPECIAL_JSON => ' json ', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; + } + + $this->adapter = $adapter; + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + return $this->adapter->getCol( 'SELECT table_name FROM information_schema.tables WHERE table_schema = ANY( current_schemas( FALSE ) )' ); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'createTable', $type ), $table ) ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table, TRUE ); + + $columnsRaw = $this->adapter->get( "SELECT column_name, data_type FROM information_schema.columns WHERE table_name='$table' AND table_schema = ANY( current_schemas( FALSE ) )" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['column_name']] = $r['data_type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( $value === INF ) return self::C_DATATYPE_TEXT; + + if ( $flagSpecial && $value ) { + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_DATE; + } + + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d(\.\d{1,6})?$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_DATETIME; + } + + if ( preg_match( '/^\([\d\.]+,[\d\.]+\)$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_POINT; + } + + if ( preg_match( '/^\[\([\d\.]+,[\d\.]+\),\([\d\.]+,[\d\.]+\)\]$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_LSEG; + } + + if ( preg_match( '/^\<\([\d\.]+,[\d\.]+\),[\d\.]+\>$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_CIRCLE; + } + + if ( preg_match( '/^\((\([\d\.]+,[\d\.]+\),?)+\)$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_POLYGON; + } + + if ( preg_match( '/^\-?(\$|€|¥|£)[\d,\.]+$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_MONEY; + } + + if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) { + return PostgreSQL::C_DATATYPE_SPECIAL_MONEY2; + } + if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) { + return self::C_DATATYPE_SPECIAL_JSON; + } + } + + if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE; + + if ( $this->startsWithZeros( $value ) ) return self::C_DATATYPE_TEXT; + + if ( $value === FALSE || $value === TRUE || $value === NULL || ( is_numeric( $value ) + && AQueryWriter::canBeTreatedAsInt( $value ) + && $value < 2147483648 + && $value > -2147483648 ) + ) { + return self::C_DATATYPE_INTEGER; + } elseif ( is_numeric( $value ) ) { + return self::C_DATATYPE_DOUBLE; + } else { + return self::C_DATATYPE_TEXT; + } + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : 99; + + if ( $includeSpecials ) return $r; + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $beanType, $column, $datatype ) + { + $table = $beanType; + $type = $datatype; + + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $newtype = $this->typeno_sqltype[$type]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'widenColumn', $beanType, $column ), $table, $column, $newtype ) ); + + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); //else we get multiple indexes due to order-effects + $name = "UQ_" . sha1( $table . implode( ',', $columns ) ); + $sql = "ALTER TABLE {$table} + ADD CONSTRAINT $name UNIQUE (" . implode( ',', $columns ) . ")"; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '42P01' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + '42703' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + '23505' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + '55P03' => QueryWriter::C_SQLSTATE_LOCK_TIMEOUT + ); + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $property ) + { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $property ); + + try { + $this->adapter->exec( "CREATE INDEX {$name} ON $table ({$column}) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) + { + $table = $this->esc( $type ); + $targetTable = $this->esc( $targetType ); + $field = $this->esc( $property ); + $targetField = $this->esc( $targetProperty ); + $tableNoQ = $this->esc( $type, TRUE ); + $fieldNoQ = $this->esc( $property, TRUE ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; + try{ + $delRule = ( $isDep ? 'CASCADE' : 'SET NULL' ); + $this->adapter->exec( "ALTER TABLE {$table} + ADD FOREIGN KEY ( {$field} ) REFERENCES {$targetTable} + ({$targetField}) ON DELETE {$delRule} ON UPDATE {$delRule} DEFERRABLE ;" ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $this->adapter->exec( 'SET CONSTRAINTS ALL DEFERRED' ); + + foreach ( $this->getTables() as $t ) { + $t = $this->esc( $t ); + //Some plugins (PostGIS have unremovable tables/views), avoid exceptions. + try { $this->adapter->exec( "DROP TABLE IF EXISTS $t CASCADE " ); }catch( \Exception $e ) {} + } + + $this->adapter->exec( 'SET CONSTRAINTS ALL IMMEDIATE' ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/SQLiteT.php b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/SQLiteT.php new file mode 100644 index 0000000..eba1431 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/QueryWriter/SQLiteT.php @@ -0,0 +1,496 @@ + array( + '*' => 'ALTER TABLE `%s` ADD `%s` %s' + ), + 'createTable' => array( + '*' => 'CREATE TABLE %s ( id INTEGER PRIMARY KEY AUTOINCREMENT )' + ), + 'widenColumn' => array( + '*' => ',`%s` %s ' + ) + ); + + /** + * Gets all information about a table (from a type). + * + * Format: + * array( + * name => name of the table + * columns => array( name => datatype ) + * indexes => array() raw index information rows from PRAGMA query + * keys => array() raw key information rows from PRAGMA query + * ) + * + * @param string $type type you want to get info of + * + * @return array + */ + protected function getTable( $type ) + { + $tableName = $this->esc( $type, TRUE ); + $columns = $this->getColumns( $type ); + $indexes = $this->getIndexes( $type ); + $keys = $this->getKeyMapForType( $type ); + + $table = array( + 'columns' => $columns, + 'indexes' => $indexes, + 'keys' => $keys, + 'name' => $tableName + ); + + $this->tableArchive[$tableName] = $table; + + return $table; + } + + /** + * Puts a table. Updates the table structure. + * In SQLite we can't change columns, drop columns, change or add foreign keys so we + * have a table-rebuild function. You simply load your table with getTable(), modify it and + * then store it with putTable()... + * + * @param array $tableMap information array + * + * @return void + */ + protected function putTable( $tableMap ) + { + $table = $tableMap['name']; + $q = array(); + $q[] = "DROP TABLE IF EXISTS tmp_backup;"; + + $oldColumnNames = array_keys( $this->getColumns( $table ) ); + + foreach ( $oldColumnNames as $k => $v ) $oldColumnNames[$k] = "`$v`"; + + $q[] = "CREATE TEMPORARY TABLE tmp_backup(" . implode( ",", $oldColumnNames ) . ");"; + $q[] = "INSERT INTO tmp_backup SELECT * FROM `$table`;"; + $q[] = "PRAGMA foreign_keys = 0 "; + $q[] = "DROP TABLE `$table`;"; + + $newTableDefStr = ''; + foreach ( $tableMap['columns'] as $column => $type ) { + if ( $column != 'id' ) { + $newTableDefStr .= sprintf( $this->getDDLTemplate( 'widenColumn', $table, $column ), $column, $type ); + } + } + + $fkDef = ''; + foreach ( $tableMap['keys'] as $key ) { + $fkDef .= ", FOREIGN KEY(`{$key['from']}`) + REFERENCES `{$key['table']}`(`{$key['to']}`) + ON DELETE {$key['on_delete']} ON UPDATE {$key['on_update']}"; + } + + $q[] = "CREATE TABLE `$table` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT $newTableDefStr $fkDef );"; + + foreach ( $tableMap['indexes'] as $name => $index ) { + if ( strpos( $name, 'UQ_' ) === 0 ) { + $cols = explode( '__', substr( $name, strlen( 'UQ_' . $table ) ) ); + foreach ( $cols as $k => $v ) $cols[$k] = "`$v`"; + $q[] = "CREATE UNIQUE INDEX $name ON `$table` (" . implode( ',', $cols ) . ")"; + } else $q[] = "CREATE INDEX $name ON `$table` ({$index['name']}) "; + } + + $q[] = "INSERT INTO `$table` SELECT * FROM tmp_backup;"; + $q[] = "DROP TABLE tmp_backup;"; + $q[] = "PRAGMA foreign_keys = 1 "; + + foreach ( $q as $sq ) $this->adapter->exec( $sq ); + } + + /** + * Returns the an array describing the indexes for type $type. + * + * @param string $type type to describe indexes of + * + * @return array + */ + protected function getIndexes( $type ) + { + $table = $this->esc( $type, TRUE ); + $indexes = $this->adapter->get( "PRAGMA index_list('$table')" ); + + $indexInfoList = array(); + foreach ( $indexes as $i ) { + $indexInfoList[$i['name']] = $this->adapter->getRow( "PRAGMA index_info('{$i['name']}') " ); + + $indexInfoList[$i['name']]['unique'] = $i['unique']; + } + + return $indexInfoList; + } + + /** + * Adds a foreign key to a type. + * Note: cant put this in try-catch because that can hide the fact + * that database has been damaged. + * + * @param string $type type you want to modify table of + * @param string $targetType target type + * @param string $field field of the type that needs to get the fk + * @param string $targetField field where the fk needs to point to + * @param integer $buildopt 0 = NO ACTION, 1 = ON DELETE CASCADE + * + * @return boolean + */ + protected function buildFK( $type, $targetType, $property, $targetProperty, $constraint = FALSE ) + { + $table = $this->esc( $type, TRUE ); + $targetTable = $this->esc( $targetType, TRUE ); + $column = $this->esc( $property, TRUE ); + $targetColumn = $this->esc( $targetProperty, TRUE ); + + $tables = $this->getTables(); + if ( !in_array( $targetTable, $tables ) ) return FALSE; + + if ( !is_null( $this->getForeignKeyForTypeProperty( $table, $column ) ) ) return FALSE; + $t = $this->getTable( $table ); + $consSQL = ( $constraint ? 'CASCADE' : 'SET NULL' ); + $label = 'from_' . $column . '_to_table_' . $targetTable . '_col_' . $targetColumn; + $t['keys'][$label] = array( + 'table' => $targetTable, + 'from' => $column, + 'to' => $targetColumn, + 'on_update' => $consSQL, + 'on_delete' => $consSQL + ); + $this->putTable( $t ); + return TRUE; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $table = $this->esc( $type, TRUE ); + $keys = $this->adapter->get( "PRAGMA foreign_key_list('$table')" ); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $label, + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + SQLiteT::C_DATATYPE_INTEGER => 'INTEGER', + SQLiteT::C_DATATYPE_NUMERIC => 'NUMERIC', + SQLiteT::C_DATATYPE_TEXT => 'TEXT', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[$v] = $k; + } + + $this->adapter = $adapter; + $this->adapter->setOption( 'setInitQuery', ' PRAGMA foreign_keys = 1 ' ); + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer $const data type to be used for IDS. + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( $value === NULL ) return self::C_DATATYPE_INTEGER; + if ( $value === INF ) return self::C_DATATYPE_TEXT; + + if ( $this->startsWithZeros( $value ) ) return self::C_DATATYPE_TEXT; + + if ( $value === TRUE || $value === FALSE ) return self::C_DATATYPE_INTEGER; + + if ( is_numeric( $value ) && ( intval( $value ) == $value ) && $value < 2147483648 && $value > -2147483648 ) return self::C_DATATYPE_INTEGER; + + if ( ( is_numeric( $value ) && $value < 2147483648 && $value > -2147483648) + || preg_match( '/\d{4}\-\d\d\-\d\d/', $value ) + || preg_match( '/\d{4}\-\d\d\-\d\d\s\d\d:\d\d:\d\d/', $value ) + ) { + return self::C_DATATYPE_NUMERIC; + } + + return self::C_DATATYPE_TEXT; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $table, $column, $type ) + { + $column = $this->check( $column ); + $table = $this->check( $table ); + $type = $this->typeno_sqltype[$type]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'addColumn', $table, $column ), $table, $column, $type ) ); + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : 99 ); + + return $r; + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $column, $datatype ) + { + $t = $this->getTable( $type ); + + $t['columns'][$column] = $this->typeno_sqltype[$datatype]; + + $this->putTable( $t ); + } + + /** + * @see QueryWriter::getTables(); + */ + public function getTables() + { + return $this->adapter->getCol( "SELECT name FROM sqlite_master + WHERE type='table' AND name!='sqlite_sequence';" ); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $type ) + { + $table = $this->esc( $type ); + + $sql = sprintf( $this->getDDLTemplate( 'createTable', $type ), $table ); + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table, TRUE ); + + $columnsRaw = $this->adapter->get( "PRAGMA table_info('$table')" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) $columns[$r['name']] = $r['type']; + + return $columns; + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $name = 'UQ_' . $this->esc( $type, TRUE ) . implode( '__', $properties ); + $t = $this->getTable( $type ); + $t['indexes'][$name] = array( 'name' => $name ); + try { + $this->putTable( $t ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION + ); + if ( $state == 'HY000' + && isset($extraDriverDetails[1]) + && $extraDriverDetails[1] == 1 + && ( in_array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, $list ) + || in_array( QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, $list ) + )) { + return TRUE; + } + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * Sets an SQL snippet to be used for the next queryRecord() operation. + * SQLite has no SELECT-FOR-UPDATE and filters this. + * + * @param string $sql SQL snippet to use in SELECT statement. + * + * return self + */ + public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { + if ( $sqlSelectSnippet === AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE) $sqlSelectSnippet = ''; + $this->sqlSelectSnippet = $sqlSelectSnippet; + return $this; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $column ) + { + $columns = $this->getColumns( $type ); + if ( !isset( $columns[$column] ) ) return FALSE; + + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $column, TRUE ); + + try { + $t = $this->getTable( $type ); + $t['indexes'][$name] = array( 'name' => $column ); + $this->putTable( $t ); + return TRUE; + } catch( SQLException $exception ) { + return FALSE; + } + } + + /** + * @see QueryWriter::wipe + */ + public function wipe( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( "DELETE FROM $table " ); + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) + { + return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDep ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $this->adapter->exec( 'PRAGMA foreign_keys = 0 ' ); + + foreach ( $this->getTables() as $t ) { + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + } + + $this->adapter->exec( 'PRAGMA foreign_keys = 1 ' ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/R.php b/vendor/gabordemooij/redbean/RedBeanPHP/R.php new file mode 100644 index 0000000..6f2648b --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/R.php @@ -0,0 +1,21 @@ +driverDetails; + } + + /** + * @param array $driverDetails + */ + public function setDriverDetails($driverDetails) + { + $this->driverDetails = $driverDetails; + } + + /** + * Returns an ANSI-92 compliant SQL state. + * + * @return string + */ + public function getSQLState() + { + return $this->sqlState; + } + + /** + * Returns the raw SQL STATE, possibly compliant with + * ANSI SQL error codes - but this depends on database driver. + * + * @param string $sqlState SQL state error code + * + * @return void + */ + public function setSQLState( $sqlState ) + { + $this->sqlState = $sqlState; + } + + /** + * To String prints both code and SQL state. + * + * @return string + */ + public function __toString() + { + return '[' . $this->getSQLState() . '] - ' . $this->getMessage()."\n". + 'trace: ' . $this->getTraceAsString(); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Repository.php b/vendor/gabordemooij/redbean/RedBeanPHP/Repository.php new file mode 100644 index 0000000..dd30ecd --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Repository.php @@ -0,0 +1,741 @@ +partialBeans; + $this->partialBeans = $yesNoBeans; + return $oldValue; + } + + /** + * Fully processes a bean and updates the associated records in the database. + * First the bean properties will be grouped as 'embedded' bean, + * addition, deleted 'trash can' or residue. Next, the different groups + * of beans will be processed accordingly and the reference bean (i.e. + * the one that was passed to the method as an argument) will be stored. + * Each type of list (own/shared) has 3 bean processors: + * + * - trashCanProcessor : removes the bean or breaks its association with the current bean + * - additionProcessor : associates the bean with the current one + * - residueProcessor : manages beans in lists that 'remain' but may need to be updated + * + * This method first groups the beans and then calls the + * internal processing methods. + * + * @param OODBBean $bean bean to process + * + * @return void + */ + protected function storeBeanWithLists( OODBBean $bean ) + { + $sharedAdditions = $sharedTrashcan = $sharedresidue = $sharedItems = $ownAdditions = $ownTrashcan = $ownresidue = $embeddedBeans = array(); //Define groups + foreach ( $bean as $property => $value ) { + $value = ( $value instanceof SimpleModel ) ? $value->unbox() : $value; + if ( $value instanceof OODBBean ) { + $this->processEmbeddedBean( $embeddedBeans, $bean, $property, $value ); + $bean->setMeta("sys.typeof.{$property}", $value->getMeta('type')); + } elseif ( is_array( $value ) ) { + foreach($value as &$item) { + $item = ( $item instanceof SimpleModel ) ? $item->unbox() : $item; + } + $originals = $bean->moveMeta( 'sys.shadow.' . $property, array() ); + if ( strpos( $property, 'own' ) === 0 ) { + list( $ownAdditions, $ownTrashcan, $ownresidue ) = $this->processGroups( $originals, $value, $ownAdditions, $ownTrashcan, $ownresidue ); + $listName = lcfirst( substr( $property, 3 ) ); + if ($bean->moveMeta( 'sys.exclusive-'. $listName ) ) { + OODBBean::setMetaAll( $ownTrashcan, 'sys.garbage', TRUE ); + OODBBean::setMetaAll( $ownAdditions, 'sys.buildcommand.fkdependson', $bean->getMeta( 'type' ) ); + } + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + list( $sharedAdditions, $sharedTrashcan, $sharedresidue ) = $this->processGroups( $originals, $value, $sharedAdditions, $sharedTrashcan, $sharedresidue ); + unset( $bean->$property ); + } + } + } + $this->storeBean( $bean ); + $this->processTrashcan( $bean, $ownTrashcan ); + $this->processAdditions( $bean, $ownAdditions ); + $this->processResidue( $ownresidue ); + $this->processSharedTrashcan( $bean, $sharedTrashcan ); + $this->processSharedAdditions( $bean, $sharedAdditions ); + $this->processSharedResidue( $bean, $sharedresidue ); + } + + /** + * Process groups. Internal function. Processes different kind of groups for + * storage function. Given a list of original beans and a list of current beans, + * this function calculates which beans remain in the list (residue), which + * have been deleted (are in the trashcan) and which beans have been added + * (additions). + * + * @param array $originals originals + * @param array $current the current beans + * @param array $additions beans that have been added + * @param array $trashcan beans that have been deleted + * @param array $residue beans that have been left untouched + * + * @return array + */ + protected function processGroups( $originals, $current, $additions, $trashcan, $residue ) + { + return array( + array_merge( $additions, array_diff( $current, $originals ) ), + array_merge( $trashcan, array_diff( $originals, $current ) ), + array_merge( $residue, array_intersect( $current, $originals ) ) + ); + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles shared addition lists; i.e. + * the $bean->sharedObject properties. + * Shared beans will be associated with eachother using the + * Association Manager. + * + * @param OODBBean $bean the bean + * @param array $sharedAdditions list with shared additions + * + * @return void + */ + protected function processSharedAdditions( $bean, $sharedAdditions ) + { + foreach ( $sharedAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + $this->oodb->getAssociationManager()->associate( $addition, $bean ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles own lists; i.e. + * the $bean->ownObject properties. + * A residue is a bean in an own-list that stays + * where it is. This method checks if there have been any + * modification to this bean, in that case + * the bean is stored once again, otherwise the bean will be left untouched. + * + * @param array $ownresidue list to process + * + * @return void + */ + protected function processResidue( $ownresidue ) + { + foreach ( $ownresidue as $residue ) { + if ( $residue->getMeta( 'tainted' ) ) { + $this->store( $residue ); + } + } + } + + /** + * Processes a list of beans from a bean. A bean may contain lists. This + * method handles own lists; i.e. the $bean->ownObject properties. + * A trash can bean is a bean in an own-list that has been removed + * (when checked with the shadow). This method + * checks if the bean is also in the dependency list. If it is the bean will be removed. + * If not, the connection between the bean and the owner bean will be broken by + * setting the ID to NULL. + * + * @param OODBBean $bean bean to process + * @param array $ownTrashcan list to process + * + * @return void + */ + protected function processTrashcan( $bean, $ownTrashcan ) + { + foreach ( $ownTrashcan as $trash ) { + + $myFieldLink = $bean->getMeta( 'type' ) . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + if ( $trash->getMeta( 'sys.garbage' ) === TRUE ) { + $this->trash( $trash ); + } else { + $trash->$myFieldLink = NULL; + $this->store( $trash ); + } + } + } + + /** + * Unassociates the list items in the trashcan. + * This bean processor processes the beans in the shared trash can. + * This group of beans has been deleted from a shared list. + * The affected beans will no longer be associated with the bean + * that contains the shared list. + * + * @param OODBBean $bean bean to process + * @param array $sharedTrashcan list to process + * + * @return void + */ + protected function processSharedTrashcan( $bean, $sharedTrashcan ) + { + foreach ( $sharedTrashcan as $trash ) { + $this->oodb->getAssociationManager()->unassociate( $trash, $bean ); + } + } + + /** + * Stores all the beans in the residue group. + * This bean processor processes the beans in the shared residue + * group. This group of beans 'remains' in the list but might need + * to be updated or synced. The affected beans will be stored + * to perform the required database queries. + * + * @param OODBBean $bean bean to process + * @param array $sharedresidue list to process + * + * @return void + */ + protected function processSharedResidue( $bean, $sharedresidue ) + { + foreach ( $sharedresidue as $residue ) { + $this->store( $residue ); + } + } + + /** + * Determines whether the bean has 'loaded lists' or + * 'loaded embedded beans' that need to be processed + * by the store() method. + * + * @param OODBBean $bean bean to be examined + * + * @return boolean + */ + protected function hasListsOrObjects( OODBBean $bean ) + { + $processLists = FALSE; + foreach ( $bean as $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $processLists = TRUE; + break; + } + } + + return $processLists; + } + + /** + * Converts an embedded bean to an ID, removes the bean property and + * stores the bean in the embedded beans array. The id will be + * assigned to the link field property, i.e. 'bean_id'. + * + * @param array $embeddedBeans destination array for embedded bean + * @param OODBBean $bean target bean to process + * @param string $property property that contains the embedded bean + * @param OODBBean $value embedded bean itself + * + * @return void + */ + protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value ) + { + $linkField = $property . '_id'; + if ( !$value->id || $value->getMeta( 'tainted' ) ) { + $this->store( $value ); + } + $id = $value->id; + if ($bean->$linkField != $id) $bean->$linkField = $id; + $bean->setMeta( 'cast.' . $linkField, 'id' ); + $embeddedBeans[$linkField] = $value; + unset( $bean->$property ); + } + + /** + * Constructor, requires a query writer and OODB. + * Creates a new instance of the bean respository class. + * + * @param OODB $oodb instance of object database + * @param QueryWriter $writer the Query Writer to use for this repository + * + * @return void + */ + public function __construct( OODB $oodb, QueryWriter $writer ) + { + $this->writer = $writer; + $this->oodb = $oodb; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. To be valid a bean + * must abide to the following rules: + * + * - It must have an primary key id property named: id + * - It must have a type + * - The type must conform to the RedBeanPHP naming policy + * - All properties must be valid + * - All values must be valid + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + //Is all meta information present? + if ( !isset( $bean->id ) ) { + throw new RedException( 'Bean has incomplete Meta Information id ' ); + } + if ( !( $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean has incomplete Meta Information II' ); + } + //Pattern of allowed characters + $pattern = '/[^a-z0-9_]/i'; + //Does the type contain invalid characters? + if ( preg_match( $pattern, $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean Type is invalid' ); + } + //Are the properties and values valid? + foreach ( $bean as $prop => $value ) { + if ( + is_array( $value ) + || ( is_object( $value ) ) + ) { + throw new RedException( "Invalid Bean value: property $prop" ); + } else if ( + strlen( $prop ) < 1 + || preg_match( $pattern, $prop ) + ) { + throw new RedException( "Invalid Bean property: property $prop" ); + } + } + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * To use a different class for beans (instead of OODBBean) set: + * REDBEAN_OODBBEAN_CLASS to the name of the class to be used. + * + * @param string $type type of bean you want to dispense + * @param int $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + $OODBBEAN = defined( 'REDBEAN_OODBBEAN_CLASS' ) ? REDBEAN_OODBBEAN_CLASS : '\RedBeanPHP\OODBBean'; + $beans = array(); + for ( $i = 0; $i < $number; $i++ ) { + $bean = new $OODBBEAN; + $bean->initializeForDispense( $type, $this->oodb->getBeanHelper() ); + $this->check( $bean ); + $this->oodb->signal( 'dispense', $bean ); + $beans[] = $bean; + } + + return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans; + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + //for backward compatibility, allow mismatch arguments: + if ( is_array( $sql ) ) { + if ( isset( $sql[1] ) ) { + $bindings = $sql[1]; + } + $sql = $sql[0]; + } + try { + $beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) ); + + return $beans; + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + + return array(); + } + + /** + * Finds a BeanCollection. + * Given a type, an SQL snippet and optionally some parameter bindings + * this methods returns a BeanCollection for your query. + * + * The BeanCollection represents a collection of beans and + * makes it possible to use database cursors. The BeanCollection + * has a method next() to obtain the first, next and last bean + * in the collection. The BeanCollection does not implement the array + * interface nor does it try to act like an array because it cannot go + * backward or rewind itself. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + try { + $cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings ); + return new BeanCollection( $type, $this, $cursor ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + return new BeanCollection( $type, $this, new NullCursor ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $processLists = $this->hasListsOrObjects( $bean ); + if ( !$processLists && !$bean->getMeta( 'tainted' ) ) { + return $bean->getID(); //bail out! + } + $this->oodb->signal( 'update', $bean ); + $processLists = $this->hasListsOrObjects( $bean ); //check again, might have changed by model! + if ( $processLists ) { + $this->storeBeanWithLists( $bean ); + } else { + $this->storeBean( $bean ); + } + $this->oodb->signal( 'after_update', $bean ); + + return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id; + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + if ( !$ids ) { + return array(); + } + $collection = array(); + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) ); + } catch ( SQLException $e ) { + $this->handleException( $e ); + $rows = FALSE; + } + $this->stash[$this->nesting] = array(); + if ( !$rows ) { + return array(); + } + foreach ( $rows as $row ) { + $this->stash[$this->nesting][$row['id']] = $row; + } + foreach ( $ids as $id ) { + $collection[$id] = $this->load( $type, $id ); + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask meta mask to apply (optional) + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = '__meta' ) + { + $masktype = gettype( $mask ); + switch ( $masktype ) { + case 'string': + break; + case 'array': + $maskflip = array(); + foreach ( $mask as $m ) { + if ( !is_string( $m ) ) { + $mask = NULL; + $masktype = 'NULL'; + break 2; + } + $maskflip[$m] = TRUE; + } + $mask = $maskflip; + break; + default: + $mask = NULL; + $masktype = 'NULL'; + } + + $collection = array(); + $this->stash[$this->nesting] = array(); + foreach ( $rows as $row ) { + if ( $mask !== NULL ) { + $meta = array(); + foreach( $row as $key => $value ) { + if ( $masktype === 'string' ) { + if ( strpos( $key, $mask ) === 0 ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } elseif ( $masktype === 'array' ) { + if ( isset( $mask[$key] ) ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } + } + } + + $id = $row['id']; + $this->stash[$this->nesting][$id] = $row; + $collection[$id] = $this->load( $type, $id ); + + if ( $mask !== NULL ) { + $collection[$id]->setMeta( 'data.bundle', $meta ); + } + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + $type = AQueryWriter::camelsSnake( $type ); + if ( count( explode( '_', $type ) ) > 2 ) { + throw new RedException( 'Invalid type for count.' ); + } + + try { + $count = (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + $count = 0; + } + return $count; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $this->oodb->signal( 'delete', $bean ); + foreach ( $bean as $property => $value ) { + if ( $value instanceof OODBBean ) { + unset( $bean->$property ); + } + if ( is_array( $value ) ) { + if ( strpos( $property, 'own' ) === 0 ) { + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + unset( $bean->$property ); + } + } + } + try { + $deleted = $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + $bean->id = 0; + $this->oodb->signal( 'after_delete', $bean ); + return isset($deleted) ? $deleted : 0; + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->writer->tableExists( $table ); + } + + /** + * Trash all beans of a given type. + * Wipes an entire type of bean. After this operation there + * will be no beans left of the specified type. + * This method will ignore exceptions caused by database + * tables that do not exist. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + try { + $this->writer->wipe( $type ); + + return TRUE; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ), $exception->getDriverDetails() ) ) { + throw $exception; + } + + return FALSE; + } + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Fluid.php b/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Fluid.php new file mode 100644 index 0000000..77ffdc5 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Fluid.php @@ -0,0 +1,323 @@ +writer->scanType( 'STRING' ); + } elseif ( $cast == 'id' ) { + $typeno = $this->writer->getTypeForID(); + } elseif ( isset( $this->writer->sqltype_typeno[$cast] ) ) { + $typeno = $this->writer->sqltype_typeno[$cast]; + } else { + throw new RedException( 'Invalid Cast' ); + } + + return $typeno; + } + + /** + * Orders the Query Writer to create a table if it does not exist already and + * adds a note in the build report about the creation. + * + * @param OODBBean $bean bean to update report of + * @param string $table table to check and create if not exists + * + * @return void + */ + private function createTableIfNotExists( OODBBean $bean, $table ) + { + //Does table exist? If not, create + if ( !$this->tableExists( $this->writer->esc( $table, TRUE ) ) ) { + $this->writer->createTable( $table ); + $bean->setMeta( 'buildreport.flags.created', TRUE ); + } + } + + /** + * Modifies the table to fit the bean data. + * Given a property and a value and the bean, this method will + * adjust the table structure to fit the requirements of the property and value. + * This may include adding a new column or widening an existing column to hold a larger + * or different kind of value. This method employs the writer to adjust the table + * structure in the database. Schema updates are recorded in meta properties of the bean. + * + * This method will also apply indexes, unique constraints and foreign keys. + * + * @param OODBBean $bean bean to get cast data from and store meta in + * @param string $property property to store + * @param mixed $value value to store + * + * @return void + */ + private function modifySchema( OODBBean $bean, $property, $value, &$columns = NULL ) + { + $doFKStuff = FALSE; + $table = $bean->getMeta( 'type' ); + if ($columns === NULL) { + $columns = $this->writer->getColumns( $table ); + } + $columnNoQ = $this->writer->esc( $property, TRUE ); + if ( !$this->oodb->isChilled( $bean->getMeta( 'type' ) ) ) { + if ( $bean->getMeta( "cast.$property", -1 ) !== -1 ) { //check for explicitly specified types + $cast = $bean->getMeta( "cast.$property" ); + $typeno = $this->getTypeFromCast( $cast ); + } else { + $cast = FALSE; + $typeno = $this->writer->scanType( $value, TRUE ); + } + if ( isset( $columns[$this->writer->esc( $property, TRUE )] ) ) { //Is this property represented in the table ? + if ( !$cast ) { //rescan without taking into account special types >80 + $typeno = $this->writer->scanType( $value, FALSE ); + } + $sqlt = $this->writer->code( $columns[$this->writer->esc( $property, TRUE )] ); + if ( $typeno > $sqlt ) { //no, we have to widen the database column type + $this->writer->widenColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.widen', TRUE ); + $doFKStuff = TRUE; + } + } else { + $this->writer->addColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.addcolumn', TRUE ); + $doFKStuff = TRUE; + } + if ($doFKStuff) { + if (strrpos($columnNoQ, '_id')===(strlen($columnNoQ)-3)) { + $destinationColumnNoQ = substr($columnNoQ, 0, strlen($columnNoQ)-3); + $indexName = "index_foreignkey_{$table}_{$destinationColumnNoQ}"; + $this->writer->addIndex($table, $indexName, $columnNoQ); + $typeof = $bean->getMeta("sys.typeof.{$destinationColumnNoQ}", $destinationColumnNoQ); + $isLink = $bean->getMeta( 'sys.buildcommand.unique', FALSE ); + //Make FK CASCADING if part of exclusive list (dependson=typeof) or if link bean + $isDep = ( $bean->moveMeta( 'sys.buildcommand.fkdependson' ) === $typeof || is_array( $isLink ) ); + $result = $this->writer->addFK( $table, $typeof, $columnNoQ, 'id', $isDep ); + //If this is a link bean and all unique columns have been added already, then apply unique constraint + if ( is_array( $isLink ) && !count( array_diff( $isLink, array_keys( $this->writer->getColumns( $table ) ) ) ) ) { + $this->writer->addUniqueConstraint( $table, $bean->moveMeta('sys.buildcommand.unique') ); + $bean->setMeta("sys.typeof.{$destinationColumnNoQ}", NULL); + } + } + } + } + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + + if ($alias) { + $addition->setMeta( "sys.typeof.{$alias}", $beanType ); + } else { + $addition->setMeta( "sys.typeof.{$beanType}", $beanType ); + } + + $this->store( $addition ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() + * method. When all lists and embedded beans (parent objects) have been processed and + * removed from the original bean the bean is passed to this method to be stored + * in the database. + * + * @param OODBBean $bean the clean bean + * + * @return void + */ + protected function storeBean( OODBBean $bean ) + { + if ( $bean->getMeta( 'changed' ) ) { + $this->check( $bean ); + $table = $bean->getMeta( 'type' ); + $this->createTableIfNotExists( $bean, $table ); + + $updateValues = array(); + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + $columnCache = NULL; + foreach ( $bean as $property => $value ) { + if ( $partial && !in_array( $property, $mask ) ) continue; + if ( $property !== 'id' ) { + $this->modifySchema( $bean, $property, $value, $columnCache ); + } + if ( $property !== 'id' ) { + $updateValues[] = array( 'property' => $property, 'value' => $value ); + } + } + + $bean->id = $this->writer->updateRecord( $table, $updateValues, $bean->id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + protected function handleException( \Exception $exception ) + { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + } catch ( SQLException $exception ) { + if ( + $this->writer->sqlStateIn( + $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), + $exception->getDriverDetails() + ) + ) { + $rows = array(); + } else { + throw $exception; + } + } + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Frozen.php b/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Frozen.php new file mode 100644 index 0000000..f07eb69 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Repository/Frozen.php @@ -0,0 +1,176 @@ +getMeta( 'changed' ) ) { + + list( $properties, $table ) = $bean->getPropertiesAndType(); + $id = $properties['id']; + unset($properties['id']); + $updateValues = array(); + $k1 = 'property'; + $k2 = 'value'; + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + foreach( $properties as $key => $value ) { + if ( $partial && !in_array( $key, $mask ) ) continue; + $updateValues[] = array( $k1 => $key, $k2 => $value ); + } + $bean->id = $this->writer->updateRecord( $table, $updateValues, $id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + * @throws RedException + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + $cachedIndex = array(); + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + $this->store( $addition ); + + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + * @throws SQLException + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModel.php b/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModel.php new file mode 100644 index 0000000..3cc9dea --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModel.php @@ -0,0 +1,127 @@ +bean = $bean; + } + + /** + * Magic Getter to make the bean properties available from + * the $this-scope. + * + * @note this method returns a value, not a reference! + * To obtain a reference unbox the bean first! + * + * @param string $prop property to get + * + * @return mixed + */ + public function __get( $prop ) + { + return $this->bean->$prop; + } + + /** + * Magic Setter. + * Sets the value directly as a bean property. + * + * @param string $prop property to set value of + * @param mixed $value value to set + * + * @return void + */ + public function __set( $prop, $value ) + { + $this->bean->$prop = $value; + } + + /** + * Isset implementation. + * Implements the isset function for array-like access. + * + * @param string $key key to check + * + * @return boolean + */ + public function __isset( $key ) + { + return isset( $this->bean->$key ); + } + + /** + * Box the bean using the current model. + * This method wraps the current bean in this model. + * This method can be reached using FUSE through a simple + * OODBBean. The method returns a RedBeanPHP Simple Model. + * This is useful if you would like to rely on PHP type hinting. + * You can box your beans before passing them to functions or methods + * with typed parameters. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return SimpleModel + */ + public function box() + { + return $this; + } + + /** + * Unbox the bean from the model. + * This method returns the bean inside the model. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return OODBBean + */ + public function unbox() + { + return $this->bean; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModelHelper.php b/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModelHelper.php new file mode 100644 index 0000000..94ff615 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/SimpleModelHelper.php @@ -0,0 +1,71 @@ +$eventName(); + } + + /** + * Attaches the FUSE event listeners. Now the Model Helper will listen for + * CRUD events. If a CRUD event occurs it will send a signal to the model + * that belongs to the CRUD bean and this model will take over control from + * there. This method will attach the following event listeners to the observable: + * + * - 'update' (gets called by R::store, before the records gets inserted / updated) + * - 'after_update' (gets called by R::store, after the records have been inserted / updated) + * - 'open' (gets called by R::load, after the record has been retrieved) + * - 'delete' (gets called by R::trash, before deletion of record) + * - 'after_delete' (gets called by R::trash, after deletion) + * - 'dispense' (gets called by R::dispense) + * + * For every event type, this method will register this helper as a listener. + * The observable will notify the listener (this object) with the event ID and the + * affected bean. This helper will then process the event (onEvent) by invoking + * the event on the bean. If a bean offers a method with the same name as the + * event ID, this method will be invoked. + * + * @param Observable $observable object to observe + * + * @return void + */ + public function attachEventListeners( Observable $observable ) + { + foreach ( array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ) as $eventID ) { + $observable->addEventListener( $eventID, $this ); + } + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/TagManager.php b/vendor/gabordemooij/redbean/RedBeanPHP/TagManager.php new file mode 100644 index 0000000..614d0ff --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/TagManager.php @@ -0,0 +1,370 @@ +redbean->find( 'tag', array( 'title' => array( $title ) ) ); + + if ( $beans ) { + $bean = reset( $beans ); + + return $bean; + } + + return NULL; + } + + /** + * Constructor. + * The tag manager offers an easy way to quickly implement basic tagging + * functionality. + * + * @param ToolBox $toolbox toolbox object + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public function hasTag( $bean, $tags, $all = FALSE ) + { + $foundtags = $this->tag( $bean ); + + $tags = $this->extractTagsIfNeeded( $tags ); + $same = array_intersect( $tags, $foundtags ); + + if ( $all ) { + return ( implode( ',', $same ) === implode( ',', $tags ) ); + } + + return (bool) ( count( $same ) > 0 ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public function untag( $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + foreach ( $tags as $tag ) { + if ( $t = $this->findTagByTitle( $tag ) ) { + $this->associationManager->unassociate( $bean, $t ); + } + } + } + + /** + * Part of RedBeanPHP Tagging API. + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public function tag( OODBBean $bean, $tagList = NULL ) + { + if ( is_null( $tagList ) ) { + + $tags = $bean->sharedTag; + $foundTags = array(); + + foreach ( $tags as $tag ) { + $foundTags[] = $tag->title; + } + + return $foundTags; + } + + $this->associationManager->clearRelations( $bean, 'tag' ); + $this->addTags( $bean, $tagList ); + + return $tagList; + } + + /** + * Part of RedBeanPHP Tagging API. + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public function addTags( OODBBean $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + if ( $tagList === FALSE ) { + return; + } + + foreach ( $tags as $tag ) { + if ( !$t = $this->findTagByTitle( $tag ) ) { + $t = $this->redbean->dispense( 'tag' ); + $t->title = $tag; + + $this->redbean->store( $t ); + } + + $this->associationManager->associate( $bean, $t ); + } + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, FALSE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, TRUE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Like taggedAll() but only counts. + * + * @see taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, TRUE, $sql, $bindings ); + } + + /** + * Like tagged() but only counts. + * + * @see tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, FALSE, $sql, $bindings ); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/ToolBox.php b/vendor/gabordemooij/redbean/RedBeanPHP/ToolBox.php new file mode 100644 index 0000000..fbd073c --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/ToolBox.php @@ -0,0 +1,167 @@ + + * $toolbox = new ToolBox( $oodb, $adapter, $writer ); + * $plugin = new MyPlugin( $toolbox ); + * + * + * The example above illustrates how the toolbox is used. + * The core objects are passed to the ToolBox constructor to + * assemble a toolbox instance. The toolbox is then passed to + * the plugin, helper or manager object. Instances of + * TagManager, AssociationManager and so on are examples of + * this, they all require a toolbox. The toolbox can also + * be obtained from the facade using: R::getToolBox(); + * + * @param OODB $oodb Object Database, OODB + * @param DBAdapter $adapter Database Adapter + * @param QueryWriter $writer Query Writer + */ + public function __construct( OODB $oodb, Adapter $adapter, QueryWriter $writer ) + { + $this->oodb = $oodb; + $this->adapter = $adapter; + $this->writer = $writer; + return $this; + } + + /** + * Returns the query writer in this toolbox. + * The Query Writer is responsible for building the queries for a + * specific database and executing them through the adapter. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return QueryWriter + */ + public function getWriter() + { + return $this->writer; + } + + /** + * Returns the OODB instance in this toolbox. + * OODB is responsible for creating, storing, retrieving and deleting + * single beans. Other components rely + * on OODB for their basic functionality. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return OODB + */ + public function getRedBean() + { + return $this->oodb; + } + + /** + * Returns the database adapter in this toolbox. + * The adapter is responsible for executing the query and binding the values. + * The adapter also takes care of transaction handling. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return DBAdapter + */ + public function getDatabaseAdapter() + { + return $this->adapter; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/ArrayTool.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/ArrayTool.php new file mode 100644 index 0000000..c8038b6 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/ArrayTool.php @@ -0,0 +1,89 @@ + + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + $str = count( $array ) ? implode( ',', array_fill( 0, count( $array ), '?' ) ) : ''; + return ( is_null( $template ) || $str === '' ) ? $str : sprintf( $template, $str ); + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + foreach( $array as $value ) { + if ( is_array( $value ) ) $result = self::flat( $value, $result ); + else $result[] = $value; + } + return $result; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Diff.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Diff.php new file mode 100644 index 0000000..491e5f5 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Diff.php @@ -0,0 +1,116 @@ +toolbox = $toolbox; + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $beans reference beans + * @param OODBBean|array $others beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public function diff( $beans, $others, $filters = array( 'created', 'modified' ), $format = '%s.%s.%s', $type = NULL ) + { + $diff = array(); + + if ( !is_array( $beans ) ) $beans = array( $beans ); + $beansI = array(); + foreach ( $beans as $bean ) { + if ( !( $bean instanceof OODBBean ) ) continue; + $beansI[$bean->id] = $bean; + } + + if ( !is_array( $others ) ) $others = array( $others ); + $othersI = array(); + foreach ( $others as $other ) { + if ( !( $other instanceof OODBBean ) ) continue; + $othersI[$other->id] = $other; + } + + if ( count( $beansI ) == 0 || count( $othersI ) == 0 ) { + return array(); + } + + $type = $type != NULL ? $type : reset($beansI)->getMeta( 'type' ); + + foreach( $beansI as $id => $bean ) { + if ( !isset( $othersI[$id] ) ) continue; + $other = $othersI[$id]; + foreach( $bean as $property => $value ) { + if ( in_array( $property, $filters ) ) continue; + $key = vsprintf( $format, array( $type, $bean->id, $property ) ); + $compare = $other->{$property}; + if ( !is_object( $value ) && !is_array( $value ) && $value != $compare ) { + $diff[$key] = array( $value, $compare ); + } else { + $diff = array_merge( $diff, $this->diff( $value, $compare, $filters, $format, $key ) ); + } + } + } + + return $diff; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/DispenseHelper.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/DispenseHelper.php new file mode 100644 index 0000000..83eebcf --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/DispenseHelper.php @@ -0,0 +1,209 @@ + + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( OODB $oodb, $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) { + + if ( is_array($typeOrBeanArray) ) { + + if ( !isset( $typeOrBeanArray['_type'] ) ) { + $list = array(); + foreach( $typeOrBeanArray as $beanArray ) { + if ( + !( is_array( $beanArray ) + && isset( $beanArray['_type'] ) ) ) { + throw new RedException( 'Invalid Array Bean' ); + } + } + foreach( $typeOrBeanArray as $beanArray ) $list[] = self::dispense( $oodb, $beanArray ); + return $list; + } + + $import = $typeOrBeanArray; + $type = $import['_type']; + unset( $import['_type'] ); + } else { + $type = $typeOrBeanArray; + } + + if (self::$enforceNamingPolicy) self::checkType( $type ); + + $beanOrBeans = $oodb->dispense( $type, $num, $alwaysReturnArray ); + + if ( isset( $import ) ) { + $beanOrBeans->import( $import ); + } + + return $beanOrBeans; + } + + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param OODB $oodb OODB + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( OODB $oodb, $order, $onlyArrays = FALSE ) + { + $list = array(); + + foreach( explode( ',', $order ) as $order ) { + if ( strpos( $order, '*' ) !== FALSE ) { + list( $type, $amount ) = explode( '*', $order ); + } else { + $type = $order; + $amount = 1; + } + + $list[] = self::dispense( $oodb, $type, $amount, $onlyArrays ); + } + + return $list; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Dump.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Dump.php new file mode 100644 index 0000000..1b428a3 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Dump.php @@ -0,0 +1,70 @@ + + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + $array = array(); + if ( $data instanceof OODBBean ) { + $str = strval( $data ); + if (strlen($str) > 35) { + $beanStr = substr( $str, 0, 35 ).'... '; + } else { + $beanStr = $str; + } + return $beanStr; + } + if ( is_array( $data ) ) { + foreach( $data as $key => $item ) { + $array[$key] = self::dump( $item ); + } + } + return $array; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Feature.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Feature.php new file mode 100644 index 0000000..f19c538 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Feature.php @@ -0,0 +1,108 @@ + + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function feature( $label ) { + switch( $label ) { + case self::C_FEATURE_NOVICE_LATEST: + case self::C_FEATURE_NOVICE_5_4: + case self::C_FEATURE_NOVICE_5_5: + OODBBean::useFluidCount( FALSE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_LATEST: + case self::C_FEATURE_5_4: + case self::C_FEATURE_5_5: + OODBBean::useFluidCount( FALSE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( TRUE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_NOVICE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_ORIGINAL: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + default: + throw new \Exception("Unknown feature set label."); + break; + } + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Look.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Look.php new file mode 100644 index 0000000..2fefe0a --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Look.php @@ -0,0 +1,109 @@ +toolbox = $toolbox; + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + $adapter = $this->toolbox->getDatabaseAdapter(); + $lines = array(); + $rows = $adapter->get( $sql, $bindings ); + foreach( $rows as $row ) { + $values = array(); + foreach( $keys as $key ) { + if (!empty($filter)) { + $values[] = call_user_func_array( $filter, array( $row[$key] ) ); + } else { + $values[] = $row[$key]; + } + } + $lines[] = vsprintf( $template, $values ); + } + $string = implode( $glue, $lines ); + return $string; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/MatchUp.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/MatchUp.php new file mode 100644 index 0000000..d000ded --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/MatchUp.php @@ -0,0 +1,118 @@ +toolbox = $toolbox; + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * Usage (this example resets a password in one go): + * + * + * $newpass = '1234'; + * $didResetPass = R::matchUp( + * 'account', ' token = ? AND tokentime > ? ', + * [ $token, time()-100 ], + * [ 'pass' => $newpass, 'token' => '' ], + * NULL, + * $account ); + * + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) + { + $finder = new Finder( $this->toolbox ); + $oodb = $this->toolbox->getRedBean(); + $bean = $finder->findOne( $type, $sql, $bindings ); + if ( $bean && $onFoundDo ) { + foreach( $onFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return TRUE; + } + if ( $onNotFoundDo ) { + $bean = $oodb->dispense( $type ); + foreach( $onNotFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return FALSE; + } + return NULL; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/MultiLoader.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/MultiLoader.php new file mode 100644 index 0000000..2ca38e8 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/MultiLoader.php @@ -0,0 +1,58 @@ + + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param OODB $oodb OODB object + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function load( OODB $oodb, $types, $id ) + { + if ( is_string( $types ) ) $types = explode( ',', $types ); + if ( !is_array( $types ) ) return array(); + foreach ( $types as $k => $typeItem ) { + $types[$k] = $oodb->load( $typeItem, $id ); + } + return $types; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/QuickExport.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/QuickExport.php new file mode 100644 index 0000000..0e2cc8a --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/QuickExport.php @@ -0,0 +1,126 @@ +toolbox = $toolbox; + } + + /** + * Makes csv() testable. + */ + public static function operation( $name, $arg1, $arg2 = TRUE ) { + $out = ''; + switch( $name ) { + case 'test': + self::$test = (boolean) $arg1; + break; + case 'header': + $out = ( self::$test ) ? $arg1 : header( $arg1, $arg2 ); + break; + case 'readfile': + $out = ( self::$test ) ? file_get_contents( $arg1 ) : readfile( $arg1 ); + break; + case 'exit': + $out = ( self::$test ) ? 'exit' : exit(); + break; + } + return $out; + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE, $options = array(',','"','\\') ) + { + list( $delimiter, $enclosure, $escapeChar ) = $options; + $path = sprintf( $path, date('Ymd_his') ); + $handle = fopen( $path, 'w' ); + if ($columns) if (PHP_VERSION_ID>=505040) fputcsv($handle, $columns, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $columns, $delimiter, $enclosure ); + $cursor = $this->toolbox->getDatabaseAdapter()->getCursor( $sql, $bindings ); + while( $row = $cursor->getNextItem() ) { + if (PHP_VERSION_ID>=505040) fputcsv($handle, $row, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $row, $delimiter, $enclosure ); + } + fclose($handle); + if ( $output ) { + $file = basename($path); + $out = self::operation('header',"Pragma: public"); + $out .= self::operation('header',"Expires: 0"); + $out .= self::operation('header',"Cache-Control: must-revalidate, post-check=0, pre-check=0"); + $out .= self::operation('header',"Cache-Control: private", FALSE ); + $out .= self::operation('header',"Content-Type: text/csv"); + $out .= self::operation('header',"Content-Disposition: attachment; filename={$file}" ); + $out .= self::operation('header',"Content-Transfer-Encoding: binary"); + $out .= self::operation('readfile',$path ); + @unlink( $path ); + self::operation('exit', FALSE); + return $out; + } + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Transaction.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Transaction.php new file mode 100644 index 0000000..35e70f8 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Transaction.php @@ -0,0 +1,90 @@ + + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param Adapter $adapter Database Adapter providing transaction mechanisms. + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( Adapter $adapter, $callback ) + { + if ( !is_callable( $callback ) ) { + throw new RedException( 'R::transaction needs a valid callback.' ); + } + + static $depth = 0; + $result = null; + try { + if ( $depth == 0 ) { + $adapter->startTransaction(); + } + $depth++; + $result = call_user_func( $callback ); //maintain 5.2 compatibility + $depth--; + if ( $depth == 0 ) { + $adapter->commit(); + } + } catch ( \Exception $exception ) { + $depth--; + if ( $depth == 0 ) { + $adapter->rollback(); + } + throw $exception; + } + return $result; + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/Util/Tree.php b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Tree.php new file mode 100644 index 0000000..dbd5874 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/Util/Tree.php @@ -0,0 +1,212 @@ +toolbox = $toolbox; + $this->writer = $toolbox->getWriter(); + $this->oodb = $toolbox->getRedBean(); + } + + /** + * Returns all child beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * Usage: + * + * + * $newsArticles = R::children( $newsPage, ' ORDER BY title ASC ' ) + * $newsArticles = R::children( $newsPage, ' WHERE title = ? ', [ $t ] ); + * $newsArticles = R::children( $newsPage, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Returns all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $newsPages = R::parents( $newsArticle, ' ORDER BY title ASC ' ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Counts all children beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countChildren( $newsArticle ); + * $count = R::countChildren( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countChildren( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } + + /** + * Counts all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countParents( $newsArticle ); + * $count = R::countParents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countParents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * Note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } +} diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/license.txt b/vendor/gabordemooij/redbean/RedBeanPHP/license.txt new file mode 100644 index 0000000..16949b5 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/license.txt @@ -0,0 +1,316 @@ + +RedBeanPHP +Written by Gabor de Mooij + +RedBean is DUAL Licensed New BSD and GPLv2. You may choose the license that fits +best for your project. + +New BSD License + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +* Neither the name of RedBeanPHP nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY GABOR DE MOOIJ ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL GABOR DE MOOIJ BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +RedBeanPHP is Written by Gabor de Mooij (G.J.G.T de Mooij) Copyright (c) 2018. + + +GPLv2 LICENSE + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/vendor/gabordemooij/redbean/RedBeanPHP/loader.php b/vendor/gabordemooij/redbean/RedBeanPHP/loader.php new file mode 100644 index 0000000..63534c9 --- /dev/null +++ b/vendor/gabordemooij/redbean/RedBeanPHP/loader.php @@ -0,0 +1,85 @@ +=5.3.4" + }, + "autoload": { + "psr-4": { + "RedBeanPHP\\" : "RedBeanPHP" + } + } +} diff --git a/vendor/gabordemooij/redbean/p533patch.php b/vendor/gabordemooij/redbean/p533patch.php new file mode 100644 index 0000000..9fe96cd --- /dev/null +++ b/vendor/gabordemooij/redbean/p533patch.php @@ -0,0 +1,19 @@ + 0) { + echo 'Applied patch for PHP < 5.3.3'; + echo PHP_EOL; + exit; +} else { + echo 'Somthing went wrong.'; + echo PHP_EOL; + exit; +} diff --git a/vendor/gabordemooij/redbean/replica2-win.php b/vendor/gabordemooij/redbean/replica2-win.php new file mode 100644 index 0000000..01f1928 --- /dev/null +++ b/vendor/gabordemooij/redbean/replica2-win.php @@ -0,0 +1,34 @@ +buildFromDirectory('./build'); +echo "Done.\n"; + +echo "Adding stub... "; +$phar->setStub($phar->createDefaultStub("loader.php")); +echo "Done.\n"; + +echo "Your PHAR file has been generated.\n"; diff --git a/vendor/gabordemooij/redbean/replica2.php b/vendor/gabordemooij/redbean/replica2.php new file mode 100644 index 0000000..f9cace4 --- /dev/null +++ b/vendor/gabordemooij/redbean/replica2.php @@ -0,0 +1,121 @@ +#!/usr/bin/env php + $code, 'rb-mysql.php' => $codeMySQL, 'rb-postgres.php' => $codePostgres, 'rb-sqlite.php' => $codeSQLite ); +foreach( $files as $file => $content ) { + echo 'Okay, seems we have all the code.. now writing file: ', $file ,PHP_EOL; + $b = file_put_contents($file, $content); + echo 'Written: ',$b,' bytes.',PHP_EOL; + if ($b > 0) { + echo 'Done!' ,PHP_EOL; + } else { + echo 'Hm, something seems to have gone wrong... ',PHP_EOL; + } +} + + + diff --git a/vendor/gabordemooij/redbean/run_all_tests.sh b/vendor/gabordemooij/redbean/run_all_tests.sh new file mode 100755 index 0000000..242c8cf --- /dev/null +++ b/vendor/gabordemooij/redbean/run_all_tests.sh @@ -0,0 +1,6 @@ +#!/bin/sh +php replica2.php +cp rb.php testing/cli/testcontainer/rb.php +cd testing +cd cli +php runtests.php diff --git a/vendor/gabordemooij/redbean/run_single_test.sh b/vendor/gabordemooij/redbean/run_single_test.sh new file mode 100755 index 0000000..92015d4 --- /dev/null +++ b/vendor/gabordemooij/redbean/run_single_test.sh @@ -0,0 +1,11 @@ +#!/bin/sh +if [ -z "$1" ] +then + echo "Please enter the name of a test suite, example: Blackhole/Version" + exit +fi +php replica2.php +cp rb.php testing/cli/testcontainer/rb.php +cd testing +cd cli +php runtests.php $1 diff --git a/vendor/gabordemooij/redbean/test-dist.ini b/vendor/gabordemooij/redbean/test-dist.ini new file mode 100644 index 0000000..8de9b9b --- /dev/null +++ b/vendor/gabordemooij/redbean/test-dist.ini @@ -0,0 +1,17 @@ +; Test suite database config +; Rename this file to test.ini if you are done + +[mysql] +host = "localhost" +schema = "oodb" +user = "root" +pass = "password" + +[pgsql] +host = "localhost" +schema = "oodb" +user = "postgres" +pass = "password" + +[sqlite] +file = "/tmp/database.db" diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT.php b/vendor/gabordemooij/redbean/testing/RedUNIT.php new file mode 100644 index 0000000..93c433e --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT.php @@ -0,0 +1,162 @@ +getDatabase()->connect(); + try { + R::freeze( FALSE ); + R::debug( FALSE ); + R::nuke(); + } catch( \Exception $e ) { + R::freeze( FALSE ); + R::debug( FALSE ); + R::nuke(); + } + } + + /** + * Runs the test. This method will run the tests implemented + * by the RedUNIT instance. The run method scans its class for + * all public instance methods except: + * run (to avoid recursion), getTargetDrivers, onEvent + * and prepare. -- added cleanUp/prepare just in case they get overridden. + * + * @return void + */ + public function run() + { + $old_error_handler = set_error_handler('redunit_error_handler'); + $class = new \ReflectionClass( $this ); + $skip = array( 'run', 'getTargetDrivers', 'onEvent', 'cleanUp', 'prepare' ); + // Call all methods except run automatically + foreach ( $class->getMethods( \ReflectionMethod::IS_PUBLIC ) as $method ) { + // Skip methods inherited from parent class + if ( $method->class != $class->getName() ) continue; + if ( in_array( $method->name, $skip ) ) continue; + $classname = str_replace( $class->getParentClass()->getName().'_', '', $method->class ); + printtext( "\n\t" . $classname."->".$method->name." [".$method->class."->".$method->name."]" . " \n\t" ); + $call = $method->name; + $this->$call(); + try { + R::nuke(); + } catch( \PDOException $e ) { + // Some tests use a broken database on purpose, so an exception is ok + } + } + restore_error_handler(); + } + + /** + * Clean-up method, to be invoked after running the test. + * This is an empty implementation, it does nothing. However this method + * should be overridden by tests that require clean-up. + * + * @return void + */ + public function cleanUp() + { + } + + /** + * Sets the current driver. + * This method is called by a test controller, runner or manager + * to activate the mode associated with the specified driver + * identifier. This mechanism allows a test to run slightly different + * in the context of another driver, for instance SQLite might not need + * some tests, or MySQL might need some additional tests etc... + * + * @param string $driver the driver identifier + * + * @return void + */ + public function setCurrentDriver( $driver ) + { + $this->currentlyActiveDriverID = $driver; + } + + /** + * Sets the round number. + * Each test can have a varying number of flavors. + * A test flavor is 'that same test' but for a different driver. + * Each 'run on a specific driver' is called a round. + * Some tests might want to know what the current round is. + * This method can be used to set the current round number. + * + * @param integer $roundNumber round, the current round number + * + * @return void + */ + public function setRound( $roundNumber ) + { + $this->round = (int) $roundNumber; + } + + /** + * Returns the current round number. + * The current round number indicates how many times + * this test has been applied (to various drivers). + * + * @return integer + */ + public function getRound() + { + return $this->round; + } + + /** + * Detects whether the current round is the first one. + * If the current round is indeed the first round, this method + * will return boolean TRUE, otherwise it will return FALSE. Note that + * the rounds are 0-based, so - if the current round equals 0, this + * method will return TRUE. + * + * @return boolean + */ + public function isFirstRound() + { + return ( $this->round === 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base.php new file mode 100644 index 0000000..5750b00 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base.php @@ -0,0 +1,55 @@ +name = 'x'; + $person->alias('teacher')->ownProject[] = $pro; + $person->alias('student')->ownCourse[] = $c; + R::store($person); + asrt($c->fresh()->fetchAs('person')->student->name, 'x'); + asrt($pro->fresh()->fetchAs('person')->teacher->name, 'x'); + $person = $person->fresh(); + $person->alias('teacher')->ownProject = array(); + $person->alias('student')->ownCourse = array(); + R::store($person); + asrt($c->fresh()->fetchAs('person')->student, NULL); + asrt($pro->fresh()->fetchAs('person')->teacher, NULL); + } + + /** + * Describing how clearing state of bean works. + * Every method returning somthing (except getID) + * clears prefix-method-state (anything set by withCond,with,alias,fetchAs). + * + * @return void + */ + public function clearStateAdditionalTests() + { + list( $project1, $project2 ) = R::dispense( 'project', 2 ); + list( $irene, $ilse ) = R::dispense('person', 2); + $project1->developer = $ilse; + $project1->designer = $irene; + $ilse->name = 'Ilse'; + $irene->name = 'Irene'; + $project2->developer = $ilse; + R::storeAll( array( $project1, $project2 ) ); + $ilse = R::load( 'person', $ilse->id ); + asrt( count( $ilse->alias( 'developer' )->ownProject ), 2); + //cached - same list + asrt( count( $ilse->ownProject ), 2); + asrt( count( $ilse->alias( 'designer' )->ownProject ), 0); + //cached - same list + asrt( count( $ilse->ownProject ), 0); + //now test state + asrt( count( $ilse->setAttr( 'a', 'b' )->alias( 'developer' )->ownProject ), 2); + //now test state + $ilse = $ilse->fresh(); + //attr clears state... + asrt( count( $ilse->alias( 'developer' )->setAttr( 'a', 'b' )->ownProject ), 0); + //but getID() does not! + $ilse = $ilse->fresh(); + $ilse->alias('developer'); + $ilse->getID(); + asrt( count( $ilse->ownProject ), 2 ); + } + + /** + * Can switch fetchAs(). + * Also checks shadow by storing. + * + * @return void + */ + public function canSwitchParentBean() + { + list( $project1, $project2 ) = R::dispense( 'project', 2 ); + list( $irene, $ilse ) = R::dispense('person', 2); + $project1->developer = $ilse; + $project1->designer = $irene; + $ilse->name = 'Ilse'; + $irene->name = 'Irene'; + $project2->developer = $ilse; + R::storeAll( array( $project1, $project2 ) ); + $project1 = R::load( 'project', $project1->id ); + asrt( $project1->fetchAs('person')->developer->name, 'Ilse' ); + asrt( $project1->fetchAs('person')->designer->name, 'Irene' ); + R::store( $project1 ); + $project1 = R::load( 'project', $project1->id ); + asrt( $project1->fetchAs('person')->designer->name, 'Irene' ); + asrt( $project1->fetchAs('person')->developer->name, 'Ilse' ); + R::store( $project1 ); + asrt( $project1->fetchAs('person')->developer->name, 'Ilse' ); + asrt( $project1->fetchAs('person')->designer->name, 'Irene' ); + asrt( $project1->fetchAs('person')->developer->name, 'Ilse' ); + } + + /** + * Switching aliases (->alias) should not change other list during + * storage. + * + * @return void + */ + public function testShadow() + { + list( $project1, $project2 ) = R::dispense( 'project', 2 ); + list( $irene, $ilse ) = R::dispense('person', 2); + $project1->developer = $ilse; + $project1->designer = $irene; + $project2->developer = $ilse; + R::storeAll( array( $project1, $project2 ) ); + $ilse = R::load( 'person', $ilse->id ); + $irene = R::load( 'person', $irene->id ); + asrt( count( $ilse->alias('developer')->ownProject ), 2 ); + asrt( count( $ilse->alias('designer')->ownProject ), 0 ); + R::store( $ilse ); + $ilse = R::load( 'person', $ilse->id ); + $irene = R::load( 'person', $irene->id ); + asrt( count( $ilse->alias('designer')->ownProject ), 0 ); + asrt( count( $ilse->alias('developer')->ownProject ), 2 ); + R::storeAll( array( $ilse, $irene) ); + $ilse = R::load( 'person', $ilse->id ); + $irene = R::load( 'person', $irene->id ); + asrt( count( $ilse->alias('designer')->ownProject ), 0 ); + asrt( count( $ilse->alias('developer')->ownProject ), 2 ); + asrt( count( $irene->alias('designer')->ownProject), 1 ); + asrt( count( $irene->alias('developer')->ownProject), 0 ); + R::storeAll( array( $ilse, $irene) ); + $ilse = R::load( 'person', $ilse->id ); + $irene = R::load( 'person', $irene->id ); + asrt( count( $ilse->alias('designer')->ownProject ), 0 ); + asrt( count( $ilse->alias('developer')->ownProject ), 2 ); + asrt( count( $irene->alias('designer')->ownProject), 1 ); + asrt( count( $irene->alias('developer')->ownProject), 0 ); + } + + /** + * Issue 291. State not cleared. + * + * @return void + */ + public function testFetchTypeConfusionIssue291() + { + list( $teacher, $student ) = R::dispense( 'person', 2 ) ; + $teacher->name = 'jimmy' ; + $student->name = 'jacko' ; + R::store( $teacher ) ; + R::store( $student ) ; + $client = R::dispense( 'client' ) ; + $client->firm = 'bean AG' ; + R::store( $client ) ; + $project = R::dispense( 'project' ) ; + $project->teacher = $teacher ; + $project->student = $student ; + $project->client = $client ; + R::store( $project ) ; + unset( $project->student ) ; + R::store( $project ) ; + $project = R::load( 'project', 1 ) ; + $teacher = $project->fetchAs( 'person' )->teacher ; + $student = $project->fetchAs( 'person' )->student ; + $client = $project->client ; // this will select from "person" instead of "client" + asrt( $client->firm, 'bean AG' ); + } + + /** + * Test switching alias (also issue #291). + * + * @return void + */ + public function testAliasSwitch() + { + $student = R::dispense( 'person' ); + $project = R::dispense( 'project' ); + $project->student = $student; + R::store( $project ); + $person = R::load( 'person', $student->id ); + asrt( count( $person->alias( 'student' )->ownProject ), 1); + asrt( count( $person->alias( 'teacher' )->ownProject ), 0); + } + + /** + * Associating two beans, then loading the associated bean + * + * @return void + */ + public function testAssociated() + { + $person = R::dispense( 'person' ); + $person->name = 'John'; + R::store( $person ); + $course = R::dispense( 'course' ); + $course->name = 'Math'; + + R::store( $course ); + $course->teacher = $person; + $id = R::store( $course ); + $course = R::load( 'course', $id ); + $teacher = $course->fetchAs( 'person' )->teacher; + asrt( $teacher->name, 'John' ); + + //Trying to load a property that has an invalid name + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->wrongProperty = array( $page ); + try { + $book->wrongProperty[] = $page; + R::store( $book ); + fail(); + } catch ( RedException $e ) { + pass(); + } catch ( \Exception $e ) { + fail(); + } + } + + /** + * Test for quick detect change. + * + * @return void + */ + public function basic() + { + $book = R::dispense( 'book' ); + + asrt( isset( $book->prop ), FALSE ); //not a very good test + asrt( in_array( 'prop', array_keys( $book->export() ) ), FALSE ); //better... + + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + + $book->paper = $page; + + $id = R::store( $book ); + $book = R::load( 'book', $id ); + + asrt( FALSE, ( isset( $book->paper ) ) ); + asrt( FALSE, ( isset( $book->page ) ) ); + + /** + * The following tests try to store various things that aren't + * beans (which is expected) with the own* and shared* properties + * which only accept beans as assignments, so they're expected to fail + */ + foreach ( array( 'a string', 1928, TRUE, NULL, array()) as $value ) { + try { + $book->ownPage[] = $value; + R::store( $book ); + $book->sharedPage[] = $value; + R::store( $book ); + fail(); + } catch ( RedException $e ) { + pass(); + } catch ( \Exception $e ) { + if (strpos($e->getMessage(),'Array to string conversion')===FALSE) { + fail(); + } + } + } + } + + /** + * Finding $person beans that have been aliased into various roles + * + * @return void + */ + public function testAliasedFinder() + { + $message = R::dispense( 'message' ); + $message->subject = 'Roommate agreement'; + list( $sender, $recipient ) = R::dispense( 'person', 2 ); + $sender->name = 'Sheldon'; + $recipient->name = 'Leonard'; + $message->sender = $sender; + $message->recipient = $recipient; + $id = R::store( $message ); + $message = R::load( 'message', $id ); + asrt( $message->fetchAs( 'person' )->sender->name, 'Sheldon' ); + asrt( $message->fetchAs( 'person' )->recipient->name, 'Leonard' ); + $otherRecipient = R::dispense( 'person' ); + $otherRecipient->name = 'Penny'; + $message->recipient = $otherRecipient; + R::store( $message ); + $message = R::load( 'message', $id ); + asrt( $message->fetchAs( 'person' )->sender->name, 'Sheldon' ); + asrt( $message->fetchAs( 'person' )->recipient->name, 'Penny' ); + } + + /** + * Test Basic Fetch AS functionality. + */ + public function testBasicFetchAs() + { + $project = R::dispense( 'project' ); + $project->name = 'Mutant Project'; + list( $teacher, $student ) = R::dispense( 'person', 2 ); + $teacher->name = 'Charles Xavier'; + $project->student = $student; + $project->student->name = 'Wolverine'; + $project->teacher = $teacher; + $id = R::store( $project ); + $project = R::load( 'project', $id ); + asrt( $project->fetchAs( 'person' )->teacher->name, 'Charles Xavier' ); + asrt( $project->fetchAs( 'person' )->student->name, 'Wolverine' ); + } + + /** + * Test Basic list variations. + * + * @return void + */ + public function testBasicListVariations() + { + $farm = R::dispense( 'building' ); + $village = R::dispense( 'village' ); + $farm->name = 'farm'; + $village->name = 'Dusty Mountains'; + $farm->village = $village; + $id = R::store( $farm ); + $farm = R::load( 'building', $id ); + asrt( $farm->name, 'farm' ); + asrt( $farm->village->name, 'Dusty Mountains' ); + $village = R::dispense( 'village' ); + list( $mill, $tavern ) = R::dispense( 'building', 2 ); + $mill->name = 'Mill'; + $tavern->name = 'Tavern'; + $village->ownBuilding = array( $mill, $tavern ); + $id = R::store( $village ); + $village = R::load( 'village', $id ); + asrt( count( $village->ownBuilding ), 2 ); + $village2 = R::dispense( 'village' ); + $army = R::dispense( 'army' ); + $village->sharedArmy[] = $army; + $village2->sharedArmy[] = $army; + $id1 = R::store( $village ); + $id2 = R::store( $village2 ); + $village1 = R::load( 'village', $id1 ); + $village2 = R::load( 'village', $id2 ); + asrt( count( $village1->sharedArmy ), 1 ); + asrt( count( $village2->sharedArmy ), 1 ); + asrt( count( $village1->ownArmy ), 0 ); + asrt( count( $village2->ownArmy ), 0 ); + } + + /** + * Tests whether aliasing plays nice with beautification. + * Ensure that aliased column aren't beautified + * + * @return void + */ + public function testAliasWithBeautify() + { + $points = R::dispense( 'point', 2 ); + $line = R::dispense( 'line' ); + $line->pointA = $points[0]; + $line->pointB = $points[1]; + R::store( $line ); + $line2 = R::dispense( 'line' ); + $line2->pointA = $line->fetchAs('point')->pointA; + $line2->pointB = R::dispense( 'point' ); + R::store( $line2 ); + + //now we have two points per line (1-to-x) + //I want to know which lines cross A: + $a = R::load( 'point', $line->pointA->id ); //reload A + $lines = $a->alias( 'pointA' )->ownLine; + asrt( count( $lines ), 2 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Arrays.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Arrays.php new file mode 100644 index 0000000..1c127db --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Arrays.php @@ -0,0 +1,273 @@ +name = 'bean'; + $bean->taste = 'salty'; + $properties = array(); + foreach($bean as $key => $value) { + $properties[ $key ] = $value; + } + asrt( count( $bean ), 3 ); + asrt( count( $properties ), 3 ); + asrt( isset( $properties['id'] ), TRUE ); + asrt( isset( $properties['name'] ), TRUE ); + asrt( isset( $properties['taste'] ), TRUE ); + $bean = R::dispense('bean'); + $bean['name'] = 'bean'; + $bean['taste'] = 'salty'; + $properties = array(); + foreach($bean as $key => $value) { + $properties[ $key ] = $value; + } + asrt( count( $bean ), 3 ); + asrt( count( $properties ), 3 ); + asrt( isset( $properties['id'] ), TRUE ); + asrt( isset( $properties['name'] ), TRUE ); + asrt( isset( $properties['taste'] ), TRUE ); + } + + /** + * Tests array access with lists. + * Tests whether list properties behave as arrays correctly. + * + * @return void + */ + public function testArrayAccessAndLists() + { + $book = R::dispense('book'); + $book['title'] = 'My Book'; + //Can we add a bean in list with array access? + $book['ownPage'][] = R::dispense('page'); + $book['ownPage'][] = R::dispense('page'); + asrt( count( $book ), 3 ); + $properties = array(); + foreach($book as $key => $value) { + $properties[ $key ] = $value; + } + asrt( count( $properties ), 3 ); + //Dont reveal aliased x-own and -List in foreach-loop + asrt( isset( $properties['id'] ), TRUE ); + asrt( isset( $properties['title'] ), TRUE ); + asrt( isset( $properties['ownPage'] ), TRUE ); + asrt( isset( $properties['ownPageList'] ), FALSE ); + asrt( isset( $properties['xownPage'] ), FALSE ); + asrt( isset( $properties['xownPageList'] ), FALSE ); + //But keep them countable + asrt( count( $book['ownPage'] ), 2 ); + asrt( count( $book['ownPageList'] ), 2 ); + asrt( count( $book['xownPage'] ), 2 ); + asrt( count( $book['xownPageList'] ), 2 ); + //And reveal state of items with isset() + asrt( isset( $book['id'] ), TRUE ); + asrt( isset( $book['title'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + //Can we add using the List alias? + $book['ownPageList'][] = R::dispense('page'); + asrt( count( $book['ownPage'] ), 3 ); + asrt( count( $book['ownPageList'] ), 3 ); + asrt( count( $book['xownPage'] ), 3 ); + asrt( count( $book['xownPageList'] ), 3 ); + //Can we add using the x-mode alias? + $book['ownPageList'][] = R::dispense('page'); + asrt( count( $book['ownPage'] ), 4 ); + asrt( count( $book['ownPageList'] ), 4 ); + asrt( count( $book['xownPage'] ), 4 ); + asrt( count( $book['xownPageList'] ), 4 ); + //Can we unset using array access? + unset( $book['ownPage'] ); + asrt( isset( $book['ownPage'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + $book['ownPage'] = array( R::dispense('page') ); + unset( $book['xownPage'] ); + asrt( isset( $book['ownPage'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + $book['ownPage'] = array( R::dispense('page') ); + unset( $book['ownPageList'] ); + asrt( isset( $book['ownPage'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + $book['ownPage'] = array( R::dispense('page') ); + unset( $book['xownPageList'] ); + asrt( isset( $book['ownPage'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + //does it work with shared lists as well? + $book['sharedCategory'] = array( R::dispense('category') ); + asrt( count( $book ), 3 ); + $properties = array(); + foreach($book as $key => $value) { + $properties[ $key ] = $value; + } + asrt( isset( $properties['sharedCategory'] ), TRUE ); + asrt( isset( $properties['sharedCategoryList'] ), FALSE ); + asrt( isset( $book['sharedCategory'] ), TRUE ); + asrt( isset( $book['sharedCategoryList'] ), TRUE ); + asrt( count( $book['sharedCategory'] ), 1 ); + asrt( count( $book['sharedCategoryList'] ), 1 ); + $book['sharedCategory'][] = R::dispense( 'category' ); + asrt( count( $book['sharedCategory'] ), 2 ); + asrt( count( $book['sharedCategoryList'] ), 2 ); + $book['sharedCategoryList'][] = R::dispense( 'category' ); + asrt( count( $book['sharedCategory'] ), 3 ); + asrt( count( $book['sharedCategoryList'] ), 3 ); + } + + /** + * Tests array access with parent beans. + * + * @return void + */ + public function testArrayAccessWithBeans() + { + $book = R::dispense( 'bean' ); + $book['author'] = R::dispense( 'author' ); + asrt( isset( $book['author'] ), TRUE ); + asrt( count( $book ), 2 ); + $book['author']['name'] = 'me'; + asrt( $book['author']['name'], 'me' ); + $book['author']['address'] = R::dispense( 'address' ); + $book['author']['ownTagList'][] = R::dispense( 'tag' ); + asrt( isset( $book['author']['address'] ), TRUE ); + asrt( isset( $book['author']['ownTag'] ), TRUE ); + asrt( count( $book['author']['ownTag'] ), 1 ); + asrt( isset( $book['author']['xownTag'] ), TRUE ); + asrt( count( $book['author']['xownTag'] ), 1 ); + asrt( isset( $book['author']['ownTagList'] ), TRUE ); + asrt( count( $book['author']['ownTagList'] ), 1 ); + asrt( isset( $book['author']['xownTagList'] ), TRUE ); + asrt( count( $book['author']['xownTagList'] ), 1 ); + unset( $book['author'] ); + asrt( isset( $book['author'] ), FALSE ); + asrt( count( $book ), 1 ); + } + + /** + * Tests array access with CRUD operations. + * + * @return void + */ + public function testArrayAccessWithCRUD() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book['ownPageList'] = R::dispense( 'page', 3 ); + R::store( $book ); + $book = $book->fresh(); + //note that isset first returns FALSE, so you can check if a list is loaded + asrt( isset( $book['ownPage'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + //count triggers load... + asrt( count( $book['ownPage'] ), 3 ); + asrt( isset( $book['ownPage'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + $book = $book->fresh(); + asrt( count( $book['xownPage'] ), 3 ); + $book = $book->fresh(); + asrt( count( $book['ownPageList'] ), 3 ); + $book = $book->fresh(); + asrt( count( $book['xownPageList'] ), 3 ); + $book['ownPage'][] = R::dispense( 'page' ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book['ownPage'] ), 4 ); + $book = $book->fresh(); + asrt( count( $book['xownPage'] ), 4 ); + $book = $book->fresh(); + asrt( count( $book['ownPageList'] ), 4 ); + $book = $book->fresh(); + asrt( count( $book['xownPageList'] ), 4 ); + //does dependency still work? + $book['xownPageList'] = array(); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book['ownPage'] ), 0 ); + $book = $book->fresh(); + asrt( count( $book['xownPage'] ), 0 ); + $book = $book->fresh(); + asrt( count( $book['ownPageList'] ), 0 ); + $book = $book->fresh(); + asrt( count( $book['xownPageList'] ), 0 ); + //does shared list work as well? + $book['sharedTag'] = R::dispense( 'tag', 2 ); + R::store( $book ); + $book = $book->fresh(); + //note that isset first returns FALSE, so you can check if a list is loaded + asrt( isset( $book['sharedTagList'] ), FALSE ); + asrt( isset( $book['sharedTag'] ), FALSE ); + //count triggers load... + asrt( count( $book['sharedTagList'] ), 2 ); + asrt( count( $book['sharedTag'] ), 2 ); + asrt( isset( $book['sharedTagList'] ), TRUE ); + asrt( isset( $book['sharedTag'] ), TRUE ); + $book['sharedTag'][] = R::dispense( 'tag' ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book['sharedTagList'] ), 3 ); + asrt( count( $book['sharedTag'] ), 3 ); + $book['sharedTagList'][] = R::dispense( 'tag' ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book['sharedTagList'] ), 4 ); + asrt( count( $book['sharedTag'] ), 4 ); + //does it also work with cross-shared + $book['sharedBookList'][] = R::dispense( 'book' ); + R::store( $book ); + $book = $book->fresh(); + asrt( isset( $book['sharedBookList'] ), FALSE ); + asrt( count( $book['sharedBookList'] ), 1 ); + $first = reset( $book['sharedBookList'] ); + $id = $first['id']; + asrt( count( $book['sharedBookList'][$id]['sharedBookList'] ), 1 ); + $properties = array(); + foreach($book as $key => $value) { + $properties[ $key ] = $value; + } + asrt( count( $properties ), 2 ); + $keys = array_keys( $properties ); + sort( $keys ); + asrt( implode( ',', $keys ), 'id,sharedBook' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Association.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Association.php new file mode 100644 index 0000000..452d816 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Association.php @@ -0,0 +1,296 @@ +currentlyActiveDriverID !== 'mysql' ) { + return; + } + testpack( 'Throw exception in case of issue with assoc constraint' ); + $bunny = R::dispense( 'bunny' ); + $carrot = R::dispense( 'carrot' ); + $faultyWriter = new \FaultyWriter( R::getToolBox()->getDatabaseAdapter() ); + $faultyOODB = new OODB( $faultyWriter ); + $faultyOODB->setBeanHelper( R::getRedBean()->getBeanHelper() ); + $faultyToolbox = new ToolBox( $faultyOODB, R::getToolBox()->getDatabaseAdapter(), $faultyWriter ); + $faultyAssociationManager = new AssociationManager( $faultyToolbox ); + $faultyWriter->setSQLState( '23000' ); + $faultyAssociationManager->associate( $bunny, $carrot ); + pass(); + $faultyWriter->setSQLState( '42S22' ); + R::nuke(); + try { + $faultyAssociationManager->associate( $bunny, $carrot ); + fail(); + } catch ( SQL $exception ) { + pass(); + } + } + + /** + * Test fast-track deletion, i.e. bypassing FUSE. + * For link beans. + * + * @return void + */ + public function testFastTrackDeletion() + { + testpack( 'Test fast-track deletion' ); + $ghost = R::dispense( 'ghost' ); + $house = R::dispense( 'house' ); + $house->sharedGhost[] = $ghost; + R::store($house); + \Model_Ghost_House::$deleted = FALSE; + R::getRedBean()->getAssociationManager()->unassociate( $house, $ghost ); + // No fast-track, assoc bean got trashed + asrt( \Model_Ghost_House::$deleted, TRUE ); + \Model_Ghost_House::$deleted = FALSE; + $house->sharedGhost[] = $ghost; + R::store($house); + R::getRedBean()->getAssociationManager()->unassociate( $house, $ghost, TRUE ); + // Fast-track, assoc bean got deleted right away + asrt( \Model_Ghost_House::$deleted, FALSE ); + } + + /** + * Test self-referential associations. + * + * @return void + */ + public function testCrossAssociation() + { + $ghost = R::dispense( 'ghost' ); + $ghost2 = R::dispense( 'ghost' ); + R::getRedBean()->getAssociationManager()->associate( $ghost, $ghost2 ); + \Model_Ghost_Ghost::$deleted = FALSE; + R::getRedBean()->getAssociationManager()->unassociate( $ghost, $ghost2 ); + // No fast-track, assoc bean got trashed + asrt( \Model_Ghost_Ghost::$deleted, TRUE ); + \Model_Ghost_Ghost::$deleted = FALSE; + R::getRedBean()->getAssociationManager()->unassociate( $ghost, $ghost2, TRUE ); + // Fast-track, assoc bean got deleted right away + asrt( \Model_Ghost_Ghost::$deleted, FALSE ); + } + + /** + * Test limited support for polymorph associations. + * RedBeanPHP does not really feature polymorph relations since + * they are not really compatible with traditional relational databases. + * However a light-weight, basic implementation has been added for + * those circumstances where you can't live without... + * i.e... possible legacy systems and so on. + * + * @return void + */ + public function testPoly() + { + testpack( 'Testing poly' ); + $shoe = R::dispense( 'shoe' ); + $lace = R::dispense( 'lace' ); + $lace->color = 'white'; + $id = R::store( $lace ); + $shoe->itemType = 'lace'; + $shoe->item = $lace; + $id = R::store( $shoe ); + $shoe = R::load( 'shoe', $id ); + $x = $shoe->poly( 'itemType' )->item; + asrt( $x->color, 'white' ); + } + + /** + * Test limited support for 1-to-1 associations. + * The rule is, one-to-ones are supposes to be in the same table, + * this is just for some legacy tables not designed to work + * with RedBeanPHP at all. + * + * @return void + */ + public function testOneToOne() + { + testpack( 'Testing one-to-ones' ); + $author = R::dispense( 'author' )->setAttr( 'name', 'a' );; + $bio = R::dispense( 'bio' )->setAttr( 'name', 'a' ); + R::storeAll( array( $author, $bio ) ); + $id1 = $author->id; + $author = R::dispense( 'author' )->setAttr( 'name', 'b' );; + $bio = R::dispense( 'bio' )->setAttr( 'name', 'b' ); + R::storeAll( array( $author, $bio ) ); + $x = $author->one( 'bio' ); + $y = $bio->one('author'); + asrt( $x->name, $bio->name ); + asrt( $y->name, $author->name ); + asrt( $x->id, $bio->id ); + asrt( $y->id, $author->id ); + $id2 = $author->id; + list( $a, $b ) = R::loadMulti( 'author,bio', $id1 ); + asrt( $a->name, $b->name ); + asrt( $a->name, 'a' ); + list( $a, $b ) = R::loadMulti( 'author,bio', $id2 ); + asrt( $a->name, $b->name ); + asrt( $a->name, 'b' ); + list( $a, $b ) = R::loadMulti( array( 'author', 'bio' ), $id1 ); + asrt( $a->name, $b->name ); + asrt( $a->name, 'a' ); + list( $a, $b ) = R::loadMulti( array( 'author', 'bio' ), $id2 ); + asrt( $a->name, $b->name ); + asrt( $a->name, 'b' ); + asrt( is_array( R::loadMulti( NULL, 1 ) ), TRUE ); + asrt( ( count( R::loadMulti( NULL, 1 ) ) === 0 ), TRUE ); + } + + /** + * Test single column bases unique constraints. + * + * @return void + */ + public function testSingleColUniqueConstraint() + { + testpack( 'Testing unique constraint on single column' ); + $book = R::dispense( 'book' ); + $book->title = 'bla'; + $book->extra = 2; + $id = R::store( $book ); + R::getWriter()->addUniqueIndex( 'book', array( 'title' ) ); + $book = R::dispense( 'book' ); + $book->title = 'bla'; + $expected = NULL; + try { + R::store( $book ); + + fail(); + } catch ( SQL $e ) { + $expected = $e; + } + asrt( ( $expected instanceof SQL ), TRUE ); + asrt( R::count( 'book' ), 1 ); + $book = R::load( 'book', $id ); + // Causes failure, table will be rebuild + $book->extra = 'CHANGE'; + $id2 = R::store( $book ); + $book2 = R::load( 'book', $id2 ); + $book = R::dispense( 'book' ); + $book->title = 'bla'; + try { + R::store( $book ); + + fail(); + } catch ( SQL $e ) { + $expected = $e; + } + asrt( ( $expected instanceof SQL ), TRUE ); + asrt( R::count( 'book' ), 1 ); + } + + /** + * Test multiple assiociation. + * + * @return void + */ + public function testMultiAssociationDissociation() + { + $wines = R::dispense( 'wine', 3 ); + $cheese = R::dispense( 'cheese', 3 ); + $olives = R::dispense( 'olive', 3 ); + R::getRedBean()->getAssociationManager()->associate( $wines, array_merge( $cheese, $olives ) ); + asrt( R::count( 'cheese' ), 3 ); + asrt( R::count( 'olive' ), 3 ); + asrt( R::count( 'wine' ), 3 ); + asrt( count( $wines[0]->sharedCheese ), 3 ); + asrt( count( $wines[0]->sharedOlive ), 3 ); + asrt( count( $wines[1]->sharedCheese ), 3 ); + asrt( count( $wines[1]->sharedOlive ), 3 ); + asrt( count( $wines[2]->sharedCheese ), 3 ); + asrt( count( $wines[2]->sharedOlive ), 3 ); + R::getRedBean()->getAssociationManager()->unassociate( $wines, $olives ); + asrt( count( $wines[0]->sharedCheese ), 3 ); + asrt( count( $wines[0]->sharedOlive ), 0 ); + asrt( count( $wines[1]->sharedCheese ), 3 ); + asrt( count( $wines[1]->sharedOlive ), 0 ); + asrt( count( $wines[2]->sharedCheese ), 3 ); + asrt( count( $wines[2]->sharedOlive ), 0 ); + R::getRedBean()->getAssociationManager()->unassociate( array( $wines[1] ), $cheese ); + asrt( count( $wines[0]->sharedCheese ), 3 ); + asrt( count( $wines[0]->sharedOlive ), 0 ); + asrt( count( $wines[1]->sharedCheese ), 0 ); + asrt( count( $wines[1]->sharedOlive ), 0 ); + asrt( count( $wines[2]->sharedCheese ), 3 ); + asrt( count( $wines[2]->sharedOlive ), 0 ); + R::getRedBean()->getAssociationManager()->unassociate( array( $wines[2] ), $cheese ); + asrt( count( $wines[0]->sharedCheese ), 3 ); + asrt( count( $wines[0]->sharedOlive ), 0 ); + asrt( count( $wines[1]->sharedCheese ), 0 ); + asrt( count( $wines[1]->sharedOlive ), 0 ); + asrt( count( $wines[2]->sharedCheese ), 0 ); + asrt( count( $wines[2]->sharedOlive ), 0 ); + } + + /** + * Tests error handling related to association. + * On database systems providing informative SQL STATE error codes + * RedBeanPHP should not mind non-existing tables or columns in + * fluid mode. + * + * @return void + */ + public function testErrorHandling() + { + R::nuke(); + list( $book, $page ) = R::dispenseAll( 'book,page' ); + $book->sharedPage[] = $page; + R::store( $page ); + $redbean = R::getRedBean(); + $am = $redbean->getAssociationManager(); + //SQLite and CUBRID do not comply with ANSI SQLState codes. + $catchAll = ( $this->currentlyActiveDriverID == 'sqlite' || $this->currentlyActiveDriverID === 'CUBRID' ); + try { + $am->related( $book, 'page', 'invalid SQL' ); + if ($catchAll) pass(); else fail(); + } catch ( SQL $e ) { + if ($catchAll) fail(); else pass(); + } + try { + $am->related( $book, 'cover'); + pass(); + } catch ( SQL $e ) { + fail(); + } + try { + $am->related( R::dispense('cover'), 'book' ); + pass(); + } catch ( SQL $e ) { + fail(); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Batch.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Batch.php new file mode 100644 index 0000000..c3117d6 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Batch.php @@ -0,0 +1,151 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $page = $redbean->dispense( "page" ); + $page->name = "page no. 1"; + $page->rating = 1; + $id1 = $redbean->store( $page ); + $page = $redbean->dispense( "page" ); + $page->name = "page no. 2"; + $id2 = $redbean->store( $page ); + $batch = $redbean->batch( "page", array( $id1, $id2 ) ); + asrt( count( $batch ), 2 ); + asrt( $batch[$id1]->getMeta( "type" ), "page" ); + asrt( $batch[$id2]->getMeta( "type" ), "page" ); + asrt( (int) $batch[$id1]->id, $id1 ); + asrt( (int) $batch[$id2]->id, $id2 ); + $book = $redbean->dispense( "book" ); + $book->name = "book 1"; + $redbean->store( $book ); + $book = $redbean->dispense( "book" ); + $book->name = "book 2"; + $redbean->store( $book ); + $book = $redbean->dispense( "book" ); + $book->name = "book 3"; + $redbean->store( $book ); + $books = $redbean->batch( "book", $adapter->getCol( "SELECT id FROM book" ) ); + asrt( count( $books ), 3 ); + $a = $redbean->batch( 'book', 9919 ); + asrt( is_array( $a ), TRUE ); + asrt( count( $a ), 0 ); + $a = $redbean->batch( 'triangle', 1 ); + asrt( is_array( $a ), TRUE ); + asrt( count( $a ), 0 ); + R::freeze( TRUE ); + $a = $redbean->batch( 'book', 9919 ); + asrt( is_array( $a ), TRUE ); + asrt( count( $a ), 0 ); + try { + $a = $redbean->batch( 'triangle', 1 ); + fail(); + } catch(SQL $e) { + pass(); + } + R::freeze( FALSE ); + asrt( R::wipe( 'spaghettimonster' ), FALSE ); + } + + /** + * Test missing bean scenarios. + * + * @return void + */ + public function testMissingBeans() + { + testpack( 'deal with missing beans' ); + + $id = R::store( R::dispense( 'beer' ) ); + $bottles = R::batch( 'beer', array( $id, $id + 1, $id + 2 ) ); + + asrt( count( $bottles ), 3 ); + asrt( (int) $bottles[$id]->id, (int) $id ); + asrt( (int) $bottles[$id + 1]->id, 0 ); + asrt( (int) $bottles[$id + 2]->id, 0 ); + } + + /** + * Test batch alias loadAll. + * + * @return void + */ + public function testBatchAliasLoadAll() + { + $ids = R::storeAll( R::dispense( 'page', 2 ) ); + $pages = R::loadAll( 'page', $ids ); + asrt( is_array( $pages ), TRUE ); + asrt( count( $pages ), 2 ); + asrt( ( $pages[$ids[0]] instanceof OODBBean ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Bean.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Bean.php new file mode 100644 index 0000000..e258ce8 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Bean.php @@ -0,0 +1,1276 @@ +title = 'My Book'; + R::store( $book ); + $books = R::findFromSQL('book','SELECT *, 100 AS pages FROM book WHERE title = ? ', array('My Book'), array('pages')); + asrt(is_array($books), TRUE); + asrt(count($books), 1); + $book = reset($books); + asrt($book->title, 'My Book'); + asrt(intval($book->info('pages')), 100); + asrt(intval($book->info('pages',0)), 100); + asrt($book->info('signatures',0), 0); + asrt(is_null($book->info('signatures')), TRUE); + $data = R::findFromSQL('book','SELECT *, 100 AS pages FROM book WHERE title = ? ', array('My Book'), array('pages'), TRUE); + asrt(is_array($data), TRUE); + asrt(count($data), 2); + $meta = $data[0]; + $books = $data[1]; + asrt(is_array($meta), TRUE); + asrt(count($meta), 1); + asrt(isset($meta['pages']), TRUE); + asrt(intval($meta['pages']), 100); + asrt(is_array($books), TRUE); + asrt(count($books), 1); + $book = reset($books); + asrt($book->title, 'My Book'); + asrt(intval($book->info('pages')), 100); + asrt(intval($book->info('pages',0)), 100); + asrt($book->info('signatures',0), 0); + asrt(is_null($book->info('signatures')), TRUE); + $data = R::findFromSQL('book','SELECT *, 100 AS pages FROM book WHERE title = ? AND id = -2 ', array('My Book'), array('pages'), TRUE); + asrt(is_array($data), TRUE); + asrt(count($data), 2); + $meta = $data[0]; + $books = $data[1]; + asrt(is_array($meta), TRUE); + asrt(is_array($books), TRUE); + asrt(count($meta), 1); + asrt(array_key_exists('pages', $meta), TRUE); + asrt($meta['pages'],NULL); + asrt(count($books), 0); + $books = R::findFromSQL('book','SELECT *, 100 AS pages FROM book WHERE title = ? ', array('Not My Book'), array('pages')); + asrt(is_array($books), TRUE); + asrt(count($books), 0); + } + + /** + * Tests whether we can override the __toString() method of + * a bean (for example to display binary data). + */ + public function testToStringOverride() + { + $bean = R::dispense('string'); + $bean->text = '"Hello"'; + $base64 = strval( $bean ); + asrt( $base64, 'IkhlbGxvIg==' ); + } + + /** + * Tests whether we can use RedBeanPHP core objects without + * Facade related objects in a non-static style. + * + * @return void + */ + public function testLooselyWired() + { + $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + $database = new RPDO( $pdo ); + $adapter = new DBAdapter( $database ); + if ($this->currentlyActiveDriverID == 'pgsql') $writer = new PostgreSQL( $adapter ); + if ($this->currentlyActiveDriverID == 'mysql') $writer = new MySQL( $adapter ); + if ($this->currentlyActiveDriverID == 'sqlite') $writer = new SQLiteT( $adapter ); + if ($this->currentlyActiveDriverID == 'CUBRID') $writer = new CUBRID( $adapter ); + $oodb = new OODB( $writer, FALSE ); + $bean = $oodb->dispense( 'bean' ); + $bean->name = 'coffeeBean'; + $id = $oodb->store( $bean ); + asrt( ( $id > 0 ), TRUE ); + $bean = $oodb->load( 'bean', $id ); + asrt( $bean->name, 'coffeeBean' ); + asrt( $oodb->count( 'bean' ), 1 ); + $oodb->trash( $bean ); + asrt( $oodb->count( 'bean' ), 0 ); + } + + /** + * From github (issue #549): + * Imagine you have a simple Post object, which has a person_id and + * is supposed to be associated to a Person. + * + * $post = R::findOne('post'); + * $a = $post->person ?? null; + * $b = $post->person; + * $c = $post->person ?? null; + * + * var_dump($a ? 'Person A is named ' . $a->name : 'No person A'); + * var_dump($b ? 'Person B is named ' . $b->name : 'No person B'); + * var_dump($c ? 'Person C is named ' . $c->name : 'No person C'); + * + * I would expect to either have the person's name in the output three times, + * or 'No person' in the output three times. + * What actually happens is that $a is null, + * so the first access fails, but the other two succeed. + * I'm guessing this happens because the implementation of offsetExists + * doesn't agree with offsetGet around relations. + * + * @return void + */ + public function testIssue549OffsetExistsVsOffsetGet() + { + R::nuke(); + list( $post, $person ) = R::dispenseAll( 'post,person' ); + $post->person = $person; + R::store( $post ); + $post = R::findOne('post'); + $a = (isset( $post->person )) ? $post->person : NULL; + $b = $post->person; + $c = (isset( $post->person )) ? $post->person : NULL; + $strA = ($a ? 'Y' : 'N'); + $strB = ($b ? 'Y' : 'N'); + $strC = ($c ? 'Y' : 'N'); + $out = "{$strA}{$strB}{$strC}"; + asrt( $out, 'NYY' ); + asrt( isset( $post->other ), FALSE ); + R::nuke(); + list( $post, $person ) = R::dispenseAll( 'post,person' ); + $post->person = $person; + R::store( $post ); + $post = R::findOne('post'); + $a = ( $post->exists('person') ) ? $post->person : NULL; + $b = $post->person; + $c = ( $post->exists('person') ) ? $post->person : NULL; + $strA = ($a ? 'Y' : 'N'); + $strB = ($b ? 'Y' : 'N'); + $strC = ($c ? 'Y' : 'N'); + $out = "{$strA}{$strB}{$strC}"; + asrt( $out, 'YYY' ); + asrt( isset( $post->other ), FALSE ); + asrt( $post->exists('comment'), FALSE ); + } + + /** + * Tests whether we can send results of a query to meta data + * when converting to bean. + */ + public function testImportMeta() + { + R::nuke(); + $book = R::dispense( array( + '_type' => 'book', + 'title' => 'Bean Recipes', + 'author' => 'Meastro de la Bean' + ) ); + $pages = R::dispenseAll( 'page*2' ); + $book->ownPageList = reset( $pages ); + R::store( $book ); + $data = R::getRow('SELECT book.*, + COUNT(page.id) AS meta_count, + 1234 AS meta_extra + FROM book + LEFT JOIN page ON page.book_id = book.id + GROUP BY book.id + '); + $bean = R::convertToBean( 'book', $data, 'meta_' ); + asrt( isset( $bean->title ), TRUE ); + asrt( isset( $bean->author ), TRUE ); + asrt( isset( $bean->meta_count ), FALSE ); + asrt( isset( $bean->meta_extra ), FALSE ); + $data = $bean->getMeta( 'data.bundle' ); + asrt( intval( $data['meta_count'] ), 2); + asrt( intval( $data['meta_extra'] ), 1234); + //now with multiple beans + $book = R::dispense( array( + '_type' => 'book', + 'title' => 'Bean Adventures', + 'author' => 'Mr Adventure' + ) ); + $pages = R::dispenseAll( 'page*3' ); + $book->ownPageList = reset( $pages ); + R::store( $book ); + $data = R::getAll('SELECT book.*, + COUNT(page.id) AS meta_pages + FROM book + LEFT JOIN page ON page.book_id = book.id + GROUP BY book.id + '); + $books = R::convertToBeans( 'book', $data, 'meta_' ); + $found = 0; + foreach( $books as $book ) { + if ( $book->title == 'Bean Recipes' ) { + $found++; + asrt( isset( $book->title ), TRUE ); + asrt( isset( $book->author ), TRUE ); + asrt( isset( $book->meta_count ), FALSE ); + asrt( isset( $book->meta_extra ), FALSE ); + $data = $book->getMeta( 'data.bundle' ); + asrt( intval( $data['meta_pages'] ), 2); + } + if ( $book->title == 'Bean Adventures' ) { + $found++; + asrt( isset( $book->title ), TRUE ); + asrt( isset( $book->author ), TRUE ); + asrt( isset( $book->meta_pages ), FALSE ); + asrt( isset( $book->meta_extra ), FALSE ); + $data = $book->getMeta( 'data.bundle' ); + asrt( intval( $data['meta_pages'] ), 3); + } + } + asrt( $found, 2 ); + } + + /** + * Test beautification conflicts... + * Issue #418 + * + * @return void + */ + public function testBeau() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->ownerId = 2; + $book->ownerCritic = 'a'; + $book->sharedbyReader = 'b'; + $id = R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['owner_id'] ), TRUE ); + asrt( isset( $columns['owner_critic'] ), TRUE ); + asrt( isset( $columns['sharedby_reader'] ), TRUE ); + asrt( isset( $columns['ownerId'] ), FALSE ); + asrt( isset( $columns['ownerCritic'] ), FALSE ); + asrt( isset( $columns['sharedbyReader'] ), FALSE ); + R::nuke(); + $book = R::dispense( 'book' ); + $book->xownerId = 2; + $book->xownerCritic = 'a'; + $book->sharedbyReader = 'b'; + $id = R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['xowner_id'] ), TRUE ); + asrt( isset( $columns['xowner_critic'] ), TRUE ); + asrt( isset( $columns['sharedby_reader'] ), TRUE ); + asrt( isset( $columns['xownerId'] ), FALSE ); + asrt( isset( $columns['xownerCritic'] ), FALSE ); + asrt( isset( $columns['sharedbyReader'] ), FALSE ); + } + + /** + * Misc bean tests. + * + * @return void + */ + public function testMisc() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->ownPage[] = R::dispense( 'page' ); + R::store( $book ); + R::nuke(); + R::store( $book ); + asrt( R::count( 'book' ), 0 ); + $book->ownPage; + R::store( $book ); + asrt( R::count( 'book' ), 0 ); + $book->title = 'x'; + R::store( $book ); + asrt( R::count( 'book' ), 0 ); + } + + /** + * Can we set a date string by passing a date object? + */ + public function testBeanDates() + { + $bean = R::dispense('bean'); + $dateTime = '1980-01-01 10:11:12'; + $bean->date = new \DateTime( $dateTime ); + asrt( $bean->date, $dateTime ); + } + + /** + * Only fire update query if the bean really contains different + * values. But make sure beans several 'parents' away still get + * saved. + * + * @return void + */ + public function testBeanTainting() + { + $logger = R::getDatabaseAdapter()->getDatabase()->getLogger(); + list( $i, $k, $c, $s ) = R::dispenseAll( 'invoice,customer,city,state' ); + $i->customer = $k; + $i->status = 0; + $k->city = $c; + $c->state = $s; + $s->name = 'x'; + R::store( $i ); + $i = $i->fresh(); + asrt( $i->customer->city->state->name, 'x' ); + $i->status = 1; + R::freeze( TRUE ); + $logger = R::debug( 1, 1 ); + //do we properly skip unmodified but tainted parent beans? + R::store( $i ); + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + //does cascade update still work? + $i = $i->fresh(); + $i->customer->city->state->name = 'y'; + R::store( $i ); + $i = $i->fresh(); + asrt( $i->customer->city->state->name, 'y' ); + $i = $i->fresh(); + $differentCity = R::dispense( 'city' ); + R::store( $differentCity ); + $i->customer->city = $differentCity; + R::store( $i ); + $i = $i->fresh(); + asrt( ( $i->customer->city->id != $c->id ), TRUE ); + asrt( is_null( $i->customer->city->state ), TRUE ); + $i->customer->city = NULL; + R::store( $i ); + $i = $i->fresh(); + asrt( is_null( $i->customer->city ), TRUE ); + $i->customer = $k; + $i->status = 0; + $k->city = $c; + $c->state = $s; + $s->name = 'x'; + R::store( $i ); + R::freeze( FALSE ); + $i = $i->fresh(); + //can we still change remote parent? + $i->customer->city->name = 'q'; + $logger->clear(); + R::store($i); + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + //print_r($logger->getLogs()); + asrt( count( $numberOfUpdateQueries ), 1 ); + $i = $i->fresh(); + asrt( $i->customer->city->name, 'q' ); + //do we properly skip unmodified but tainted parent beans? + $i->status = 3; + $logger->clear(); + R::store( $i ); + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + } + + /** + * Test whether the number of update queries + * executed is limited to the ones that are absolutely + * necessary to sync the database. + * + * @return void + */ + public function testUpdateQueries() + { + $book = R::dispense( 'book' ); + $book->title = 'Eye of Wight'; + $book->xownPageList = R::dispense( 'page', 10 ); + $book->sharedCategoryList = R::dispense( 'category', 2 ); + $n = 1; + foreach( $book->xownPageList as $page ) { + $page->number = $n++; + } + $book->sharedCategory[0]->name = 'adventure'; + $book->sharedCategory[1]->name = 'puzzle'; + $book->author = R::dispense( 'author' ); + $book->author->name = 'John'; + $book->map = R::dispense( 'map' ); + $book->map->name = 'Wight'; + $book->map->xownLocationList = R::dispense( 'location', 3 ); + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), TRUE ); + R::store( $book ); + asrt( $book->getMeta('tainted'), FALSE ); + asrt( $book->getMeta('changed'), FALSE ); + $logger = R::debug( 1, 1 ); + $book = $book->fresh(); + asrt( $book->getMeta('tainted'), FALSE ); + asrt( $book->getMeta('changed'), FALSE ); + $book->author; + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + $logger->clear(); + R::store( $book ); + //read only, no updates + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $book->title = 'Spirit of the Stones'; + R::store( $book ); + //changed title, 1 update + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $logger->clear(); + //store again, no changes, no updates + R::store( $book ); + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $logger->clear(); + $book = $book->fresh(); + $book->xownPageList; + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + R::store( $book ); + //access only, no update + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $logger->clear(); + $book = $book->fresh(); + $book->sharedCategoryList; + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + R::store( $book ); + //access only, no update + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $logger->clear(); + $book = $book->fresh(); + unset($book->xownPageList[5]); + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + R::store( $book ); + //remove only, no update, just 1 delete + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $numberOfUpdateQueries = $logger->grep( 'DELETE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $book = $book->fresh(); + asrt( count( $book->xownPageList ), 9 ); + $logger->clear(); + $book = $book->fresh(); + $book->xownPageList[] = R::dispense('page'); + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + R::store( $book ); + //no update, 1 insert, just adding + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $numberOfUpdateQueries = $logger->grep( 'INSERT' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $book = $book->fresh(); + asrt( count( $book->xownPageList ), 10 ); + $logger->clear(); + $book = $book->fresh(); + $book->map->xownLocationList[1]->name = 'Horshoe Bay'; + asrt( $book->getMeta('tainted'), TRUE ); + asrt( $book->getMeta('changed'), FALSE ); + asrt( $book->map->getMeta('tainted'), TRUE ); + asrt( $book->map->getMeta('changed'), FALSE ); + asrt( $book->map->xownLocationList[1]->getMeta('tainted'), TRUE ); + asrt( $book->map->xownLocationList[1]->getMeta('changed'), TRUE ); + R::store( $book ); + //1 update for child of parent, no other updates + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $book = $book->fresh(); + asrt( $book->map->xownLocationList[1]->name, 'Horshoe Bay' ); + $logger->clear(); + R::store( $book ); + //just access, no updates + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 0 ); + $logger->clear(); + $book = $book->fresh(); + $book->ownPageList[2]->number = 99; + R::store( $book ); + //1 update, do not update rest of pages or book itself + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $book = $book->fresh(); + $book->author->name = 'Worsley'; + $logger->clear(); + R::store( $book ); + //1 update for parent + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + $author = R::dispense('author'); + $author->name = 'J.W.'; + R::store( $author ); + $book = $book->fresh(); + $book->author = $author; + $author->name = 'JW'; + $logger->clear(); + R::store( $book ); + //2 updates, one for author, one for link field: author_id needs update. + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 2 ); + $author->country = R::dispense( 'country' )->setAttr( 'name', 'England' ); + R::store( $author ); + $book = $book->fresh(); + $book->author->country->name = 'Wight'; + $logger->clear(); + R::store( $book ); + //1 update, country only, dont update for intermediate parents: book -> author -> ... + $numberOfUpdateQueries = $logger->grep( 'UPDATE' ); + asrt( count( $numberOfUpdateQueries ), 1 ); + } + + /** + * Tests effects of importFrom and setProperty. + * + * @return void + */ + public function testImportFromAndSetProp() + { + $bean = R::dispense( 'bean' ); + asrt( $bean->getMeta( 'tainted' ), TRUE ); + asrt( $bean->getMeta( 'changed' ), TRUE ); + $bean->setMeta( 'tainted', FALSE ); + $bean->setMeta( 'changed', FALSE ); + asrt( $bean->getMeta( 'tainted' ), FALSE ); + asrt( $bean->getMeta( 'changed' ), FALSE ); + $bean->importFrom( R::dispense( 'bean' ) ); + asrt( $bean->getMeta( 'tainted' ), TRUE ); + asrt( $bean->getMeta( 'changed' ), TRUE ); + $bean->setMeta( 'tainted', FALSE ); + $bean->setMeta( 'changed', FALSE ); + asrt( $bean->getMeta( 'tainted' ), FALSE ); + asrt( $bean->getMeta( 'changed' ), FALSE ); + $bean->setProperty( 'id', 0, TRUE, TRUE ); + asrt( $bean->getMeta( 'tainted' ), TRUE ); + asrt( $bean->getMeta( 'changed' ), TRUE ); + $bean->setMeta( 'tainted', FALSE ); + $bean->setMeta( 'changed', FALSE ); + asrt( $bean->getMeta( 'tainted' ), FALSE ); + asrt( $bean->getMeta( 'changed' ), FALSE ); + $bean->setProperty( 'id', 0, TRUE, FALSE ); + asrt( $bean->getMeta( 'tainted' ), FALSE ); + asrt( $bean->getMeta( 'changed' ), FALSE ); + $bean->name = 'x'; + asrt( $bean->getMeta( 'tainted' ), TRUE ); + asrt( $bean->getMeta( 'changed' ), TRUE ); + } + + /** + * Setup + * + * @return void + */ + private function _createBook() + { + R::nuke(); + $book = R::dispense( 'book' ); + $pages = R::dispense( 'page', 2 ); + $ads = R::dispense('ad', 3 ); + $tags = R::dispense( 'tag', 2 ); + $author = R::dispense( 'author' ); + $coauthor = R::dispense( 'author' ); + $book->alias( 'magazine' )->ownAd = $ads; + $book->ownPage = $pages; + $book->sharedTag = $tags; + $book->via( 'connection' )->sharedUser = array( R::dispense( 'user' ) ); + $book->author = $author; + $book->coauthor = $coauthor; + R::store( $book ); + return $book->fresh(); + } + + /* + * Can we add a bean to a list? + * + * @return void + */ + public function testWhetherWeCanAddToLists() + { + $book = $this->_createBook(); + $book->ownPage[] = R::dispense( 'page' ); + R::store( $book ); + asrt( R::count('page'), 3 ); + $book = $this->_createBook(); + $book->ownPageList[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('page'), 3 ); + $book = $this->_createBook(); + $book->xownPage[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('page'), 3 ); + $book = $this->_createBook(); + $book->xownPageList[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('page'), 3 ); + $ads = R::dispense('ad', 3 ); + $book = $this->_createBook(); + $book->alias('magazine')->ownAd = $ads; + $book->ownPage[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('ad'), 6 ); + asrt( R::count('page'), 3 ); + $ads = R::dispense('ad', 3 ); + $book = $this->_createBook(); + $book->alias('magazine')->ownAdList = $ads; + $book->ownPageList[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('ad'), 6 ); + asrt( R::count('page'), 3 ); + $ads = R::dispense('ad', 3 ); + $book = $this->_createBook(); + $book->alias('magazine')->xownAd = $ads; + $book->xownPage[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('ad'), 3 ); + asrt( R::count('page'), 3 ); + $ads = R::dispense('ad', 3 ); + $book = $this->_createBook(); + $book->alias('magazine')->xownAdList = $ads; + $book->xownPageList[] = R::dispense('page'); + R::store( $book ); + asrt( R::count('ad'), 3 ); + asrt( R::count('page'), 3 ); + $book = $this->_createBook(); + $book->sharedTag[] = R::dispense('tag'); + R::store( $book ); + asrt( R::count('tag'), 3 ); + $book = $this->_createBook(); + $book->sharedTagList[] = R::dispense('tag'); + R::store( $book ); + asrt( R::count('tag'), 3 ); + } + + /** + * Can we delete a bean in a list by its ID? + * Only the UNSET() variant should work. + * + * @return void + */ + public function testDeleteByIDs() + { + $book = $this->_createBook(); + $firstPage = reset( $book->ownPageList ); + $book->ownPage[ $firstPage->id ] = NULL; + try { R::store( $book ); fail(); }catch(\Exception $e) { pass(); } + $book = $this->_createBook(); + asrt( count( $book->ownPage ), 2 ); + $firstPage = reset( $book->ownPageList ); + unset( $book->ownPage[ $firstPage->id ] ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 1 ); + $firstPage = reset( $book->ownPageList ); + $book->ownPage[ $firstPage->id ] = FALSE; + try { R::store( $book ); fail(); }catch(\Exception $e) { pass(); } + $book = $book->fresh(); + asrt( count( $book->ownPage ), 0 ); + $book = $this->_createBook(); + $firstAd = reset( $book->alias('magazine')->ownAd ); + $book->alias('magazine')->ownAd[ $firstAd->id ] = NULL; + try { R::store( $book ); fail(); }catch(\Exception $e) { pass(); } + $book = $this->_createBook(); + asrt( count( $book->alias('magazine')->ownAd ), 3 ); + $firstAd = reset( $book->alias('magazine')->ownAdList ); + unset( $book->alias('magazine')->ownAdList[ $firstAd->id ] ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->alias('magazine')->ownAd ), 2 ); + $firstAd = reset( $book->alias('magazine')->ownAd ); + $book->alias('magazine')->ownAd[ $firstAd->id ] = FALSE; + try { R::store( $book ); fail(); }catch(\Exception $e) { pass(); } + $book = $book->fresh(); + asrt( count( $book->alias('magazine')->ownAd ), 1 ); + + } + + /** + * You CAN delete an own-list by assiging an empty array. + * + * @return void + */ + public function testDeleteOwnListWithEmptyArray() + { + $book = $this->_createBook(); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); //when loaded has 2 + $book->ownPage = array(); //remove all + R::store( $book ); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 0 ); + } + + /** + * You cannot delete an own-list by assigning NULL. + * + * @return void + */ + public function testCANTDeleteOwnListWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); //when loaded has 2 + $book->ownPage = NULL; //remove all + R::store( $book ); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); + } + + /** + * You cannot delete an own-list by assigning FALSE. + * + * @return void + */ + public function testCANTDeleteOwnListWithFalse() + { + $book = $this->_createBook(); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); //when loaded has 2 + $book->ownPage = FALSE; //remove all + R::store( $book ); + asrt( isset($book->ownPage), TRUE ); //not loaded yet, lazy loading + asrt( $book->ownPage, '0' ); + } + + /** + * You cannot delete an own-list by unsetting it. + */ + public function testCANTDeleteOwnListWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); //when loaded has 2 + unset( $book->ownPage ); //does NOT remove all + R::store( $book ); + asrt( isset($book->ownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->ownPage ), 2 ); + } + + /** + * You CAN delete an aliased own-list by assiging an empty array. + * + * @return void + */ + public function testDeleteAliasedOwnListWithEmptyArray() + { + $book = $this->_createBook(); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); //when loaded has 2 + $book->alias('magazine')->ownAd = array(); //remove all + $book->ownPage[] = R::dispense('page'); + R::store( $book ); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 0 ); + asrt( count( $book->alias('magazine')->ownPage ), 0 ); //also test possible confusion + asrt( count( $book->all()->ownPageList ), 3 ); + } + + /** + * You cannot delete an aliased own-list by assigning NULL. + * + * @return void + */ + public function testCANTDeleteAliasedOwnListWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); //when loaded has 2 + $book->alias('magazine')->ownAd = NULL; //remove all + R::store( $book ); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); + } + + /** + * You cannot delete an aliased own-list by assigning FALSE. + * + * @return void + */ + public function testCANTDeleteAliasedOwnListWithFalse() + { + $book = $this->_createBook(); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); //when loaded has 2 + $book->alias('magazine')->ownAd = FALSE; //remove all + R::store( $book ); + asrt( isset($book->alias('magazine')->ownAd), TRUE ); //not loaded yet, lazy loading + asrt( $book->alias('magazine')->ownAd, '0' ); + } + + /** + * You cannot delete an aliased own-list by unsetting it. + * + * @return void + */ + public function testCANTDeleteAliasedOwnListWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); //when loaded has 2 + unset( $book->alias('magazine')->ownAd ); //does NOT remove all + R::store( $book ); + asrt( isset($book->alias('magazine')->ownAd), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->alias('magazine')->ownAd ), 3 ); + } + + /** + * You CAN delete an x-own-list by assiging an empty array. + * + * @return void + */ + public function testDeleteXOwnListWithEmptyArray() + { + $book = $this->_createBook(); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); //when loaded has 2 + $book->xownPage = array(); //remove all + R::store( $book ); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 0 ); + } + + /** + * You cannot delete an x-own-list by assigning NULL. + * + * @return void + */ + public function testCANTDeleteXOwnListWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); //when loaded has 2 + $book->xownPage = NULL; //remove all + R::store( $book ); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); + } + + /** + * You cannot delete an x-own-list by assigning FALSE. + * + * @return void + */ + public function testCANTDeleteXOwnListWithFalse() + { + $book = $this->_createBook(); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); //when loaded has 2 + $book->xownPage = FALSE; //remove all + R::store( $book ); + asrt( isset($book->xownPage), TRUE ); //not loaded yet, lazy loading + asrt( $book->xownPage, '0' ); + } + + /** + * You cannot delete an x-own-list by unsetting it. + * + * @return void + */ + public function testCANTDeleteXOwnListWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); //when loaded has 2 + unset( $book->xownPage ); //does NOT remove all + R::store( $book ); + asrt( isset($book->xownPage), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->xownPage ), 2 ); + } + + /** + * You CAN delete a shared-list by assiging an empty array. + * + * @return void + */ + public function testDeleteSharedListWithEmptyArray() + { + $book = $this->_createBook(); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); //when loaded has 2 + $book->sharedTag = array(); //remove all + R::store( $book ); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 0 ); + } + + /** + * You cannot delete a shared list by assigning NULL. + * + * @return void + */ + public function testCANTDeleteSharedListWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); //when loaded has 2 + $book->sharedTag = NULL; //remove all + R::store( $book ); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); + } + + /** + * You cannot delete a shared-list by assigning FALSE. + * + * @return void + */ + public function testCANTDeleteSharedListWithFalse() + { + $book = $this->_createBook(); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); //when loaded has 2 + $book->sharedTag = FALSE; //remove all + R::store( $book ); + asrt( isset($book->sharedTag), TRUE ); //not loaded yet, lazy loading + asrt( $book->sharedTag, '0' ); + } + + /** + * You cannot delete a shared-list by unsetting it. + * + * @return void + */ + public function testCANTDeleteSharedWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); //when loaded has 2 + unset( $book->sharedTag ); //does NOT remove all + R::store( $book ); + asrt( isset($book->sharedTag), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->sharedTag ), 2 ); + } + + /** + * You CAN delete a shared-list by assiging an empty array. + * + * @return void + */ + public function testDeleteViaSharedListWithEmptyArray() + { + $book = $this->_createBook(); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); //when loaded has 2 + $book->via('connection')->sharedUser = array(); //remove all + R::store( $book ); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 0 ); + } + + /** + * You cannot delete a shared-list by assigning NULL. + * + * @return void + */ + public function testCANTDeleteViaSharedListWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); //when loaded has 2 + $book->via('connection')->sharedUser = NULL; //remove all + R::store( $book ); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); + } + + /** + * You cannot delete a shared list by assigning FALSE. + * + * @return void + */ + public function testCANTDeleteViaSharedListWithFalse() + { + $book = $this->_createBook(); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); //when loaded has 1 + $book->via('connection')->sharedUser = FALSE; //remove all + R::store( $book ); + asrt( isset($book->via('connection')->sharedUser), TRUE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); //when loaded has 1 + + } + + /** + * You cannot delete a shared-list by unsetting it. + * + * @return void + */ + public function testCANTDeleteViaSharedWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); //when loaded has 2 + unset( $book->via('connection')->sharedUser ); //does NOT remove all + R::store( $book ); + asrt( isset($book->via('connection')->sharedUser), FALSE ); //not loaded yet, lazy loading + asrt( count( $book->via('connection')->sharedUser ), 1 ); + } + + /** + * You cannot delete a parent bean by unsetting it. + * + * @return void + */ + public function testYouCANTDeleteParentBeanWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), TRUE ); + unset( $book->author ); + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), TRUE ); + } + + /** + * You cannot delete a parent bean by setting it to NULL. + * + * @return void + */ + public function testYouCANDeleteParentBeanWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), TRUE ); + $book->author = NULL; + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), FALSE ); + } + + /** + * You CAN delete a parent bean by setting it to FALSE. + * + * @return void + */ + public function testYouCANDeleteParentBeanWithFALSE() + { + $book = $this->_createBook(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), TRUE ); + $book->author = FALSE; + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->author), FALSE ); + asrt( (boolean) ($book->author), FALSE ); + } + + /** + * You cannot delete an aliased parent bean by unsetting it. + * + * @return void + */ + public function testYouCANTDeleteAliasedParentBeanWithUnset() + { + $book = $this->_createBook(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), TRUE ); + unset( $book->fetchAs('author')->coauthor ); + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), TRUE ); + } + + /** + * You CAN delete an aliased parent bean by setting it to NULL. + * + * @return void + */ + public function testYouCANDeleteAliasedParentBeanWithNULL() + { + $book = $this->_createBook(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), TRUE ); + $book->fetchAs('author')->coauthor = NULL; + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), FALSE ); + } + + /** + * You cannot delete an aliased parent bean by setting it to FALSE. + * + * @return void + */ + public function testYouCANDeleteAliasedParentBeanWithFALSE() + { + $book = $this->_createBook(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), TRUE ); + $book->fetchAs('author')->coauthor = FALSE; + R::store( $book ); + $book = $book->fresh(); + asrt( isset($book->fetchAs('author')->coauthor), FALSE ); + asrt( (boolean) ($book->fetchAs('author')->coauthor), FALSE ); + } + + /** + * Tests the effects of unsetting on the shadow of a list. + * + * @return void + */ + public function testUnsettingAListAndShadow() + { + $book = $this->_createBook(); + //should work with ownPage and ownPageList as well... + unset( $book->ownPageList ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 2 ); + unset( $book->ownPage ); + //shadow should be reloaded as well... + $book->with(' LIMIT 1 ')->ownPage; + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 2 ); + asrt( count( $book->getMeta('sys.shadow.ownPage') ), 2 ); + unset( $book->ownPage ); + asrt( $book->getMeta('sys.shadow.ownPage'), NULL ); + //no load must clear shadow as well... + $book->noLoad()->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->getMeta('sys.shadow.ownPage') ), 0 ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 3 ); + $lists = array( 'ownPage', 'ownPageList', 'xownPage', 'xownPageList', 'sharedPage', 'sharedPageList' ); + foreach( $lists as $list ) { + $book = R::dispense( 'book' ); + $book->$list; + $shadowKey = $list; + if ( strpos( $list, 'x' ) === 0) $shadowKey = substr( $shadowKey, 1 ); + $shadowKey = preg_replace( '/List$/', '', $shadowKey ); + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + unset( $book->$list ); + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->$list; //reloading brings back shadow + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + $book->$list = array(); //keeps shadow (very important to compare deletions!) + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + R::store( $book ); //clears shadow + $book->alias('magazine')->$list; //reloading with alias also brings back shadow + unset( $book->$list ); + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book = $book->fresh(); //clears shadow, reload + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->noLoad()->$list; //reloading with noload also brings back shadow + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + asrt( count( $book->getMeta('sys.shadow.'.$shadowKey) ), 0 ); + $book = $book->fresh(); //clears shadow, reload + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->all()->$list; //reloading with all also brings back shadow + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + $book = $book->fresh(); //clears shadow, reload + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->with(' LIMIT 1 ')->$list; //reloading with with- all also brings back shadow + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + $book = $book->fresh(); //clears shadow, reload + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->$list = array(); //keeps shadow (very important to compare deletions!) + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + $book = $book->fresh(); //clears shadow, reload + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + $book->$list = array(); //keeps shadow (very important to compare deletions!) + asrt( is_array( $book->getMeta('sys.shadow.'.$shadowKey) ), TRUE ); + R::trash( $book ); + asrt( $book->getMeta('sys.shadow.'.$shadowKey), NULL ); + } + + //no shadow for parent bean + $book = $book->fresh(); + $book->author = R::dispense( 'author' ); + asrt( $book->getMeta('sys.shadow.author'), NULL ); + R::store( $book ); + $book = $book->fresh(); + unset( $book->author ); //we can unset and it does not remove + R::store( $book ); + $book = $book->fresh(); + asrt( is_object( $book->author ),TRUE ); + //but we can also remove + $book->author = NULL; + R::store( $book ); + $book = $book->fresh(); + asrt( $book->author, NULL ); + + } + + /** + * Test whether the tainted flag gets set correctly. + * + * @return void + */ + public function testAccessingTainting() + { + $book = $this->_createBook(); + asrt( $book->isTainted(), FALSE ); + $book->ownPage; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->author; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->fetchAs('author')->coauthor; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->alias('magazine')->xownAdList; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->title = 'Hello'; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->sharedTag; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->via('connection')->sharedUser; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->coauthor; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->ownFakeList; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->sharedFakeList; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->alias('fake')->ownFakeList; + asrt( $book->isTainted(), TRUE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->title; + asrt( $book->isTainted(), FALSE ); + $book = $book->fresh(); + asrt( $book->isTainted(), FALSE ); + $book->title = 1; + $book->setMeta( 'tainted', FALSE ); + asrt( $book->isTainted(), FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Boxing.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Boxing.php new file mode 100644 index 0000000..1c281c7 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Boxing.php @@ -0,0 +1,70 @@ +box(); + R::trash( $bean ); + pass(); + $bean = R::dispense( 'boxedbean' ); + $bean->sharedBoxbean = R::dispense( 'boxedbean' )->box(); + R::store( $bean ); + pass(); + $bean = R::dispense( 'boxedbean' ); + $bean->ownBoxedbean = R::dispense( 'boxedbean' )->box(); + R::store( $bean ); + pass(); + $bean = R::dispense( 'boxedbean' ); + $bean->other = R::dispense( 'boxedbean' )->box(); + R::store( $bean ); + pass(); + $bean = R::dispense( 'boxedbean' ); + $bean->title = 'MyBean'; + $box = $bean->box(); + asrt( ( $box instanceof \Model_Boxedbean ), TRUE ); + R::store( $box ); + } + + /** + * Test fix for issue #512 - thanks for reporting Bernhard H. + * OODBBean::__toString() implementation only works with C_ERR_IGNORE + * + * @return void + */ + public function testToStringIssue512() + { + R::setErrorHandlingFUSE( \RedBeanPHP\OODBBean::C_ERR_FATAL ); + $boxedBean = R::dispense( 'boxedbean' ); + $str = (string) $boxedBean; + asrt( $str, '{"id":0}' ); //no fatal error + R::setErrorHandlingFUSE( FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Chill.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Chill.php new file mode 100644 index 0000000..74fed17 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Chill.php @@ -0,0 +1,135 @@ +col1 = '1'; + $bean->col2 = '2'; + R::store( $bean ); + asrt( count( R::getWriter()->getColumns( 'bean' ) ), 3 ); + $bean->col3 = '3'; + R::store( $bean ); + asrt( count( R::getWriter()->getColumns( 'bean' ) ), 4 ); + R::freeze( array( 'umbrella' ) ); + $bean->col4 = '4'; + R::store( $bean ); + asrt( count( R::getWriter()->getColumns( 'bean' ) ), 5 ); + R::freeze( array( 'bean' ) ); + $bean->col5 = '5'; + try { + R::store( $bean ); + fail(); + } catch (\Exception $e ) { + pass(); + } + asrt( count( R::getWriter()->getColumns( 'bean' ) ), 5 ); + R::freeze( array() ); + $bean->col5 = '5'; + R::store( $bean ); + asrt( count( R::getWriter()->getColumns( 'bean' ) ), 6 ); + } + + /** + * Test whether we cannot add unique constraints on chilled tables, + * otherwise you cannot avoid this from happening when adding beans to the + * shared list :) -- this is almost a theoretical issue however we want it + * to work according to specifications! + * + * @return void + */ + public function testDontAddUniqueConstraintForChilledBeanTypes() + { + R::nuke(); + $person = R::dispense( 'person' ); + $role = R::dispense( 'role' ); + $person->sharedRole[] = $role; + R::store( $person ); + $person->sharedRole[] = R::dispense( 'role' ); + R::store( $person ); + $bean = R::getRedBean()->dispense('person_role'); + $bean->personId = $person->id; + $bean->roleId = $role->id; + try { + R::store( $bean ); + fail(); + } catch(\Exception $e) { + pass(); + } + asrt(R::count('person_role'), 2); + R::nuke(); + $link = R::getRedBean()->dispense('person_role'); + $person = R::dispense( 'person' ); + $role = R::dispense( 'role' ); + $link->person = $person; + $link->role = $role; + R::store( $link ); + R::freeze(array('person_role')); + $person->sharedRole[] = R::dispense( 'role' ); + R::store( $person ); + $bean = R::getRedBean()->dispense('person_role'); + $bean->personId = $person->id; + $bean->roleId = $role->id; + try { + R::store( $bean ); + pass(); + } catch(\Exception $e) { + fail(); + } + asrt(R::count('person_role'), 3); + R::freeze( array() ); //set freeze to FALSE and clear CHILL LIST! + } + + /** + * Test whether we can set and reset the chill list and check the contents + * of the chill list. + * + * @return void + */ + public function testChillTest() + { + R::freeze( array( 'beer' ) ); + $oodb = R::getRedBean(); + asrt( $oodb->isChilled( 'beer' ), TRUE ); + asrt( $oodb->isChilled( 'wine' ), FALSE ); + R::freeze( FALSE ); + $oodb = R::getRedBean(); + asrt( $oodb->isChilled( 'beer' ), TRUE ); + asrt( $oodb->isChilled( 'wine' ), FALSE ); + R::freeze( TRUE ); + $oodb = R::getRedBean(); + asrt( $oodb->isChilled( 'beer' ), TRUE ); + asrt( $oodb->isChilled( 'wine' ), FALSE ); + R::freeze( array() ); + $oodb = R::getRedBean(); + asrt( $oodb->isChilled( 'beer' ), FALSE ); + asrt( $oodb->isChilled( 'wine' ), FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Close.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Close.php new file mode 100644 index 0000000..454e892 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Close.php @@ -0,0 +1,62 @@ +getMessage(), 'Unknown feature set label.' ); + } + try { + R::nuke(); + fail(); + } catch( \Exception $e ) { + asrt( $e->getMessage(), 'The nuke() command has been disabled using noNuke() or R::feature(novice/...).' ); + } + R::useFeatureSet('latest'); + //Close + R::getDatabaseAdapter()->setOption( 'setInitQuery', NULL ); + asrt( R::getDatabaseAdapter()->getDatabase()->isConnected(), TRUE ); + R::close(); + asrt( R::getDatabaseAdapter()->getDatabase()->isConnected(), FALSE ); + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Concurrency.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Concurrency.php new file mode 100644 index 0000000..fbae963 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Concurrency.php @@ -0,0 +1,314 @@ +value = 123; + R::store($lock); + $c = pcntl_fork(); + if ($c == -1) exit(1); + if (!$c) { + R::selectDatabase($this->currentlyActiveDriverID . 'c2'); + R::freeze(TRUE); + try { R::exec('SET SESSION innodb_lock_wait_timeout=5');}catch( \Exception $e ){} + try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){} + R::begin(); + $lock = R::loadForUpdate('lock', $lock->id); + $lock->value = 4; + sleep(10); + R::store($lock); + exit(0); + } else { + R::selectDatabase($this->currentlyActiveDriverID . 'c2'); + sleep(1); + R::freeze(TRUE); + try{ R::exec('SET SESSION innodb_lock_wait_timeout=5');}catch( \Exception $e ){} + try{R::exec("SET lock_timeout = '1s';");}catch( \Exception $e ){} + try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){} + R::begin(); + $exception = NULL; + try { + $lock = R::loadForUpdate('lock', $lock->id); + } catch( \Exception $e ) { + $exception = $e; + } + if ( !$exception ) fail(); + pass(); + $details = $exception->getDriverDetails(); + asrt( ($details[1]===1205 || $details[0]==='55P03'), TRUE ); + var_dump($lock); + } + try { R::exec('SET autocommit = 1'); }catch( \Exception $e ){} + pcntl_wait($status); + try { R::exec('SET SESSION innodb_lock_wait_timeout=50');}catch( \Exception $e ){} + try{R::exec("SET lock_timeout = '50s';");}catch( \Exception $e ){} + } + + /** + * Tests basic locking scenarios using fork(). + * + * @return void + */ + public function testConcurrency() + { + $c = pcntl_fork(); + if ($c == -1) exit(1); + if (!$c) { + R::selectDatabase($this->currentlyActiveDriverID . 'c'); + try{ R::exec('SET SESSION innodb_lock_wait_timeout=51');}catch( \Exception $e ){} + try{R::exec("SET lock_timeout = '51s';");}catch( \Exception $e ){} + R::exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + sleep(2); + try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){} + R::freeze(TRUE); + R::begin(); + echo "CHILD: SUBTRACTING 2 START\n"; + $i = R::loadForUpdate('inventory', 1); + $i->apples -= 2; + sleep(4); + R::store($i); + R::commit(); + echo "CHILD: SUBTRACTING 2 DONE\n"; + echo (R::load('inventory', 1)); + echo "\n"; + exit(0); + } else { + R::selectDatabase($this->currentlyActiveDriverID . 'c'); + try{ R::exec('SET SESSION innodb_lock_wait_timeout=51');}catch( \Exception $e ){} + try{R::exec("SET lock_timeout = '51s';");}catch( \Exception $e ){} + echo "PARENT: PREP START\n"; + R::nuke(); + $i = R::dispense('inventory'); + $i->apples = 10; + R::store($i); + R::exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + echo "PARENT: PREP DONE\n"; + sleep(3); + echo "PARENT: ADDING 5 START\n"; + try { R::exec('SET autocommit = 0'); }catch( \Exception $e ){} + R::freeze( TRUE ); + R::begin(); + $i = R::findForUpdate('inventory', ' id = ? ', array(1)); + $i = reset( $i ); + print_r($i); + $i->apples += 5; + R::store($i); + R::commit(); + echo "PARENT ADDING 5 DONE\n"; + $i = R::getAll('select * from inventory where id = 1'); + print_r($i); + asrt((int)$i[0]['apples'], 13); + R::freeze( FALSE ); + try { R::exec('SET autocommit = 1'); }catch( \Exception $e ){} + pcntl_wait($status); + } + } + + /** + * Test whether we can use setSQLSnippet with find() and batch(). + * + * @return void + */ + public function testBatchAndFind() + { + /* baseline */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::batch( 'bean', $ids ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 0 ); + $logs->clear(); + + /* findOneForUpdate */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::findOneForUpdate( 'bean' ); + asrt( count( $beans ), 1 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 0 ); + $logs->clear(); + + /* findForUpdate */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::findForUpdate( 'bean' ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 0 ); + $logs->clear(); + + /* batch + snippet */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + R::getWriter()->setSQLSelectSnippet('for update'); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::batch( 'bean', $ids ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 1 ); + print_r( $entries ); + $logs->clear(); + + /* baseline */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::batch( 'bean', $ids ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 0 ); + $logs->clear(); + + /* find + snippet */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + R::getWriter()->setSQLSelectSnippet('for update'); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::find( 'bean' ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 1 ); + print_r( $entries ); + $logs->clear(); + + /* baseline */ + R::nuke(); + $beans = R::dispenseAll('bean*10'); + R::storeAll($beans[0]); + $ids = array(); + foreach($beans[0] as $bean) $ids[] = $bean->id; + R::debug( TRUE, 1 ); + $logs = R::getDatabaseAdapter() + ->getDatabase() + ->getLogger(); + $beans = R::batch( 'bean', $ids ); + asrt( count( $beans ), 10 ); + $entries = $logs->grep('for update'); + asrt( count( $entries ), 0 ); + $logs->clear(); + + R::debug( FALSE ); + } + + /** + * loadForUpdate/findForUpdate should be applied even if caching is on. + * Caching may not interfere with locking beans. + * + * @return void + */ + public function testLockAndCache() + { + R::nuke(); + $bean = R::dispense('lock'); + $bean->title = 'lock'; + $id = R::store( $bean ); + R::getWriter()->setUseCache( TRUE ); + $lock = R::loadForUpdate( 'lock', $id ); + R::debug( TRUE, 1 ); + $lock = R::loadForUpdate( 'lock', $id ); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + asrt( count($logs), 1 ); //if no cache clear, then would have been 2 + R::debug( FALSE ); + $lock = R::findForUpdate( 'lock', 'id = ?', array( $id ) ); + R::debug( TRUE, 1 ); + $lock = R::findForUpdate( 'lock', 'id = ?', array( $id ) ); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + asrt( count($logs), 1 ); //if no cache clear, then would have been 2 + R::getWriter()->setUseCache( FALSE ); + R::debug( FALSE ); + R::nuke(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Copy.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Copy.php new file mode 100644 index 0000000..2787ae9 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Copy.php @@ -0,0 +1,224 @@ +ownDocument[] = $document; + R::store( $document ); + $duplicate = R::dup( $document ); + pass(); //if RB cant handle this is will crash (nesting level error from PHP). + $id2 = R::store( $duplicate ); + $duplicate = R::load( 'document', $id ); + asrt( (int) $document->document_id, $id ); + asrt( (int) $duplicate->document_id, $id2 ); + // Export variant + $duplicate = R::exportAll( $document ); + asrt( (int) $duplicate[0]['document_id'], $id ); + } + + /** + * Test real world scenario: Versioning + */ + public function testVersioning() + { + $document = R::dispense( 'document' ); + $page = R::dispense( 'page' ); + $document->title = 'test'; + $page->content = 'lorem ipsum'; + $user = R::dispense( 'user' ); + $user->name = 'Leo'; + $document->sharedUser[] = $user; + $document->ownPage[] = $page; + $document->starship_id = 3; + $document->planet = R::dispense( 'planet' ); + R::store( $document ); + $duplicate = R::dup( $document ); + R::store( $duplicate ); + $duplicate = R::dup( $document ); + R::store( $duplicate ); + asrt( R::count( 'planet' ), 1 ); + asrt( R::count( 'user' ), 1 ); + asrt( R::count( 'document' ), 3 ); + asrt( R::count( 'page' ), 3 ); + asrt( R::count( 'spaceship' ), 0 ); + } + + /** + * Same as above but now with intermediate save, counts must be same + */ + public function testVersioningIntermediateSaves() + { + $document = R::dispense( 'document' ); + $page = R::dispense( 'page' ); + $document->title = 'test'; + $page->content = 'lorem ipsum'; + $user = R::dispense( 'user' ); + $user->name = 'Leo'; + $document->sharedUser[] = $user; + $document->ownPage[] = $page; + $document->starship_id = 3; + $document->planet = R::dispense( 'planet' ); + R::store( $document ); + $duplicate = R::dup( $document ); + R::store( $document ); + R::store( $duplicate ); + R::store( $document ); + $duplicate = R::dup( $document ); + R::store( $document ); + R::store( $duplicate ); + asrt( R::count( 'planet' ), 1 ); + asrt( R::count( 'user' ), 1 ); + asrt( R::count( 'document' ), 3 ); + asrt( R::count( 'page' ), 3 ); + asrt( R::count( 'spaceship' ), 0 ); + // same, but now with intermediate save, counts must be same + R::freeze( TRUE ); + $document = R::dispense( 'document' ); + $page = R::dispense( 'page' ); + $document->title = 'test'; + $page->content = 'lorem ipsum'; + $user = R::dispense( 'user' ); + $user->name = 'Leo'; + $document->sharedUser[] = $user; + $document->ownPage[] = $page; + $document->starship_id = 3; + $document->planet = R::dispense( 'planet' ); + R::store( $document ); + $duplicate = R::dup( $document ); + R::store( $document ); + R::store( $duplicate ); + R::store( $document ); + $duplicate = R::dup( $document ); + R::store( $document ); + R::store( $duplicate ); + asrt( R::count( 'planet' ), 2 ); + asrt( R::count( 'user' ), 2 ); + asrt( R::count( 'document' ), 6 ); + asrt( R::count( 'page' ), 6 ); + try { asrt( R::count( 'spaceship' ), 0 ); }catch(\Exception $e){pass();} + R::freeze( FALSE ); + } + + /** + * Test Recursion + */ + public function testRecursion() + { + list( $d1, $d2 ) = R::dispense( 'document', 2 ); + $page = R::dispense( 'page' ); + list( $p1, $p2 ) = R::dispense( 'paragraph', 2 ); + list( $e1, $e2 ) = R::dispense( 'excerpt', 2 ); + $id2 = R::store( $d2 ); + $p1->name = 'a'; + $p2->name = 'b'; + $page->title = 'my page'; + $page->ownParagraph = array( $p1, $p2 ); + $p1->ownExcerpt[] = $e1; + $p2->ownExcerpt[] = $e2; + $e1->ownDocument[] = $d2; + $e2->ownDocument[] = $d1; + $d1->ownPage[] = $page; + $id1 = R::store( $d1 ); + $d1 = R::load( 'document', $id1 ); + $d = R::dup( $d1 ); + $ids = array(); + asrt( ( $d instanceof OODBBean ), TRUE ); + asrt( count( $d->ownPage ), 1 ); + foreach ( end( $d->ownPage )->ownParagraph as $p ) { + foreach ( $p->ownExcerpt as $e ) { + $ids[] = end( $e->ownDocument )->id; + } + } + sort( $ids ); + asrt( (int) $ids[0], 0 ); + asrt( (int) $ids[1], $id1 ); + R::store( $d ); + pass(); + $phillies = R::dispense( 'diner' ); + list( $lonelyman, $man, $woman ) = R::dispense( 'guest', 3 ); + $attendant = R::dispense( 'employee' ); + $lonelyman->name = 'Bennie Moten'; + $man->name = 'Daddy Stovepipe'; + $woman->name = 'Mississippi Sarah'; + $attendant->name = 'Gus Cannon'; + $phillies->sharedGuest = array( $lonelyman, $man, $woman ); + $phillies->ownEmployee[] = $attendant; + $props = R::dispense( 'prop', 2 ); + $props[0]->kind = 'cigarette'; + $props[1]->kind = 'coffee'; + $thought = R::dispense( 'thought' ); + $thought->content = 'Blues'; + $thought2 = R::dispense( 'thought' ); + $thought2->content = 'Jazz'; + $woman->ownProp[] = $props[0]; + $man->sharedProp[] = $props[1]; + $attendant->ownThought = array( $thought, $thought2 ); + R::store( $phillies ); + $diner = R::findOne( 'diner' ); + $diner2 = R::dup( $diner ); + $id2 = R::store( $diner2 ); + $diner2 = R::load( 'diner', $id2 ); + asrt( count( $diner->ownEmployee ), 1 ); + asrt( count( $diner2->ownEmployee ), 1 ); + asrt( count( $diner->sharedGuest ), 3 ); + asrt( count( $diner2->sharedGuest ), 3 ); + $employee = reset( $diner->ownEmployee ); + asrt( count( $employee->ownThought ), 2 ); + $employee = reset( $diner2->ownEmployee ); + asrt( count( $employee->ownThought ), 2 ); + // Can we change something in the duplicate without changing the original? + $employee->name = 'Marvin'; + $thought = R::dispense( 'thought' ); + $thought->content = 'depression'; + $employee->ownThought[] = $thought; + array_pop( $diner2->sharedGuest ); + $guest = reset( $diner2->sharedGuest ); + $guest->name = 'Arthur Dent'; + $id2 = R::store( $diner2 ); + $diner2 = R::load( 'diner', $id2 ); + asrt( count( $diner->ownEmployee ), 1 ); + asrt( count( $diner2->ownEmployee ), 1 ); + asrt( count( $diner->sharedGuest ), 3 ); + asrt( count( $diner2->sharedGuest ), 2 ); + $employeeOld = reset( $diner->ownEmployee ); + asrt( count( $employeeOld->ownThought ), 2 ); + $employee = reset( $diner2->ownEmployee ); + asrt( count( $employee->ownThought ), 3 ); + asrt( $employee->name, 'Marvin' ); + asrt( $employeeOld->name, 'Gus Cannon' ); + // However the shared beans must not be copied + asrt( R::count( 'guest' ), 3 ); + asrt( R::count( 'guest_prop' ), 1 ); + $arthur = R::findOne( 'guest', ' ' . R::getWriter()->esc( 'name' ) . ' = ? ', array( 'Arthur Dent' ) ); + asrt( $arthur->name, 'Arthur Dent' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Count.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Count.php new file mode 100644 index 0000000..1b83207 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Count.php @@ -0,0 +1,226 @@ +sharedPage = R::dispense( 'page', 10 ); + R::store( $book ); + asrt( R::count('bookPage'), 10 ); + try { + R::count( 'WrongTypeName' ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + try { + R::count( 'wrong_type_name' ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + } + + /** + * Test count and wipe. + * + * @return void + */ + public function testCountAndWipe() + { + testpack( "Test count and wipe" ); + $page = R::dispense( "page" ); + $page->name = "ABC"; + R::store( $page ); + $n1 = R::count( "page" ); + $page = R::dispense( "page" ); + $page->name = "DEF"; + R::store( $page ); + $n2 = R::count( "page" ); + asrt( $n1 + 1, $n2 ); + R::wipe( "page" ); + asrt( R::count( "page" ), 0 ); + asrt( R::getRedBean()->count( "page" ), 0 ); + asrt( R::getRedBean()->count( "kazoo" ), 0 ); // non existing table + R::freeze( TRUE ); + try { + asrt( R::getRedBean()->count( "kazoo" ), 0 ); // non existing table + fail(); + } catch( \Exception $e ) { + pass(); + } + R::freeze( FALSE ); + $page = R::dispense( 'page' ); + $page->name = 'foo'; + R::store( $page ); + $page = R::dispense( 'page' ); + $page->name = 'bar'; + R::store( $page ); + asrt( R::count( 'page', ' name = ? ', array( 'foo' ) ), 1 ); + // Now count something that does not exist, this should return 0. (just be polite) + asrt( R::count( 'teapot', ' name = ? ', array( 'flying' ) ), 0 ); + asrt( R::count( 'teapot' ), 0 ); + $currentDriver = $this->currentlyActiveDriverID; + // Some drivers don't support that many error codes. + if ( $currentDriver === 'mysql' || $currentDriver === 'postgres' ) { + try { + R::count( 'teaport', ' for tea ' ); + fail(); + } catch ( SQL $e ) { + pass(); + } + } + } + + /** + * Can we count the number of shared beans? + * + * @return void + */ + public function testCountShared() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->sharedPageList = R::dispense( 'page', 5 ); + R::store( $book ); + asrt( $book->countShared('page'), 5 ); + asrt( $book->countShared('leaflet'), 0 ); + asrt( R::dispense( 'book' )->countShared('page'), 0 ); + $am = R::getRedBean()->getAssociationManager(); + asrt( $am->relatedCount( R::dispense( 'book' ), 'page' ), 0); + try { + $am->relatedCount( 'not a bean', 'type' ); + fail(); + } catch( RedException $e ) { + pass(); + } + R::getWriter()->setUseCache(TRUE); + asrt( $book->countShared('page'), 5 ); + R::exec('DELETE FROM book_page WHERE book_id > 0 -- keep-cache'); + asrt( $book->countShared('page'), 5 ); + R::getWriter()->setUseCache(FALSE); + asrt( $book->countShared('page'), 0 ); + } + + /** + * Test $bean->countOwn($type); + * + * @return void + */ + public function testCountOwn() + { + R::nuke(); + $book = R::dispense( 'book' ); + $empty = R::dispense( 'book' ); + $nothing = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->ownPageList[] = $page; + R::store( $book ); + R::store( $empty ); + OODBBean::useFluidCount( FALSE ); + asrt( $book->countOwn('page'), 1 ); + asrt( $empty->countOwn('page'), 0 ); + asrt( $nothing->countOwn('page'), 0 ); + $old = OODBBean::useFluidCount( TRUE ); + asrt( $old, FALSE ); + asrt( $book->countOwn('page'), 1 ); + asrt( $empty->countOwn('page'), 0 ); + asrt( $nothing->countOwn('page'), 0 ); + R::freeze( TRUE ); + asrt( $book->countOwn('page'), 1 ); + asrt( $empty->countOwn('page'), 0 ); + asrt( $nothing->countOwn('page'), 0 ); + R::freeze( FALSE ); + R::nuke(); + asrt( $empty->countOwn('page'), 0 ); + asrt( $nothing->countOwn('page'), 0 ); + R::freeze( TRUE ); + asrt( $nothing->countOwn('page'), 0 ); + try { asrt( $empty->countOwn('page'), 0 ); fail(); } catch(\Exception $e) { pass(); } + try { asrt( $book->countOwn('page'), 0 ); fail(); } catch(\Exception $e) { pass(); } + R::freeze( FALSE ); + OODBBean::useFluidCount( FALSE ); + try { asrt( $empty->countOwn('page'), 0 ); fail(); } catch(\Exception $e) { pass(); } + try { asrt( $book->countOwn('page'), 0 ); fail(); } catch(\Exception $e) { pass(); } + OODBBean::useFluidCount( TRUE ); + } + + /** + * Test $bean->withCondition( ... )->countOwn( $type ); + * + * @return void + */ + public function testCountWithCondition() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->ownPageList[] = R::dispense( 'page' ); + R::store( $book ); + OODBBean::useFluidCount( FALSE ); + $count = $book + ->withCondition(' id > :id ', array( ':id' => 0 ) ) + ->countOwn('page'); + asrt( $count, 1 ); + $count = $book + ->withCondition(' id > ? ', array( 0 ) ) + ->countOwn('page'); + asrt( $count, 1 ); + $count = $book + ->withCondition(' id < :id ', array( ':id' => 0 ) ) + ->countOwn('page'); + asrt( $count, 0 ); + $count = $book + ->withCondition(' id < ? ', array( 0 ) ) + ->countOwn('page'); + asrt( $count, 0 ); + OODBBean::useFluidCount( TRUE ); + $count = $book + ->withCondition(' id > :id ', array( ':id' => 0 ) ) + ->countOwn('page'); + asrt( $count, 1 ); + $count = $book + ->withCondition(' id > ? ', array( 0 ) ) + ->countOwn('page'); + asrt( $count, 1 ); + $count = $book + ->withCondition(' id < :id ', array( ':id' => 0 ) ) + ->countOwn('page'); + asrt( $count, 0 ); + $count = $book + ->withCondition(' id < ? ', array( 0 ) ) + ->countOwn('page'); + asrt( $count, 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cross.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cross.php new file mode 100644 index 0000000..81d3fee --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cross.php @@ -0,0 +1,474 @@ +name = 'Page 3'; + $book->xownPageList[] = $page; + R::store( $book ); + $book = $book->fresh(); + $texts = $book->aggr( 'ownPageList', 'text' ); + pass(); + asrt( count( $texts ), 0 ); + asrt( is_array( $texts ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $page1 = R::dispense( 'page' ); + $page1->name = 'Page 1'; + $text1 = R::dispense('text'); + $text1->content = 'Text 1'; + $page1->text = $text1; + $book->xownPageList[] = $page1; + $page2 = R::dispense( 'page' ); + $page2->name = 'Page 2'; + $text2 = R::dispense( 'text' ); + $text2->content = 'Text 2'; + $page2->text = $text2; + $book->xownPageList[] = $page2; + $page3 = R::dispense( 'page' ); + $page3->name = 'Page 3'; + $book->xownPageList[] = $page3; + R::store( $book ); + $book = $book->fresh(); + $texts = $book->aggr( 'ownPageList', 'text' ); + pass(); + asrt( count( $texts ), 2 ); + } + + /** + * Test many different scenarios with self referential + * many-to-many relations. + * + * @return void + */ + public function testSelfReferentialCRUD() + { + R::nuke(); + $buddies = R::dispense( 'buddy', 4 ); + $buddies[0]->name = 'A'; + $buddies[1]->name = 'B'; + $buddies[2]->name = 'C'; + $buddies[3]->name = 'D'; + $buddies[0]->sharedBuddyList = array( $buddies[1], $buddies[2] ); + $buddies[3]->sharedBuddyList = array( $buddies[2] ); + R::storeAll( $buddies ); + $buddies[0] = $buddies[0]->fresh(); + asrt( count( $buddies[0]->sharedBuddyList ), 2 ); + //does this yield valid combinations - cross relations / self ref n-m + //need to symmetric.... + $names = R::gatherLabels( $buddies[0]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'B,C' ); + unset( $buddies[0]->sharedBuddy ); + R::storeAll( $buddies ); + $buddies[0] = $buddies[0]->fresh(); + asrt( count( $buddies[0]->sharedBuddyList ), 2 ); + $buddies[3] = $buddies[3]->fresh(); + asrt( count( $buddies[3]->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddies[3]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'C' ); + $buddies[2] = $buddies[2]->fresh(); + asrt( count( $buddies[2]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[2]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'A,D' ); + $buddies[1] = $buddies[1]->fresh(); + asrt( count( $buddies[1]->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddies[1]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'A' ); + //Can we add one? + $buddies[1]->sharedBuddyList[] = R::dispense('buddy')->setAttr('name', 'E'); + R::store( $buddies[1] ); + $buddies[0] = $buddies[0]->fresh(); + asrt( count( $buddies[0]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[0]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'B,C' ); + $buddies[1] = $buddies[1]->fresh(); + asrt( count( $buddies[1]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[1]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'A,E' ); + $all = R::find( 'buddy' ); + asrt( count( $all ), 5 ); + foreach( $buddies[1]->sharedBuddy as $buddy ) { + if ( $buddy->name === 'E' ) { + $buddyE = $buddy; + } + } + asrt( isset( $buddyE ), TRUE ); + asrt( $buddyE->name, 'E' ); + //can we update? + foreach( $buddies[0]->sharedBuddy as $buddy ) { + if ( $buddy->name === 'C' ) { + $buddy->name = 'C2'; + } + } + R::store( $buddies[0] ); + $buddies[0] = $buddies[0]->fresh(); + asrt( count( $buddies[0]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[0]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'B,C2' ); + $buddies[2] = $buddies[2]->fresh(); + asrt( count( $buddies[2]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[2]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'A,D' ); + //can we delete? + foreach( $buddies[0]->sharedBuddyList as $id => $buddy ) { + if ( $buddy->name === 'B' ) { + unset( $buddies[0]->sharedBuddyList[$id] ); + } + } + R::store( $buddies[0] ); + $buddies[0] = $buddies[0]->fresh(); + asrt( count( $buddies[0]->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddies[0]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'C2' ); + $buddies[1] = $buddies[1]->fresh(); + asrt( count( $buddies[1]->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddies[1]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'E' ); + asrt( R::count( 'buddy' ), 5 ); + asrt( R::count( 'buddyBuddy' ), 3 ); + $buddies[3] = $buddies[3]->fresh(); + asrt( count( $buddies[3]->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddies[3]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'C2' ); + $buddies[2] = $buddies[2]->fresh(); + asrt( count( $buddies[2]->sharedBuddyList ), 2 ); + $names = R::gatherLabels( $buddies[2]->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'A,D' ); + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->sharedBuddyList ), 1 ); + $names = R::gatherLabels( $buddyE->sharedBuddyList ); + sort( $names ); + $names = implode( ',', $names ); + asrt( $names, 'B' ); + if ( $this->currentlyActiveDriverID === 'mysql' ) { + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->with(' HAVING linked_by > 0 ')->sharedBuddyList ), 1 ); + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->with(' HAVING linked_by < 0 ')->sharedBuddyList ), 0 ); + } + if ( $this->currentlyActiveDriverID === 'sqlite' ) { + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->withCondition(' linked_by > 0 ')->sharedBuddyList ), 1 ); + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->withCondition(' linked_by < 0 ')->sharedBuddyList ), 0 ); + } + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->withCondition(' buddy_buddy.buddy_id > 0 AND buddy_buddy.buddy2_id > 0 ')->sharedBuddyList ), 1 ); + $buddyE = $buddyE->fresh(); + asrt( count( $buddyE->withCondition(' buddy_buddy.buddy_id < 0 AND buddy_buddy.buddy2_id < 0 ')->sharedBuddyList ), 0 ); + } + + /** + * Test self referential N-M relations (page_page). + * + * @return void + */ + public function testSelfReferential() + { + $page = R::dispense( 'page' )->setAttr( 'title', 'a' ); + $page->sharedPage[] = R::dispense( 'page' )->setAttr( 'title', 'b' ); + R::store( $page ); + $page = $page->fresh(); + $page = reset( $page->sharedPage ); + asrt( $page->title, 'b' ); + $tables = array_flip( R::inspect() ); + asrt( isset( $tables['page_page'] ), TRUE ); + $columns = R::inspect( 'page_page' ); + asrt( isset( $columns['page2_id'] ), TRUE ); + } + + /** + * Test the unique constraint. + * Just want to make sure it is not too limiting and functions + * properly for typical RedBeanPHP usage. + * + * @return void + */ + public function testUnique() + { + R::nuke(); + $page1 = R::dispense( 'page' ); + $tag1 = R::dispense( 'tag' ); + $page2 = R::dispense( 'page' ); + $tag2 = R::dispense( 'tag' ); + $page3 = R::dispense( 'page' ); + $tag3 = R::dispense( 'tag' ); + $page1->sharedTag[] = $tag1; + R::store( $page1 ); + //can we save all combinations with unique? + asrt( R::count( 'pageTag' ), 1); + $page1->sharedTag[] = $tag2; + R::store( $page1 ); + asrt( R::count( 'pageTag' ), 2 ); + $page1->sharedTag[] = $tag3; + $page2->sharedTag[] = $tag1; + $page2->sharedTag[] = $tag2; + $page2->sharedTag[] = $tag3; + $page3->sharedTag[] = $tag1; + $page3->sharedTag[] = $tag2; + $page3->sharedTag[] = $tag3; + R::storeAll( array( $page1, $page2, $page3 ) ); + asrt( R::count('pageTag'), 9 ); + $page1 = $page1->fresh(); + $page1->sharedTag[] = $tag3; + R::store( $page1 ); + //cant add violates unique + asrt( R::count( 'pageTag' ), 9 ); + } + + /** + * Shared lists can only be formed using types. + * If you happen to have two beans of the same type you can still + * have a shared list but not with a sense of direction. + * I.e. quest->sharedQuest returns all the quests that follow the first one, + * but also the ones that are followed by the first one. + * If you want to have some sort of direction; i.e. one quest follows another one + * you'll have to use an alias: quest->target, but now you can't use the shared list + * anymore because it will start looking for a type named 'target' (which is just an alias) + * for quest, but it cant find that table and it's not possible to 'keep remembering' + * the alias throughout the system. + * + * The aggr() method solves this inconvenience. + * Aggr iterates through the list identified by its first argument ('target' -> ownQuestTargetList) + * and fetches every ID of the target (quest_target.target_id), loads these beans in batch for + * optimal performance, puts them back in the beans (questTarget->target) and returns the + * references. + * + * @return void + */ + public function testAggregationInSelfRefNM() + { + R::nuke(); + $quest1 = R::dispense( 'quest' ); + $quest1->name = 'Quest 1'; + $quest2 = R::dispense( 'quest' ); + $quest2->name = 'Quest 2'; + $quest3 = R::dispense( 'quest' ); + $quest3->name = 'Quest 3'; + $quest4 = R::dispense( 'quest' ); + $quest4->name = 'Quest 4'; + $quest1->link( 'questTarget' )->target = $quest2; + $quest1->link( 'questTarget' )->target = $quest3; + $quest3->link( 'questTarget' )->target = $quest4; + $quest3->link( 'questTarget' )->target = $quest1; + R::storeAll( array( $quest1, $quest3 ) ); + //There should be 4 links + asrt( (int) R::count('questTarget'), 4 ); + $quest1 = $quest1->fresh(); + $targets = $quest1->aggr( 'ownQuestTargetList', 'target', 'quest' ); + //can we aggregate the targets over the link type? + asrt( count( $targets), 2 ); + //are the all valid beans? + foreach( $targets as $target ) { + //are they beans? + asrt( ( $target instanceof OODBBean ), TRUE ); + //are they fetched as quest? + asrt( ( $target->getMeta( 'type' ) ), 'quest' ); + } + //list target should already have been loaded, nuke has no effect + R::nuke(); + $links = $quest1->ownQuestTargetList; + //are the links still there, have they been set in the beans as well? + asrt( count( $links ), 2); + //are they references instead of copies, changes in the aggregation set should affect the beans in links! + foreach( $targets as $target ) { + $target->name .= 'b'; + asrt( substr( $target->name, -1 ), 'b' ); + } + //do the names end with a 'b' here as well ? i.e. have they been changed through references? + foreach( $links as $link ) { + asrt( substr( $target->name, -1 ), 'b' ); + } + //now test the effect on existing shadow... + R::nuke(); + $quest1 = R::dispense('quest'); + $quest1->name = 'Quest 1'; + $quest2 = R::dispense('quest'); + $quest2->name = 'Quest 2'; + $quest3 = R::dispense('quest'); + $quest3->name = 'Quest 3'; + $quest4 = R::dispense('quest'); + $quest4->name = 'Quest 4'; + $quest1->link( 'questTarget' )->target = $quest2; + $quest1->link( 'questTarget' )->target = $quest3; + R::store($quest1); + asrt( (int) R::count( 'questTarget' ), 2 ); + //now lets first build a shadow + $quest1->link( 'questTarget' )->target = $quest4; + //$quest1 = $quest1->fresh(); + $targets = $quest1->aggr( 'ownQuestTargetList', 'target', 'quest' ); + //targets should not include the new bean... + asrt( count($targets), 2 ); + //this should not overwrite the existing beans + R::store($quest1); + asrt( (int) R::count( 'questTarget' ), 3 ); + } + + /** + * Test aggr without the aliasing. + * + * @return void + */ + public function testAggrBasic() + { + R::nuke(); + $book = R::dispense( 'book' ); + $page1 = R::dispense( 'page' ); + $page1->name = 'Page 1'; + $text1 = R::dispense('text'); + $text1->content = 'Text 1'; + $page1->text = $text1; + $book->xownPageList[] = $page1; + $page2 = R::dispense( 'page' ); + $page2->name = 'Page 2'; + $text2 = R::dispense( 'text' ); + $text2->content = 'Text 2'; + $page2->text = $text2; + $book->xownPageList[] = $page2; + R::store( $book ); + $book = $book->fresh(); + $texts = $book->aggr( 'ownPageList', 'text' ); + R::nuke(); + asrt( count( $texts ), 2 ); + foreach( $texts as $text ) { + asrt( ( $text instanceof OODBBean ), TRUE ); + } + $pages = $book->ownPageList; + asrt( count( $pages ), 2 ); + asrt( R::count( 'page' ), 0 ); + foreach( $pages as $page ) { + asrt( ( $page instanceof OODBBean ), TRUE ); + $text = $page->text; + asrt( ( $text instanceof OODBBean ), TRUE ); + $text->content = 'CHANGED'; + } + foreach( $texts as $text ) { + asrt( $text->content, 'CHANGED' ); + } + } + + /** + * Test aggr with basic aliasing. + * + * @return void + */ + public function testAggrWithOnlyAlias() + { + R::nuke(); + $book = R::dispense( 'book' ); + $page1 = R::dispense( 'page' ); + $page1->name = 'Page 1'; + $text1 = R::dispense( 'text' ); + $text1->content = 'Text 1'; + $page1->content = $text1; + $book->xownPageList[] = $page1; + $page2 = R::dispense( 'page' ); + $page2->name = 'Page 2'; + $text2 = R::dispense( 'text' ); + $text2->content = 'Text 2'; + $page2->content = $text2; + $book->xownPageList[] = $page2; + R::store( $book ); + $book = $book->fresh(); + $texts = $book->aggr( 'ownPageList', 'content', 'text' ); + R::nuke(); + asrt( count( $texts ), 2 ); + foreach( $texts as $text ) { + asrt( ( $text instanceof OODBBean), TRUE ); + } + $pages = $book->ownPageList; + asrt( count( $pages ), 2 ); + asrt( R::count( 'page' ), 0 ); + foreach( $pages as $page ) { + asrt( ( $page instanceof OODBBean ), TRUE ); + $text = $page->content; + asrt( ( $text instanceof OODBBean ), TRUE ); + $text->content = 'CHANGED'; + } + foreach( $texts as $text ) { + asrt( $text->content, 'CHANGED' ); + } + } + + /** + * The aggr method can only be used with own-list. + * + * @return void + */ + public function testErrorHandlingAggr() + { + $wrongLists = array( 'not-an-own-list', 'OWNlist', 'Ownpage', 'ownbook', 'own book', '!', 'sharedBook' ); + foreach( $wrongLists as $wrongList ) { + $bean = R::dispense( 'bean' ); + try { + $bean->aggr( $wrongList, 'field' ); + fail(); + } catch ( \Exception $exception ) { + pass(); + asrt( ( $exception instanceof RedException ), TRUE ); + } + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cursors.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cursors.php new file mode 100644 index 0000000..2272118 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Cursors.php @@ -0,0 +1,160 @@ +number = $i; + $page->content = sha1( $i ); + R::store( $page ); + } + $cursor = R::getCursor( 'SELECT * FROM page ORDER BY page.number ASC' ); + asrt( get_class( $cursor ), 'RedBeanPHP\Cursor\PDOCursor'); + $i = 0; + $list = array(); + while( $row = $cursor->getNextItem() ) { + asrt( is_array( $row ), TRUE ); + asrt( (string) $row['number'], strval( $i ) ); + asrt( $row['content'], sha1( $i ) ); + $list[] = $row['content']; + $i ++; + } + } + + /** + * Test basic cursor functionality. + * + * @return void + */ + public function testBasicCursors() + { + R::nuke(); + for( $i=0; $i<20; $i++ ) { + $page = R::dispense( 'page' ); + $page->number = $i; + $page->content = sha1( $i ); + R::store( $page ); + } + $collection = R::findCollection( 'page', 'ORDER BY page.number ASC' ); + asrt( get_class( $collection ), 'RedBeanPHP\BeanCollection'); + $i = 0; + $list = array(); + while( $bean = $collection->next() ) { + asrt( ( $bean instanceof OODBBean ), TRUE ); + asrt( (string) $bean->number, strval( $i ) ); + asrt( $bean->content, sha1( $i ) ); + $list[] = $bean->content; + $i ++; + } + $collection->reset(); + $i = 0; + while( $bean = $collection->next() ) { + asrt( ( $bean instanceof OODBBean ), TRUE ); + asrt( (string) $bean->number, strval( $i ) ); + asrt( $bean->content, sha1( $i ) ); + $i ++; + } + $collection = R::findCollection( 'page', ' ORDER BY content ASC ' ); + sort( $list ); + $i = 0; + while( $bean = $collection->next() ) { + asrt( $bean->content, $list[$i] ); + $i ++; + } + $collection = R::findCollection( 'page', ' ORDER BY content ASC LIMIT 5 ' ); + sort( $list ); + $i = 0; + while( $bean = $collection->next() ) { + asrt( $bean->content, $list[$i] ); + $i ++; + if ( $i > 5 ) break; + } + $key = array_rand( $list ); + $content = $list[ $key ]; + $collection = R::findCollection( 'page', ' content = ? ', array( $content ) ); + $bean = $collection->next(); + asrt( $bean->content, $content ); + $collection->close(); + } + + /** + * Can we use a filtered cursor? + * + * @return void + */ + public function testCursorWithFilter() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->title = 'Title'; + R::store( $book ); + $filter = array( + QueryWriter::C_SQLFILTER_READ => array( + 'book' => array('title' => ' LOWER(book.title) ' ) + ) + ); + AQueryWriter::setSQLFilters( $filter ); + $books = R::findCollection( 'book' ); + $book = $books->next(); + asrt( $book->title, 'title' ); + AQueryWriter::setSQLFilters( array() ); + $books = R::findCollection( 'book' ); + $book = $books->next(); + asrt( $book->title, 'Title' ); + } + + /** + * Test empty collections (NULLCursor). + * + * @return void + */ + public function testEmptyCollection() + { + R::nuke(); + $page = R::dispense( 'page' ); + $page->content = 'aaa'; + R::store( $page ); + $collection = R::findCollection( 'page' ); + asrt( get_class( $collection ), 'RedBeanPHP\BeanCollection'); + $collection = R::findCollection( 'page', ' content = ?', array( 'bbb' ) ); + asrt( get_class( $collection ), 'RedBeanPHP\BeanCollection'); + asrt( is_null( $collection->next() ), TRUE ); + $collection = R::findCollection( 'something' ); + asrt( get_class( $collection ), 'RedBeanPHP\BeanCollection'); + asrt( is_null( $collection->next() ), TRUE ); + asrt( is_null( $collection->reset() ), TRUE ); + $collection->close(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Database.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Database.php new file mode 100644 index 0000000..979ebf8 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Database.php @@ -0,0 +1,461 @@ +currentlyActiveDriverID == 'pgsql') { + R::getWriter()->setSQLFilters(array('r'=>array('book'=>array('__meta_total'=>'COUNT(*) OVER()'))), FALSE); + } else { + R::getWriter()->setSQLFilters(array('r'=>array('book'=>array('__meta_total'=>'2'))), FALSE); + } + $books = R::find('book', 'LIMIT 1'); + $book = reset($books); + $bundle = $book->getMeta('data.bundle'); + asrt(intval($bundle['__meta_total']),2); + R::getWriter()->setSQLFilters(array(), FALSE); + } + + /** + * Test whether we cannot just bind function names but + * also function templates, i.e. little SQL snippets. + * + * @return void + */ + public function testBindFuncFunctionTemplates() + { + R::bindFunc('read', 'xbean.lucky', '111 * %s', TRUE); + $bean = R::dispense('xbean'); + $bean->lucky = 7; + $id = R::store( $bean ); + $bean = R::load( 'xbean', $id ); + asrt( intval($bean->lucky), 777 ); + R::bindFunc('write', 'xbean.triple', '3 * %s', TRUE); + $bean->triple = 3; + R::store($bean); + $bean = $bean->fresh(); + asrt( intval($bean->triple), 9); + R::bindFunc('read', 'xbean.lucky', NULL); + R::bindFunc('write', 'xbean.triple', NULL); + R::getRedBean()->clearAllFuncBindings(); + } + + /** + * Make ConvertToBean work together with getRow #759. + * When no results are found for getRow it returns [] + * Then when you give that to convertToBean it wraps your + * single row into an array of multiple rows, so you get [[]]. + * Then this loop has something to + * iterate on foreach ( $rows as $row ) { ... + * And then it crashes on: $id = $row['id']; + */ + public function testHarmonizeConvertToBeanAndGetRow() + { + R::nuke(); + $book = R::convertToBean( 'book', R::getRow( 'SELECT * FROM book' ) ); + asrt( is_null( $book ), TRUE ); + $book = R::convertToBean( 'book', array() ); + asrt( is_null( $book ), TRUE ); + } + + /** + * Test for bugfix: + * adhere to return type specification for R::getRow #728 + * public static function getRow is documented as a function + * that returns an array. However, in a situation + * where the resultset is empty, this returns a boolean + * and causes an unexpected failure in + * code like this because it is expecting an array. + */ + public function testReturnTypeGetRow() + { + R::nuke(); + $book = R::dispense( 'book' ); + R::store( $book ); + $row = R::getRow('SELECT * FROM book'); + asrt( is_array( $row ), TRUE ); + R::trash( $book ); + $row = R::getRow('SELECT * FROM book'); + asrt( is_array( $row ), TRUE ); + R::nuke(); + $row = R::getRow('SELECT * FROM book'); + asrt( is_array( $row ), TRUE ); + } + + /** + * Test the (protected) database capability checker method + * of the RedBeanPHP PDO driver (RPDO). + */ + public function testDatabaseCapabilityChecker() + { + $capChecker = new \DatabaseCapabilityChecker( R::getDatabaseAdapter()->getDatabase()->getPDO() ); + $result = $capChecker->checkCapability('creativity'); + asrt( $result, FALSE ); /* nope, no strong AI yet.. */ + } + + /** + * Test whether we can obtain the PDO object from the + * database driver for custom database operations. + * + * @return void + */ + public function testGetPDO() + { + $driver = R::getDatabaseAdapter(); + asrt( ( $driver instanceof DBAdapter), TRUE ); + $pdo = $driver->getDatabase()->getPDO(); + asrt( ( $pdo instanceof \PDO ), TRUE ); + $pdo2 = R::getPDO(); + asrt( ( $pdo2 instanceof \PDO ), TRUE ); + asrt( ( $pdo === $pdo2 ), TRUE ); + } + + /** + * Test setter maximum integer bindings. + * + * @return void + */ + public function testSetMaxBind() + { + $driver = R::getDatabaseAdapter()->getDatabase(); + $old = $driver->setMaxIntBind( 10 ); + //use SQLite to confirm... + if ( $this->currentlyActiveDriverID === 'sqlite' ) { + $type = R::getCell( 'SELECT typeof( ? ) ', array( 11 ) ); + asrt( $type, 'text' ); + $type = R::getCell( 'SELECT typeof( ? ) ', array( 10 ) ); + asrt( $type, 'integer' ); + $type = R::getCell( 'SELECT typeof( ? ) ', array( 9 ) ); + asrt( $type, 'integer' ); + } + $new = $driver->setMaxIntBind( $old ); + asrt( $new, 10 ); + try { + $driver->setMaxIntBind( '10' ); + fail(); + } catch( RedException $e ) { + pass(); + } + $new = $driver->setMaxIntBind( $old ); + asrt( $new, $old ); + $new = $driver->setMaxIntBind( $old ); + asrt( $new, $old ); + } + + /** + * Can we use colons in SQL? + * + * @return void + */ + public function testColonsInSQL() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->title = 'About :'; + R::store( $book ); + pass(); + $book = R::findOne( 'book', ' title LIKE :this ', array( + ':this' => 'About :' + ) ); + asrt( ( $book instanceof OODBBean ), TRUE ); + //without the colon? + $book = R::findOne( 'book', ' title LIKE :this ', array( + 'this' => 'About :' + ) ); + asrt( ( $book instanceof OODBBean ), TRUE ); + $book = R::findOne( 'book', ' title LIKE :this ', array( + ':this' => '%:%' + ) ); + asrt( ( $book instanceof OODBBean ), TRUE ); + $book = R::findOne( 'book', ' title LIKE :this OR title LIKE :that', array( + 'this' => '%:%', ':that' => 'That' + ) ); + asrt( ( $book instanceof OODBBean ), TRUE ); + $records = R::getAll('SELECT * FROM book WHERE title LIKE :this', array( ':this' => 'About :' ) ); + asrt( count( $records ), 1 ); + $records = R::getAll('SELECT * FROM book WHERE title LIKE :this', array( 'this' => 'About :' ) ); + asrt( count( $records ), 1 ); + $records = R::getAll('SELECT * FROM book WHERE title LIKE :this OR title LIKE :that', array( ':this' => 'About :', ':that' => 'That' ) ); + asrt( count( $records ), 1 ); + $records = R::getRow('SELECT * FROM book WHERE title LIKE :this', array( ':this' => 'About :' ) ); + asrt( count( $records ), 2 ); + $records = R::getRow('SELECT * FROM book WHERE title LIKE :this', array( 'this' => 'About :' ) ); + asrt( count( $records ), 2 ); + $records = R::getRow('SELECT * FROM book WHERE title LIKE :this OR title LIKE :that', array( ':this' => 'About :', ':that' => 'That' ) ); + asrt( count( $records ), 2 ); + } + + /** + * Test setting direct PDO. + * Not much to test actually. + * + * @return void + */ + public function testDirectPDO() + { + $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + R::getDatabaseAdapter()->getDatabase()->setPDO( $pdo ); + pass(); + } + + /** + * Test for testConnection() method. + * + * @return void + */ + public function testConnectionTester() + { + asrt( R::testConnection(), TRUE ); + } + + /** + * Tests the various ways to fetch (select queries) + * data using adapter methods in the facade. + * Also tests the new R::getAssocRow() method, + * as requested in issue #324. + */ + public function testFetchTypes() + { + R::nuke(); + $page = R::dispense( 'page' ); + $page->a = 'a'; + $page->b = 'b'; + R::store( $page ); + $page = R::dispense( 'page' ); + $page->a = 'c'; + $page->b = 'd'; + R::store( $page ); + $expect = '[{"id":"1","a":"a","b":"b"},{"id":"2","a":"c","b":"d"}]'; + asrt( json_encode( R::getAll( 'SELECT * FROM page' ) ), $expect ); + $expect = '{"1":"a","2":"c"}'; + asrt( json_encode( R::getAssoc( 'SELECT id, a FROM page' ) ), $expect ); + $expect = '{"1":{"a":"a","b":"b"},"2":{"a":"c","b":"d"}}'; + asrt( json_encode( R::getAssoc( 'SELECT id, a, b FROM page' ) ), $expect ); + $expect = '[{"id":"1","a":"a"},{"id":"2","a":"c"}]'; + asrt( json_encode( R::getAssocRow( 'SELECT id, a FROM page' ) ), $expect ); + $expect = '[{"id":"1","a":"a","b":"b"},{"id":"2","a":"c","b":"d"}]'; + asrt( json_encode( R::getAssocRow( 'SELECT id, a, b FROM page' ) ), $expect ); + $expect = '{"id":"1","a":"a","b":"b"}'; + asrt( json_encode( R::getRow( 'SELECT * FROM page WHERE id = 1' ) ), $expect ); + $expect = '"a"'; + asrt( json_encode( R::getCell( 'SELECT a FROM page WHERE id = 1' ) ), $expect ); + $expect = '"b"'; + asrt( json_encode( R::getCell( 'SELECT b FROM page WHERE id = 1') ), $expect ); + $expect = '"c"'; + asrt( json_encode( R::getCell('SELECT a FROM page WHERE id = 2') ), $expect ); + $expect = '["a","c"]'; + asrt( json_encode( R::getCol( 'SELECT a FROM page' ) ), $expect ); + $expect = '["b","d"]'; + asrt( json_encode( R::getCol('SELECT b FROM page') ), $expect ); + } + + /** + * Tests whether we can store an empty bean. + * An empty bean has no properties, only ID. Normally we would + * skip the ID field in an INSERT, this test forces the driver + * to specify a value for the ID field. Different writers have to + * use different values: Mysql uses NULL to insert a new auto-generated ID, + * while Postgres has to use DEFAULT. + */ + public function testEmptyBean() + { + testpack( 'Test Empty Bean Storage.' ); + R::nuke(); + $bean = R::dispense( 'emptybean' ); + $id = R::store( $bean ); + asrt( ( $id > 0 ), TRUE ); + asrt( R::count( 'emptybean' ), 1 ); + $bean = R::dispense( 'emptybean' ); + $id = R::store( $bean ); + asrt( ( $id > 0 ), TRUE ); + asrt( R::count( 'emptybean' ), 2 ); + //also test in frozen mode + R::freeze( TRUE ); + $bean = R::dispense( 'emptybean' ); + $id = R::store( $bean ); + asrt( ( $id > 0 ), TRUE ); + asrt( R::count( 'emptybean' ), 3 ); + R::freeze( FALSE ); + } + + /** + * Test the database driver and low level functions. + * + * @return void + */ + public function testDriver() + { + $currentDriver = $this->currentlyActiveDriverID; + R::store( R::dispense( 'justabean' ) ); + $adapter = new TroubleDapter( R::getToolBox()->getDatabaseAdapter()->getDatabase() ); + $adapter->setSQLState( 'HY000' ); + $writer = new SQLiteT( $adapter ); + $redbean = new OODB( $writer ); + $toolbox = new ToolBox( $redbean, $adapter, $writer ); + // We can only test this for a known driver... + if ( $currentDriver === 'sqlite' ) { + try { + $redbean->find( 'bean' ); + pass(); + } catch (\Exception $e ) { + var_dump( $e->getSQLState() ); + fail(); + } + } + $adapter->setSQLState( -999 ); + try { + $redbean->find( 'bean' ); + fail(); + } catch (\Exception $e ) { + pass(); + } + try { + $redbean->wipe( 'justabean' ); + fail(); + } catch (\Exception $e ) { + pass(); + } + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $page = $redbean->dispense( "page" ); + try { + $adapter->exec( "an invalid query" ); + fail(); + } catch ( SQL $e ) { + pass(); + } + // Special data type description should result in magic number 99 (specified) + if ( $currentDriver == 'mysql' ) { + asrt( $writer->code( MySQL::C_DATATYPE_SPECIAL_DATE ), 99 ); + } + if ( $currentDriver == 'pgsql' ) { + asrt( $writer->code( PostgreSQL::C_DATATYPE_SPECIAL_DATE ), 99 ); + } + if ( $currentDriver == 'CUBRID' ) { + asrt( $writer->code( CUBRID::C_DATATYPE_SPECIAL_DATE ), 99 ); + } + asrt( (int) $adapter->getCell( "SELECT 123" ), 123 ); + $page->aname = "my page"; + $id = (int) $redbean->store( $page ); + asrt( (int) $page->id, 1 ); + asrt( (int) $pdo->GetCell( "SELECT count(*) FROM page" ), 1 ); + asrt( $pdo->GetCell( "SELECT aname FROM page LIMIT 1" ), "my page" ); + asrt( (int) $id, 1 ); + $page = $redbean->load( "page", 1 ); + asrt( $page->aname, "my page" ); + asrt( ( (bool) $page->getMeta( "type" ) ), TRUE ); + asrt( isset( $page->id ), TRUE ); + asrt( ( $page->getMeta( "type" ) ), "page" ); + asrt( (int) $page->id, $id ); + } + + /** + * Test selecting. + * + * @return void + */ + public function testSelects() + { + $rooms = R::dispense( 'room', 2 ); + $rooms[0]->kind = 'suite'; + $rooms[1]->kind = 'classic'; + $rooms[0]->number = 6; + $rooms[1]->number = 7; + R::store( $rooms[0] ); + R::store( $rooms[1] ); + $rooms = R::getAssoc('SELECT * FROM room WHERE id < -999'); + asrt(is_array($rooms), TRUE); + asrt(count($rooms), 0); + $rooms = R::getAssoc( 'SELECT ' . R::getWriter()->esc( 'number' ) . ', kind FROM room ORDER BY kind ASC' ); + foreach ( $rooms as $key => $room ) { + asrt( ( $key === 6 || $key === 7 ), TRUE ); + asrt( ( $room == 'classic' || $room == 'suite' ), TRUE ); + } + $rooms = R::getDatabaseAdapter()->getAssoc( 'SELECT kind FROM room' ); + foreach ( $rooms as $key => $room ) { + asrt( ( $room == 'classic' || $room == 'suite' ), TRUE ); + asrt( $room, $key ); + } + $rooms = R::getAssoc( 'SELECT `number`, kind FROM rooms2 ORDER BY kind ASC' ); + asrt( count( $rooms ), 0 ); + asrt( is_array( $rooms ), TRUE ); + // GetCell should return NULL in case of exception + asrt( NULL, R::getCell( 'SELECT dream FROM fantasy' ) ); + } +} + +/** + * Malfunctioning database adapter to test exceptions. + */ +class TroubleDapter extends DBAdapter +{ + private $sqlState; + public function setSQLState( $sqlState ) + { + $this->sqlState = $sqlState; + } + public function get( $sql, $values = array() ) + { + $exception = new SQL( 'Just a trouble maker' ); + $exception->setSQLState( $this->sqlState ); + $exception->setDriverDetails( array(0,1,0) ); + throw $exception; + } + public function getRow( $sql, $aValues = array() ) + { + $this->get( $sql, $aValues ); + } + public function exec( $sql, $aValues = array(), $noEvent = FALSE ) + { + $this->get( $sql, $aValues ); + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dispense.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dispense.php new file mode 100644 index 0000000..1a43a8e --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dispense.php @@ -0,0 +1,182 @@ +dispense( "page" ); + // Does it have a meta type? + asrt( ( (bool) $page->getMeta( "type" ) ), TRUE ); + // Does it have an ID? + asrt( isset( $page->id ), TRUE ); + // Type should be 'page' + asrt( ( $page->getMeta( "type" ) ), "page" ); + // ID should be 0 because bean does not exist in database yet. + asrt( ( $page->id ), 0 ); + // Try some faulty dispense actions. + foreach ( array( "", ".", "-") as $value ) { + try { + $redbean->dispense( $value ); + fail(); + } catch (RedException $e ) { + pass(); + } + } + $bean = $redbean->dispense( "testbean" ); + $bean["property"] = 123; + $bean["abc"] = "def"; + asrt( $bean["property"], 123 ); + asrt( $bean["abc"], "def" ); + asrt( $bean->abc, "def" ); + asrt( isset( $bean["abd"] ), FALSE ); + asrt( isset( $bean["abc"] ), TRUE ); + } + + /** + * Tests the facade-only dispenseAll method. + * + * @return void + */ + public function testDispenseAll() + { + list( $book, $page ) = Facade::dispenseAll( 'book,page' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( ( $page instanceof OODBBean ), TRUE ); + asrt( $book->getMeta( 'type' ), 'book'); + asrt( $page->getMeta( 'type' ), 'page'); + list( $book, $page, $texts, $mark ) = R::dispenseAll( 'book,page,text*2,mark' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( ( $page instanceof OODBBean ), TRUE ); + asrt( is_array( $texts ), TRUE ); + asrt( ( $mark instanceof OODBBean ), TRUE ); + asrt( $book->getMeta( 'type'), 'book' ); + asrt( $page->getMeta( 'type'), 'page' ); + asrt( $mark->getMeta( 'type'), 'mark' ); + asrt( $texts[0]->getMeta( 'type'), 'text' ); + asrt( $texts[1]->getMeta( 'type'), 'text' ); + list( $eggs, $milk, $butter ) = R::dispenseAll( 'eggs*3,milk*1,butter*9' ); + asrt( count( $eggs ), 3 ); + asrt( ( $milk instanceof OODBBean ), TRUE ); + asrt( count( $butter ), 9 ); + list( $eggs, $milk, $butter ) = R::dispenseAll( 'eggs*3,milk*1,butter*9', TRUE ); + asrt( count( $eggs ), 3 ); + asrt( count( $milk ), 1 ); + asrt( count( $eggs ), 3 ); + list( $beer ) = R::dispenseAll( 'beer*0', TRUE ); + asrt( is_array( $beer ), TRUE ); + asrt( count( $beer ), 0 ); + list( $beer ) = R::dispenseAll( 'beer*0', FALSE ); + asrt( is_array( $beer ), FALSE ); + asrt( is_null( $beer ), TRUE ); + } + + /** + * Tests different return values of dispense(). + * + * @return void + */ + public function testDispenseArray() + { + $oodb = R::getRedBean(); + $array = $oodb->dispense( 'book', 0, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = $oodb->dispense( 'book', 1, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = $oodb->dispense( 'book', 2, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = R::dispense( 'book', 0, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = R::dispense( 'book', 1, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = R::dispense( 'book', 2, TRUE ); + asrt( is_array( $array ), TRUE ); + $array = $oodb->dispense( 'book', 0, FALSE ); + asrt( is_array( $array ), FALSE ); + asrt( is_null( $array ), TRUE ); + $array = $oodb->dispense( 'book', 1, FALSE ); + asrt( is_array( $array ), FALSE ); + asrt( ( $array instanceof OODBBean ), TRUE ); + $array = $oodb->dispense( 'book', 2, FALSE ); + asrt( is_array( $array ), TRUE ); + $array = R::dispense( 'book', 0, FALSE ); + asrt( is_array( $array ), FALSE ); + $array = R::dispense( 'book', 1, FALSE ); + asrt( is_array( $array ), FALSE ); + $array = R::dispense( 'book', 2, FALSE ); + asrt( is_array( $array ), TRUE ); + $array = $oodb->dispense( 'book', 0 ); + asrt( is_array( $array ), FALSE ); + asrt( is_null( $array ), TRUE ); + $array = $oodb->dispense( 'book', 1 ); + asrt( is_array( $array ), FALSE ); + asrt( ( $array instanceof OODBBean ), TRUE ); + $array = $oodb->dispense( 'book', 2 ); + asrt( is_array( $array ), TRUE ); + $array = R::dispense( 'book', 0 ); + asrt( is_array( $array ), FALSE ); + $array = R::dispense( 'book', 1 ); + asrt( is_array( $array ), FALSE ); + $array = R::dispense( 'book', 2 ); + asrt( is_array( $array ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dup.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dup.php new file mode 100644 index 0000000..b6c78eb --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Dup.php @@ -0,0 +1,647 @@ +name = R::dispense('book'); + $export = $bean->export( FALSE, FALSE, FALSE, array( 'book' ) ); + asrt( isset( $export['name'] ), TRUE ); + $export = $bean->export( FALSE, FALSE, FALSE, array( 'book2' ) ); + asrt( isset( $export['name'] ), FALSE ); + } + + /** + * Tests whether the original ID is stored + * in meta data (quite handy for ID mappings). + * + * @return void + */ + public function testKeepOldID() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->xownPageList[] = R::dispense( 'page' ); + R::store( $book ); + $bookID = $book->id; + $page = reset( $book->xownPageList ); + $pageID = $page->id; + $book = $book->fresh(); + $copy = R::duplicate( $book ); + asrt( $copy->getMeta( 'sys.dup-from-id' ), $bookID ); + $copyPage = reset( $copy->xownPageList ); + asrt( $copyPage->getMeta( 'sys.dup-from-id' ), $pageID ); + } + + /** + * Test export camelCase. + * + * @return void + */ + public function testExportCamelCase() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->isCheap = TRUE; + $book->hasISBNCode = FALSE; + $page = R::dispense('page'); + $page->isWrittenWell = TRUE; + $page->containsInterestingText = TRUE; + $book->ownPageList[] = $page; + R::store( $book ); + $book = $book->fresh(); + $export = R::exportAll( $book ); + asrt( isset( $export[0]['id'] ), TRUE ); + asrt( isset( $export[0]['is_cheap'] ), TRUE ); + asrt( isset( $export[0]['has_isbn_code'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['id'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['is_written_well'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['contains_interesting_text'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['book_id'] ), TRUE ); + R::useExportCase( 'camel' ); + $export = R::exportAll( $book ); + asrt( isset( $export[0]['id'] ), TRUE ); + asrt( isset( $export[0]['isCheap'] ), TRUE ); + asrt( isset( $export[0]['hasIsbnCode'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['id'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['isWrittenWell'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['containsInterestingText'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['bookId'] ), TRUE ); + R::useExportCase( 'dolphin' ); + $export = R::exportAll( $book ); + asrt( isset( $export[0]['id'] ), TRUE ); + asrt( isset( $export[0]['isCheap'] ), TRUE ); + asrt( isset( $export[0]['hasIsbnCode'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['id'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['isWrittenWell'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['containsInterestingText'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['bookID'] ), TRUE ); + R::useExportCase( 'default' ); + $export = R::exportAll( $book ); + asrt( isset( $export[0]['id'] ), TRUE ); + asrt( isset( $export[0]['is_cheap'] ), TRUE ); + asrt( isset( $export[0]['has_isbn_code'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['id'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['is_written_well'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['contains_interesting_text'] ), TRUE ); + asrt( isset( $export[0]['ownPage']['0']['book_id'] ), TRUE ); + try { + R::useExportCase( 'invalid' ); + fail(); + } catch ( RedException $exception ) { + pass(); + } + } + + /** + * Test whether we can duplicate part of a tree + * without infinite loops. + * + * @return void + */ + public function testDupPortionOfATree() + { + R::nuke(); + $article = R::dispense( 'article' ); + $article->name = 'article 1'; + list( $article2, $article3 ) = R::dispense( 'article', 2 ); + $article2->name = 'article 2'; + $article3->name = 'article 3'; + list( $article4, $article5 ) = R::dispense( 'article' , 2); + $article4->name = 'article 4'; + $article5->name = 'article 5'; + list( $article6, $article7 ) = R::dispense( 'article' , 2); + $article6->name = 'article 6'; + $article7->name = 'article 7'; + $article3->xownArticleList[] = $article7; + $article4->xownArticleList[] = $article6; + $article2->xownArticleList = array( $article5, $article4 ); + $article->xownArticleList = array( $article2, $article3 ); + R::store( $article ); + asrt( R::count( 'article' ), 7 ); + $article2 = $article2->fresh(); + $dupArticle2 = R::duplicate( $article2 ); + $dupArticle2->name = 'article 2b'; + $dupBeans = $dupArticle2->xownArticleList; + foreach( $dupBeans as $dupBean ) { + $list[] = $dupBean->name; + } + sort( $list ); + $listStr = implode( ',', $list ); + asrt( $listStr, 'article 4,article 5' ); + foreach( $dupBeans as $dupBean ) { + if ( $dupBean->name === 'article 4' ) { + $dup4 = $dupBean; + } + } + asrt( isset( $dup4 ), TRUE ); + $dupBeans = $dup4->xownArticleList; + foreach( $dupBeans as $dupBean ) { + asrt( $dupBean->name, 'article 6' ); + } + //so we have extracted part of the tree, can we store it? + $id = R::store( $dupArticle2 ); + asrt( ( $id > 0 ), TRUE ); + asrt( R::count( 'article' ), 11 ); + $originalArticle = $article->fresh(); + asrt( $originalArticle->name, 'article 1' ); + $subArticles = $originalArticle->xownArticleList; + $list = array(); + foreach( $subArticles as $subArticle ) { + $list[] = $subArticle->name; + } + sort( $list ); + $listStr = implode( ',', $list ); + asrt( $listStr, 'article 2,article 2b,article 3' ); + foreach( $subArticles as $subArticle ) { + if ( $subArticle->name === 'article 2' ) { + $sub2 = $subArticle; + } + if ( $subArticle->name === 'article 3' ) { + $sub3 = $subArticle; + } + } + $subArticles = $sub2->xownArticleList; + $list = array(); + foreach( $subArticles as $subArticle ) { + $list[] = $subArticle->name; + } + sort( $list ); + $listStr = implode( ',', $list ); + asrt( $listStr, 'article 4,article 5' ); + $subArticles = $sub3->xownArticleList; + $list = array(); + foreach( $subArticles as $subArticle ) { + $list[] = $subArticle->name; + } + sort( $list ); + $listStr = implode( ',', $list ); + asrt( $listStr, 'article 7' ); + $subArticles = $sub2->xownArticleList; + foreach( $subArticles as $subArticle ) { + if ( $subArticle->name === 'article 4' ) { + $sub4 = $subArticle; + } + if ( $subArticle->name === 'article 5' ) { + $sub5 = $subArticle; + } + } + asrt( count( $sub4->xownArticleList ), 1 ); + $subBeans = $sub4->xownArticleList; + $subBean = reset( $subBeans ); + asrt( $subBean->name, 'article 6'); + asrt( count( $sub5->xownArticleList ), 0 ); + $dupArticle2 = $dupArticle2->fresh(); + $subArticles = $dupArticle2->xownArticleList; + $list = array(); + foreach( $subArticles as $subArticle ) { + $list[] = $subArticle->name; + } + sort( $list ); + $listStr = implode( ',', $list ); + asrt( $listStr, 'article 4,article 5' ); + foreach( $subArticles as $subArticle ) { + if ( $subArticle->name === 'article 4' ) { + $sub4 = $subArticle; + } + if ( $subArticle->name === 'article 5' ) { + $sub5 = $subArticle; + } + } + asrt( count( $sub4->xownArticleList ), 1 ); + $subBeans = $sub4->xownArticleList; + $subBean = reset( $subBeans ); + asrt( $subBean->name, 'article 6'); + asrt( count( $sub5->xownArticleList ), 0 ); + } + + /** + * Test exportAll and caching. + * + * @return void + */ + public function testExportAllAndCache() + { + testpack( 'exportAll() and Cache' ); + $can = R::dispense( 'can' )->setAttr( 'size', 3 ); + $can->ownCoffee[] = R::dispense( 'coffee' )->setAttr( 'color', 'black' ); + $can->sharedTag[] = R::dispense( 'tag' )->setAttr( 'name', 'cool' ); + $id = R::store( $can ); + R::debug( TRUE ); + ob_start(); + $can = R::load( 'can', $id ); + $cache = $this->getCache(); + $data = R::exportAll( array( $can ), TRUE ); + $queries = ob_get_contents(); + R::debug( FALSE ); + ob_end_clean(); + $len1 = strlen( $queries ); + $can = R::dispense( 'can' )->setAttr( 'size', 3 ); + $can->ownCoffee[] = R::dispense( 'coffee' )->setAttr( 'color', 'black' ); + $can->sharedTag[] = R::dispense( 'tag' )->setAttr( 'name', 'cool' ); + $id = R::store( $can ); + R::debug( TRUE ); + ob_start(); + $can = R::load( 'can', $id ); + $cache = $this->getCache(); + $data = R::exportAll( array( $can ), TRUE ); + $queries = ob_get_contents(); + R::debug( FALSE ); + ob_end_clean(); + $len2 = strlen( $queries ); + asrt( ( $len1 ), ( $len2 ) ); + $can = R::dispense( 'can' )->setAttr( 'size', 3 ); + $can->ownCoffee[] = R::dispense( 'coffee' )->setAttr( 'color', 'black' ); + $can->sharedTag[] = R::dispense( 'tag' )->setAttr( 'name', 'cool' ); + $id = R::store( $can ); + R::debug( TRUE ); + ob_start(); + $can = R::load( 'can', $id ); + $cache = $this->getCache(); + R::getDuplicationManager()->setTables( $cache ); + $data = R::exportAll( array( $can ), TRUE ); + $queries = ob_get_contents(); + R::debug( FALSE ); + ob_end_clean(); + $len3 = strlen( $queries ); + asrt( ( ( $len3 ) < ( $len2 ) ), TRUE ); + asrt( count( $data ), 1 ); + asrt( $data[0]['ownCoffee'][0]['color'], 'black' ); + R::getDuplicationManager()->setCacheTables( FALSE ); + } + + /** + * Test duplication and caching. + * + * @return void + */ + public function DupAndCache() + { + testpack( 'Dup() and Cache' ); + $can = R::dispense( 'can' )->setAttr( 'size', 3 ); + $can->ownCoffee[] = R::dispense( 'coffee' )->setAttr( 'color', 'black' ); + $can->sharedTag[] = R::dispense( 'tag' )->setAttr( 'name', 'cool' ); + $can = R::load( 'can', R::store( $can ) ); + $d = new DuplicationManager( R::getToolBox() ); + $d->setCacheTables( TRUE ); + ob_start(); + R::debug( 1 ); + $x = $d->dup( $can ); + $queries = ob_get_contents(); + R::debug( 0 ); + ob_end_clean(); + $len1 = strlen( $queries ); + asrt( ( $len1 > 40 ), TRUE ); + asrt( isset( $x->ownCoffee ), TRUE ); + asrt( count( $x->ownCoffee ), 1 ); + asrt( isset( $x->sharedTag ), TRUE ); + asrt( count( $x->sharedTag ), 1 ); + $cache = $d->getSchema(); + R::nuke(); + $can = R::dispense( 'can' )->setAttr( 'size', 3 ); + $can->ownCoffee[] = R::dispense( 'coffee' )->setAttr( 'color', 'black' ); + $can->sharedTag[] = R::dispense( 'tag' )->setAttr( 'name', 'cool' ); + $can = R::load( 'can', R::store( $can ) ); + $d = new DuplicationManager( R::getToolBox() ); + + /** + * $cache = '{"book": { + * "id": "INTEGER", + * "title": "TEXT" + * }, "bean": { + * "id": "INTEGER", + * "prop": "INTEGER" + * }, "pessoa": { + * "id": "INTEGER", + * "nome": "TEXT", + * "nome_meio": "TEXT", + * "sobrenome": "TEXT", + * "nascimento": "NUMERIC", + * "reg_owner": "TEXT" + * }, "documento": { + * "id": "INTEGER", + * "nome_documento": "TEXT", + * "numero_documento": "TEXT", + * "reg_owner": "TEXT", + * "ownPessoa_id": "INTEGER" + * }, "can": { + * "id": "INTEGER", + * "size": "INTEGER" + * }, "coffee": { + * "id": "INTEGER", + * "color": "TEXT", + * "can_id": "INTEGER" + * }, "tag": { + * "id": "INTEGER", + * "name": "TEXT" + * }, "can_tag": { + * "id": "INTEGER", + * "tag_id": "INTEGER", + * "can_id": "INTEGER" + * }}' + */ + + $d->setTables( $cache ); + ob_start(); + R::debug( 1 ); + $x = $d->dup( $can ); + $queries = ob_get_contents(); + ob_end_clean(); + R::debug( 0 ); + $len2 = strlen( $queries ); + asrt( isset( $x->ownCoffee ), TRUE ); + asrt( count( $x->ownCoffee ), 1 ); + asrt( isset( $x->sharedTag ), TRUE ); + asrt( count( $x->sharedTag ), 1 ); + asrt( json_encode( $cache ), json_encode( $d->getSchema() ) ); + asrt( ( $len1 > $len2 ), TRUE ); + } + + /** + * Test duplication and tainting. + * + * @return void + */ + public function testDupAndExportNonTainting() + { + testpack( 'Dup() and Export() should not taint beans' ); + $p = R::dispense( 'page' ); + $b = R::dispense( 'book' ); + $b->ownPage[] = $p; + $b->title = 'a'; + $id = R::store( $b ); + $b = R::load( 'book', $id ); + asrt( ( !$b->getMeta( 'tainted' ) ), TRUE ); + R::exportAll( $b ); + asrt( ( !$b->getMeta( 'tainted' ) ), TRUE ); + R::dup( $b ); + asrt( ( !$b->getMeta( 'tainted' ) ), TRUE ); + testpack( 'Test issue with ownItems and stealing Ids.' ); + R::nuke(); + $bill = R::dispense( 'bill' ); + $item = R::dispense( 'item' ); + $element = R::dispense( 'element' ); + $bill->ownItem[] = $item; + $bill->sharedElement[] = $element; + R::store( $bill ); + $bill = R::load( 'bill', 1 ); + $bill->ownItem; + $bill->sharedElement; + $copy = R::dup( $bill ); + R::store( $copy ); + $rows = ( R::getAll( 'select * from bill_element' ) ); + asrt( count( $rows ), 2 ); + $rows = ( R::getAll( 'select * from item' ) ); + foreach ( $rows as $row ) { + asrt( ( $row['bill_id'] > 0 ), TRUE ); + } + R::nuke(); + $this->runOnce(); + R::freeze( TRUE ); + $this->runOnce( FALSE ); + R::freeze( FALSE ); + } + + /** + * Test exporting with filters. + * + * @return void + */ + public function ExportWithFilters() + { + testpack( 'Export with filters' ); + $book = R::dispense( 'book' ); + $pages = R::dispense( 'page', 2 ); + $texts = R::dispense( 'text', 2 ); + $images = R::dispense( 'image', 2 ); + $author = R::dispense( 'author' ); + $pub = R::dispense( 'publisher' ); + $bookmarks = R::dispense( 'bookmark', 2 ); + $pages[0]->ownText = array( $texts[0] ); + $pages[0]->ownImage = array( $images[0] ); + $pages[1]->ownText = array( $texts[1] ); + $pages[1]->ownImage = array( $images[1] ); + $pages[0]->sharedBookmark[] = $bookmarks[0]; + $pages[1]->sharedBookmark[] = $bookmarks[1]; + $bookmarks[0]->ownNote[] = R::dispense( 'note' )->setAttr( 'text', 'a note' ); + $bookmarks[1]->ownNote[] = R::dispense( 'note' )->setAttr( 'text', 'a note' ); + $book->ownPage = $pages; + $book->author = $author; + $author->publisher = $pub; + $bookID = R::store( $book ); + R::getDuplicationManager()->setTables( R::getWriter()->getTables() ); + $objects = ( R::exportAll( array( $book ), TRUE, array() ) ); + asrt( isset( $objects[0]['ownPage'] ), TRUE ); + asrt( count( $objects[0]['ownPage'] ), 2 ); + asrt( isset( $objects[0]['author'] ), TRUE ); + asrt( isset( $objects[0]['ownPage'][0]['ownText'] ), TRUE ); + asrt( count( $objects[0]['ownPage'][0]['ownText'] ), 1 ); + asrt( isset( $objects[0]['ownPage'][0]['ownImage'] ), TRUE ); + asrt( count( $objects[0]['ownPage'][0]['ownImage'] ), 1 ); + $objects = ( R::exportAll( array( $book ), TRUE, array( 'page', 'author', 'text', 'image' ) ) ); + asrt( isset( $objects[0]['ownPage'] ), TRUE ); + asrt( count( $objects[0]['ownPage'] ), 2 ); + asrt( isset( $objects[0]['author'] ), TRUE ); + asrt( isset( $objects[0]['ownPage'][0]['ownText'] ), TRUE ); + asrt( count( $objects[0]['ownPage'][0]['ownText'] ), 1 ); + asrt( isset( $objects[0]['ownPage'][0]['ownImage'] ), TRUE ); + asrt( count( $objects[0]['ownPage'][0]['ownImage'] ), 1 ); + $objects = ( R::exportAll( array( $book ), TRUE, 'author' ) ); + asrt( isset( $objects[0]['ownPage'] ), FALSE ); + asrt( isset( $objects[0]['ownPage'][0]['ownText'] ), FALSE ); + $objects = ( R::exportAll( array( $book ), TRUE, array( 'page' ) ) ); + asrt( isset( $objects[0]['author'] ), FALSE ); + asrt( isset( $objects[0]['ownPage'][0]['ownText'] ), FALSE ); + $objects = ( R::exportAll( array( $book ), TRUE, array( 'page', 'text' ) ) ); + asrt( isset( $objects[0]['author'] ), FALSE ); + asrt( isset( $objects[0]['ownPage'] ), TRUE ); + asrt( isset( $objects[0]['ownPage'][0]['ownText'] ), TRUE ); + asrt( count( $objects[0]['ownPage'][0]['ownText'] ), 1 ); + asrt( isset( $objects[0]['ownPage'][0]['ownImage'] ), FALSE ); + $objects = ( R::exportAll( array( $book ), TRUE, array( 'none' ) ) ); + asrt( isset( $objects[0]['author'] ), FALSE ); + asrt( isset( $objects[0]['ownPage'] ), FALSE ); + $texts = R::find( 'text' ); + R::getDuplicationManager()->setCacheTables( FALSE ); + testpack( 'Keyless export' ); + $book = R::load( 'book', $bookID ); + $book->ownPage; + $export = $book->export(); + asrt( isset( $export['ownPage'][0] ), TRUE ); + } + + /** + * Helper function getCache(). + * + * @return array + */ + private function getCache() + { + return array( + 'coffee' => array( + 'color' => 'color', + 'id' => 'id', + 'can_id' => 'can_id' + ), + 'can' => array( + 'size' => 'size', + 'id' => 'id' + ), + 'can_tag' => array( + 'id' => 'id', + 'can_id' => 'can_id', + 'tag_id' => 'tag_id' + ), + 'tag' => array( + 'id' => 'id', + 'name' => 'name' ) + ); + } + + /** + * Compares object with export + * + * @param type $object + * @param type $array + * + * @return void + */ + private function compare( $object, $array ) + { + foreach ( $object as $property => $value ) { + if ( is_array( $value ) ) { + foreach ( $value as $index => $nestedObject ) { + if ( $nestedObject->id ) { + $foundMatch = FALSE; + //order might be different + foreach ( $array[$property] as $k => $a ) { + if ( $a['id'] == $nestedObject->id ) { + $foundMatch = TRUE; + $index = $k; + } + } + if ( !$foundMatch ) throw new\Exception( 'failed to find match for object ' . $nestedObject->id ); + } + $this->compare( $nestedObject, $array[$property][$index] ); + } + } elseif ( !is_object( $value ) ) { + asrt( strval( $array[$property] ), strval( $value ) ); + } + } + } + + /** + * Run tests + */ + private function runOnce( $n = TRUE ) + { + $books = R::dispense( 'book', 10 ); + $pages = R::dispense( 'page', 10 ); + $readers = R::dispense( 'reader', 10 ); + $texts = R::dispense( 'text', 10 ); + $i = 0; + foreach ( $books as $book ) $book->name = 'book-' . ( $i++ ); + $i = 0; + foreach ( $pages as $page ) $page->name = 'page-' . ( $i++ ); + $i = 0; + foreach ( $readers as $reader ) $reader->name = 'reader-' . ( $i++ ); + $i = 0; + foreach ( $texts as $text ) $text->content = 'lorem ipsum -' . ( $i++ ); + + foreach ( $texts as $text ) { + $pages[array_rand( $pages )]->ownText[] = $text; + } + foreach ( $pages as $page ) { + $books[array_rand( $books )]->ownPage[] = $page; + } + foreach ( $readers as $reader ) { + $books[array_rand( $books )]->sharedReader[] = $reader; + } + $i = $noOfReaders = $noOfPages = $noOfTexts = 0; + foreach ( $books as $key => $book ) { + $i++; + $noOfPages += count( $book->ownPage ); + $noOfReaders += count( $book->sharedReader ); + foreach ( $book->ownPage as $page ) $noOfTexts += count( $page->ownText ); + $arr = R::exportAll( $book ); + echo "\nIntermediate info: " . json_encode( $arr ) . ": Totals = $i,$noOfPages,$noOfReaders,$noOfTexts "; + $this->compare( $book, $arr[0] ); + $copiedBook = R::dup( $book ); + $copiedBookArray = R::exportAll( $copiedBook ); + $this->compare( $book, $copiedBookArray[0] ); + $copiedBookArrayII = $copiedBook->export(); + $this->compare( $book, $copiedBookArrayII ); + $copyFromCopy = R::dup( $copiedBook ); + $copyFromCopyArray = R::exportAll( $copyFromCopy ); + $this->compare( $book, $copyFromCopyArray[0] ); + $copyFromCopyArrayII = $copyFromCopy->export(); + $this->compare( $book, $copyFromCopyArrayII ); + $id = R::store( $book ); + $copiedBook = R::dup( $book ); + R::store( $book ); //should not be damaged + $copiedBookArray = R::exportAll( $copiedBook ); + $originalBookArray = R::exportAll( $book ); + $this->compare( $copiedBook, $copiedBookArray[0] ); + $this->compare( $book, $originalBookArray[0] ); + $book = R::load( 'book', $id ); + $this->compare( $book, $originalBookArray[0] ); + $copiedBook = R::dup( $book ); + $this->compare( $copiedBook, $copiedBook->export() ); + R::store( $copiedBook ); + $this->compare( $copiedBook, $copiedBook->export() ); + $copyFromCopy = R::dup( $copiedBook ); + $this->compare( $copyFromCopy, $copyFromCopy->export() ); + R::store( $copyFromCopy ); + $newPage = R::dispense( 'page' ); + $newPage->name = 'new'; + $copyFromCopy->ownPage[] = $newPage; + $modifiedCopy = R::dup( $copyFromCopy ); + $exportMod = R::exportAll( $modifiedCopy ); + $this->compare( $modifiedCopy, $exportMod[0] ); + asrt( count( $modifiedCopy->ownPage ), count( $copiedBook->ownPage ) + 1 ); + R::store( $modifiedCopy ); + if ( $n ) { + asrt( (int) R::getCell( 'SELECT count(*) FROM book' ), $i * 4 ); + asrt( (int) R::getCell( 'SELECT count(*) FROM page' ), ( $noOfPages * 4 ) + $i ); + asrt( (int) R::getCell( 'SELECT count(*) FROM text' ), $noOfTexts * 4 ); + asrt( (int) R::getCell( 'SELECT count(*) FROM book_reader' ), $noOfReaders * 4 ); + asrt( (int) R::getCell( 'SELECT count(*) FROM reader' ), $noOfReaders ); + } + } + if ( $n ) { + asrt( $noOfTexts, 10 ); + asrt( $noOfReaders, 10 ); + asrt( $noOfPages, 10 ); + asrt( $i, 10 ); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Exceptions.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Exceptions.php new file mode 100644 index 0000000..a0a0536 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Exceptions.php @@ -0,0 +1,166 @@ +id, 0 ); + R::freeze( TRUE ); + $exception = NULL; + try { + $book = R::load( 'book', 1 ); + } catch( RedException $exception ) {} + asrt( ( $exception instanceof RedException ), TRUE ); + R::freeze( FALSE ); + R::store( $book ); + /* bean does not exist - table exists */ + $book = R::load( 'book', 2 ); + pass(); + asrt( $book->id, 0 ); + R::freeze( TRUE ); + $book = R::load( 'book', 2 ); + pass(); + asrt( $book->id, 0 ); + /* other error */ + if ( !( R::getWriter() instanceof SQLiteT ) ) { + R::freeze( FALSE ); + $exception = NULL; + try { + $book = R::load( 'book', 1, 'invalid sql' ); + } catch( RedException $exception ) {} + //not supported for CUBRID + if ($this->currentlyActiveDriverID !== 'CUBRID') { + asrt( ( $exception instanceof RedException ), TRUE ); + } + } else { + /* error handling in SQLite is suboptimal */ + R::freeze( FALSE ); + $book = R::load( 'book', 1, 'invalid sql' ); + pass(); + asrt( $book->id, 0 ); + } + R::freeze( TRUE ); + $exception = NULL; + try { + $book = R::load( 'book', 1, 'invalid sql' ); + } catch( RedException $exception ) {} + asrt( ( $exception instanceof RedException ), TRUE ); + R::freeze( FALSE ); + R::nuke(); + } + + /** + * Test delete exceptions + * + * - in fluid mode no complaining about missing structures + * + * @return void + */ + public function testDeleteExceptions() + { + R::nuke(); + $book = R::dispense( 'book' ); + R::store( $book ); + R::nuke(); + R::trash( $book ); + R::freeze( TRUE ); + $exception = NULL; + try { + R::trash( $book ); + } catch( RedException $exception ) {} + asrt( ( $exception instanceof RedException ), TRUE ); + R::freeze( FALSE ); + $adapter = R::getDatabaseAdapter(); + R::nuke(); + $book = R::dispense( 'book' ); + R::store( $book ); + $broken = new BrokenWriter( $adapter ); + $redbean = R::getRedBean(); + $oodb = new OODB( $broken, $redbean->isFrozen() ); + R::setRedBean( $oodb ); + $exception = NULL; + try { + R::trash( $book ); + } catch( RedException $exception ) {} + asrt( ( $exception instanceof RedException ), TRUE ); + R::freeze( TRUE ); + $exception = NULL; + try { + R::trash( $book ); + } catch( RedException $exception ) {} + asrt( ( $exception instanceof RedException ), TRUE ); + R::setRedBean( $redbean ); + } + + /** + * Test chaining of exceptions. + * + * @return void + */ + public function testChainingExceptions() + { + R::freeze( TRUE ); + $exception = NULL; + try { + $book = R::load( 'book', 1, 'invalid sql' ); + } catch( RedException $exception ) {} + pass(); + asrt( ( $exception instanceof RedException ), TRUE ); + asrt( ( $exception->getPrevious() instanceof \Exception ), TRUE ); + } +} + +class BrokenWriter extends SQLiteT { + public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + throw new SQLException('oops'); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Facade.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Facade.php new file mode 100644 index 0000000..affab87 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Facade.php @@ -0,0 +1,137 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + asrt( R::getRedBean() instanceof OODB, TRUE ); + asrt( R::getToolBox() instanceof ToolBox, TRUE ); + asrt( R::getDatabaseAdapter() instanceof Adapter, TRUE ); + asrt( R::getWriter() instanceof QueryWriter, TRUE ); + $book = R::dispense( "book" ); + asrt( $book instanceof OODBBean, TRUE ); + $book->title = "a nice book"; + $id = R::store( $book ); + asrt( ( $id > 0 ), TRUE ); + $book = R::load( "book", (int) $id ); + asrt( $book->title, "a nice book" ); + asrt( R::load( 'book', 999 )->title, NULL ); + R::freeze( TRUE ); + try { + R::load( 'bookies', 999 ); + fail(); + } catch (\Exception $e ) { + pass(); + } + R::freeze( FALSE ); + $author = R::dispense( "author" ); + $author->name = "me"; + R::store( $author ); + $book9 = R::dispense( "book" ); + $author9 = R::dispense( "author" ); + $author9->name = "mr Nine"; + $a9 = R::store( $author9 ); + $book9->author_id = $a9; + $bk9 = R::store( $book9 ); + $book9 = R::load( "book", $bk9 ); + $author = R::load( "author", $book9->author_id ); + asrt( $author->name, "mr Nine" ); + R::trash( $author ); + R::trash( $book9 ); + pass(); + $book2 = R::dispense( "book" ); + $book2->title = "second"; + R::store( $book2 ); + $book3 = R::dispense( "book" ); + $book3->title = "third"; + R::store( $book3 ); + asrt( count( R::find( "book" ) ), 3 ); + asrt( count( R::findAll( "book" ) ), 3 ); + asrt( count( R::findAll( "book", " LIMIT 2" ) ), 2 ); + asrt( count( R::find( "book", " id=id " ) ), 3 ); + asrt( count( R::find( "book", " title LIKE ?", array( "third" ) ) ), 1 ); + asrt( count( R::find( "book", " title LIKE ?", array( "%d%" ) ) ), 2 ); + // Find without where clause + asrt( count( R::findAll( 'book', ' order by id' ) ), 3 ); + R::trash( $book3 ); + R::trash( $book2 ); + asrt( count( R::getAll( "SELECT * FROM book " ) ), 1 ); + asrt( count( R::getCol( "SELECT title FROM book " ) ), 1 ); + asrt( (int) R::getCell( "SELECT 123 " ), 123 ); + $book = R::dispense( "book" ); + $book->title = "not so original title"; + $author = R::dispense( "author" ); + $author->name = "Bobby"; + R::store( $book ); + $aid = R::store( $author ); + $author = R::findOne( "author", " name = ? ", array( "Bobby" ) ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Finding.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Finding.php new file mode 100644 index 0000000..52bbc59 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Finding.php @@ -0,0 +1,1336 @@ +name = 'a'; + $gebruiker2->name = 'b'; + $gebruiker3->name = 'c'; + $country1->name = 'Netherlands'; + $country2->name = 'France'; + $country3->name = 'USA'; + $language1->name = 'Dutch'; + $language2->name = 'French'; + $language3->name = 'English'; + $gebruiker1->country = $country1; + $gebruiker1->language = $language1; + $gebruiker2->country = $country2; + $gebruiker2->language = $language2; + $gebruiker3->country = $country3; + $gebruiker3->language = $language3; + R::storeAll(array($gebruiker1,$gebruiker2,$gebruiker3)); + $gebruikers = R::find('gebruiker'); //imagine some difficult query here + //can we attach other beans using findMulti? + $all = R::findMulti( + 'country', + 'SELECT country.* FROM country WHERE id IN ('. R::genSlots( $gebruikers ) . ') ', colfield( $gebruikers, 'country_id' ), + array( + array( + 'a' => 'country', + 'b' => $gebruikers, + 'matcher' => function( $a, $b ) { + return ($b->country_id == $a->id); + }, + 'do' => function( $a, $b ) { + $b->noLoad()->country = $a; + } + ) + ) + ); + asrt(count($all),2); + asrt(isset($all['country']), TRUE); + asrt(isset($all['gebruiker']), TRUE); + asrt(count($all['country']), 3); + asrt(count($all['gebruiker']), 3); + foreach($all['gebruiker'] as $gebruiker) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + } + //Using a static mapper makes it more readable + $gebruikers = R::find('gebruiker'); //imagine some difficult query here + $all = R::findMulti( + 'country', + 'SELECT country.* FROM country WHERE id IN ('. R::genSlots( $gebruikers ) . ') ', colfield( $gebruikers, 'country_id' ), + array( + Finder::onMap('country', $gebruikers) + ) + ); + asrt(count($all),2); + asrt(isset($all['country']), TRUE); + asrt(isset($all['gebruiker']), TRUE); + asrt(count($all['country']), 3); + asrt(count($all['gebruiker']), 3); + foreach($all['gebruiker'] as $gebruiker) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + } + //More generic scenario + $all = R::findMulti( + 'gebruiker,country', + ' + SELECT gebruiker.*, country.* FROM gebruiker + LEFT JOIN country ON gebruiker.country_id = country.id', + array(), + array( + Finder::onMap('country', 'gebruiker') + ) + ); + asrt(count($all),2); + asrt(isset($all['country']), TRUE); + asrt(isset($all['gebruiker']), TRUE); + asrt(count($all['country']), 3); + asrt(count($all['gebruiker']), 3); + foreach($all['gebruiker'] as $gebruiker) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + if ( $gebruiker->name == 'a' ) { + asrt( ( + $gebruiker->name == 'a' && + $gebruiker->country->name == 'Netherlands' ), + TRUE ); + } + elseif ( $gebruiker->name == 'b' ) { + asrt( ( + $gebruiker->name == 'b' && + $gebruiker->country->name == 'France' ), + TRUE ); + } + elseif ( $gebruiker->name == 'c' ) { + asrt( ( + $gebruiker->name == 'c' && + $gebruiker->country->name == 'USA' ), + TRUE ); + } + else { + fail(); + } + pass(); + } + //Even more compact + $gebruikers = R::find('gebruiker'); //imagine some difficult query here + $all = R::findMulti( + 'country', + R::genSlots( $gebruikers, 'SELECT country.* FROM country WHERE id IN ( %s )' ), colfield( $gebruikers, 'country_id' ), + array( + Finder::onMap('country', $gebruikers) + ) + ); + asrt(count($all),2); + asrt(isset($all['country']), TRUE); + asrt(isset($all['gebruiker']), TRUE); + asrt(count($all['country']), 3); + asrt(count($all['gebruiker']), 3); + foreach($all['gebruiker'] as $gebruiker) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + if ( $gebruiker->name == 'a' ) { + asrt( ( + $gebruiker->name == 'a' && + $gebruiker->country->name == 'Netherlands' ), + TRUE ); + } + elseif ( $gebruiker->name == 'b' ) { + asrt( ( + $gebruiker->name == 'b' && + $gebruiker->country->name == 'France' ), + TRUE ); + } + elseif ( $gebruiker->name == 'c' ) { + asrt( ( + $gebruiker->name == 'c' && + $gebruiker->country->name == 'USA' ), + TRUE ); + } + else { + fail(); + } + pass(); + } + //Yet, even more compact + $gebruikers = R::find('gebruiker'); //imagine some difficult query here + $gebruikers = R::loadJoined( $gebruikers, 'country' ); + $gebruikers = R::loadJoined( $gebruikers, 'language' ); + asrt(count($gebruikers), 3); + foreach($gebruikers as $gebruiker) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + if ( $gebruiker->name == 'a' ) { + asrt( ( + $gebruiker->name == 'a' && + $gebruiker->country->name == 'Netherlands' && + $gebruiker->language->name == 'Dutch' ), + TRUE ); + } + elseif ( $gebruiker->name == 'b' ) { + asrt( ( + $gebruiker->name == 'b' && + $gebruiker->country->name == 'France' && + $gebruiker->language->name == 'French' ), + TRUE ); + } + elseif ( $gebruiker->name == 'c' ) { + asrt( ( + $gebruiker->name == 'c' && + $gebruiker->country->name == 'USA' && + $gebruiker->language->name == 'English' ), + TRUE ); + } + else { + fail(); + } + pass(); + } + } + + /** + * Test whether we can also preload own-lists. + */ + public function testPreloadingOwnListWithMulti() + { + R::nuke(); + R::freeze(TRUE); + for($i=0;$i<10;$i++) { + R::freeze((boolean)($i)); + $book = R::dispense('book'); + $book->title = "book{$i}"; + list($pages) = R::dispenseAll('page*2'); + $pages[0]->title = "1/{$i}"; + $pages[1]->title = "2/{$i}"; + $book->noLoad()->ownPage = $pages; + R::store($book); + } + $books = R::find('book'); + $all = R::findMulti( 'page', + 'SELECT page.* FROM page where book_id IN ('. R::genSlots($books).')', + colfield($books,'id'), + array(array( + 'a' => 'page', + 'b' => $books, + 'matcher' => TRUE, + 'do' => function($page, $books){ + $books[$page->book_id]->noLoad()->ownPage[] = $page; + }, + )) + ); + asrt(count($all),2); + asrt(count($all['book']),10); + asrt(count($all['page']),20); + $i=0; + foreach($all['book'] as $book) { + asrt(count($book->noLoad()->ownPageList),2); + asrt($book->title,"book{$i}"); + $pages = $book->noLoad()->ownPageList; + usort($pages, function($a,$b){ return $a->title > $b->title; }); + $first = reset($pages); + asrt( $first->title, "1/$i" ); + $last = end($pages); + asrt( $last->title, "2/$i" ); + $i++; + } + R::freeze(FALSE); + } + + /** + * Test somewhat more complex findMulti plus onmap() + * + * @return void + */ + public function testComplexFindMultis() + { + R::nuke(); + $gebruiker1 = R::dispense('gebruiker'); + $gebruiker2 = R::dispense('gebruiker'); + $gebruiker3 = R::dispense('gebruiker'); + $country1 = R::dispense('country'); + $country2 = R::dispense('country'); + $country3 = R::dispense('country'); + $language1 = R::dispense('language'); + $language2 = R::dispense('language'); + $language3 = R::dispense('language'); + $gebruiker1->name = 'a'; + $gebruiker2->name = 'b'; + $gebruiker3->name = 'c'; + $country1->name = 'Netherlands'; + $country2->name = 'France'; + $country3->name = 'USA'; + $language1->name = 'Dutch'; + $language2->name = 'French'; + $language3->name = 'English'; + $gebruiker1->country = $country1; + $gebruiker1->language = $language1; + $gebruiker2->country = $country2; + $gebruiker2->language = $language2; + $gebruiker3->country = $country3; + $gebruiker3->language = $language3; + R::storeAll(array($gebruiker1,$gebruiker2,$gebruiker3)); + $all = R::findMulti( + 'gebruiker,country,language', ' + SELECT gebruiker.*, country.*, language.* + FROM gebruiker + INNER JOIN country ON country.id = gebruiker.country_id + INNER JOIN language ON language.id = gebruiker.language_id + ', + array(), + array( + Finder::onMap('country','gebruiker'), + Finder::onMap('language', 'gebruiker') + )); + asrt(count($all), 3); + asrt(isset($all['gebruiker']), TRUE); + asrt(isset($all['country']), TRUE); + asrt(isset($all['language']), TRUE); + asrt(count($all['gebruiker']), 3); + asrt(count($all['country']), 3); + asrt(count($all['language']), 3); + foreach( $all['gebruiker'] as $gebruiker ) { + asrt( $gebruiker->noLoad()->country instanceof OODBBean, TRUE ); + asrt( $gebruiker->noLoad()->language instanceof OODBBean, TRUE ); + } + } + + /** + * Improve the sql check in glueLimitOne #776. + * + * @return void + */ + public function testIssue776() + { + $writer = R::getWriter(); + asrt( 'SELECT * FROM people LIMIT 2', $writer->glueLimitOne('SELECT * FROM people LIMIT 2') ); + asrt( 'SELECT * FROM limits LIMIT 2', $writer->glueLimitOne('SELECT * FROM limits LIMIT 2') ); + asrt( 'SELECT * FROM people LIMIT 1 ', $writer->glueLimitOne('SELECT * FROM people') ); + asrt( 'SELECT * FROM limits LIMIT 1 ', $writer->glueLimitOne('SELECT * FROM limits') ); + } + + /** + * Helper for testing findLike. + * + * @param array $flowers beans + * @param boolean $noSort sorting? + * + * @return string + */ + private function getColors( $flowers, $noSort = FALSE ) + { + $colors = array(); + foreach( $flowers as $flower ) $colors[] = $flower->color; + if ( !$noSort) sort( $colors ); + return implode( ',', $colors ); + } + + /** + * Inserts data for findMulti-tests. + * + * @return void + */ + private function insertBookData() + { + list( $books, $pages, $texts, $categories ) = R::dispenseAll( 'book*5,page*25,text*60,category*3' ); + $texts[0]->content = 'C is a beautiful language.'; + $texts[1]->content = 'But also a bit dangerous.'; + $texts[2]->content = 'You need to know what you are doing.'; + $texts[3]->content = 'Javascript is very flexible.'; + $texts[4]->content = 'It can be anything you want...'; + $texts[5]->content = 'But it can lead to chaos.'; + $texts[6]->content = 'CSS was meant for documents'; + $texts[7]->content = 'Now we use it for applications...'; + $texts[8]->content = 'PHP is an easy language to learn,'; + $texts[9]->content = 'Maybe a bit too easy...'; + $texts[10]->content = 'SQL is much more powerful than you think.'; + $pages[0]->ownTextList = array( $texts[0], $texts[1] ); + $pages[1]->ownTextList = array( $texts[2] ); + $pages[2]->ownTextList = array( $texts[3] ); + $pages[3]->ownTextList = array( $texts[4] ); + $pages[4]->ownTextList = array( $texts[5] ); + $pages[5]->ownTextList = array( $texts[6], $texts[7] ); + $pages[6]->ownTextList = array( $texts[8] ); + $pages[7]->ownTextList = array( $texts[9] ); + $pages[8]->ownTextList = array( $texts[10] ); + $books[0]->ownPageList = array( $pages[0], $pages[1] ); + $books[1]->ownPageList = array( $pages[2], $pages[3], $pages[4] ); + $books[2]->ownPageList = array( $pages[5] ); + $books[3]->ownPageList = array( $pages[6], $pages[7] ); + $books[4]->ownPageList = array( $pages[8] ); + $books[0]->title = 'Diehard C'; + $books[1]->title = 'Adventures in JavaScript'; + $books[2]->title = 'CSS ala Picasso'; + $books[3]->title = 'PHP Tips and Tricks'; + $books[4]->title = 'Secrets of SQL'; + $categories[0]->name = 'Programming'; + $categories[1]->name = 'Design'; + $categories[2]->name = 'Web Development'; + $books[0]->sharedCategoryList = array( $categories[0] ); + $books[1]->sharedCategoryList = array( $categories[0], $categories[2] ); + $books[2]->sharedCategoryList = array( $categories[0], $categories[2], $categories[1] ); + $books[3]->sharedCategoryList = array( $categories[0], $categories[2] ); + $books[4]->sharedCategoryList = array( $categories[0], $categories[2] ); + R::storeAll( $books ); + } + + /** + * Test NM-Map. + * + * @return void + */ + public function testNMMap() + { + R::nuke(); + $book1 = R::dispense( 'book' ); + $book1->title = 'book 1'; + R::tag( $book1, array('tag 1','tag 2') ); + $book2 = R::dispense( 'book' ); + $book2->title = 'book 2'; + R::tag( $book2, array('tag 2', 'tag 3') ); + $collection = R::findMulti( 'book,book_tag,tag', + 'SELECT book.*, book_tag.*, tag.* FROM book + LEFT JOIN book_tag ON book_tag.book_id = book.id + LEFT JOIN tag ON book_tag.tag_id = tag.id + ORDER BY tag.title ASC + ', array(), array( + Finder::nmMap( 'book', 'tag' ), + )); + asrt( count( $collection ), 3 ); + asrt( isset( $collection['book'] ), TRUE ); + asrt( isset( $collection['book_tag'] ), TRUE ); + asrt( isset( $collection['tag'] ), TRUE ); + $books = $collection['book']; + foreach( $books as $book ) { + asrt( count( $book->noLoad()->sharedTagList ), 2 ); + $tags = array(); + if ( $book->title == 'book 1' ) { + foreach( $book->sharedTagList as $tag ) { + $tags[] = $tag->title; + } + asrt( implode( ',', $tags ), 'tag 1,tag 2' ); + } + $tags = array(); + if ( $book->title == 'book 2' ) { + foreach( $book->sharedTagList as $tag ) { + $tags[] = $tag->title; + } + asrt( implode( ',', $tags ), 'tag 2,tag 3' ); + } + } + } + + /** + * Test explicit param binding. + * + * @return void + */ + public function testExplParaBin() + { + R::nuke(); + $bean = R::dispense('bean'); + $bean->property = 1; + $bean->property2 = 2; + $bean->property3 = '3'; + R::store($bean); + $value = 1; + $value2 = 2; + $value3 = '3'; + $found = R::findOne( 'bean', ' property = ? AND property2 = ? AND property3 = ? ', + array($value, array( $value2, \PDO::PARAM_INT ),array( $value3, \PDO::PARAM_STR ))); + asrt( $bean->id, $found->id ); + } + + /** + * FindMulti should not throw errors in case of + * a record-type mismatch. + * + * @return void + */ + public function testFindMultiErrorHandling() + { + $result = R::findMulti('a,b', array()); + asrt( is_array( $result ), TRUE ); + asrt( count( $result ), 2 ); + asrt( isset( $result['a'] ), TRUE ); + asrt( isset( $result['b'] ), TRUE ); + asrt( is_array( $result['a'] ), TRUE ); + asrt( is_array( $result['b'] ), TRUE ); + asrt( count( $result['a'] ), 0 ); + asrt( count( $result['b'] ), 0 ); + pass(); + $result = R::findMulti( 'book', array( + array( 'book__title' => 'The missing ID.' ) + ) ); + asrt( is_array( $result ), TRUE ); + asrt( count( $result ), 1 ); + asrt( isset( $result['book'] ), TRUE ); + asrt( is_array( $result['book'] ), TRUE ); + asrt( count( $result['book'] ), 0 ); + pass(); + } + + /** + * Like testFindMultiExtFunc but uses findMulti with + * $sql = NULL. + * + * @return void + */ + public function testFindMultiWithSQLNULL() + { + R::nuke(); + $shop = R::dispense( 'shop' ); + $shop2 = R::dispense( 'shop' ); + $products = R::dispense( 'product', 3 ); + $price = R::dispense( 'price' ); + $price->tag = 5; + $products[0]->name = 'vase'; + $products[1]->name = 'candle'; + $products[2]->name = 'plate'; + $products[1]->ownPriceList[] = $price; + $shop->ownProduct[] = $products[0]; + $shop->ownProduct[] = $products[1]; + $shop2->ownProduct[] = $products[2]; + R::storeAll( array( $shop, $shop2 ) ); + $collection = R::findMulti( 'shop,product,price', NULL, array(), array( + '0' => Finder::map( 'shop', 'product' ), + '1' => Finder::map( 'product', 'price' ), + )); + asrt( is_array( $collection ), TRUE ); + asrt( count( $collection ), 3 ); + asrt( count( $collection['shop'] ), 2 ); + asrt( count( $collection['product'] ), 3 ); + asrt( count( $collection['price'] ), 1 ); + $shop = reset( $collection['shop'] ); + asrt( count( $shop->ownProductList ), 2 ); + $shop2 = next( $collection['shop'] ); + asrt( count( $shop2->ownProductList ), 1 ); + $candle = NULL; + foreach( $shop->ownProduct as $product ) { + if ( $product->name == 'candle' ) { + $candle = $product; + } + } + asrt( is_null( $candle ), FALSE ); + asrt( count( $candle->ownPrice ), 1 ); + asrt( $candle->name, 'candle' ); + $price = reset( $candle->ownPrice ); + asrt( (int) $price->tag, 5 ); + } + + /** + * You can build your own mapping functions to remap records to bean. + * Just like the preloader once did. However now you can define the + * mapping yourself using closures. This test verifies that such a + * function would actually work. + * + * This method also tests whether empty records (resulting from LEFT JOINS for + * instance) do not produce unnecessary, empty beans. + * + * @return void + */ + public function testFindMultiExtFunc() + { + R::nuke(); + $shop = R::dispense( 'shop' ); + $shop2 = R::dispense( 'shop' ); + $products = R::dispense( 'product', 3 ); + $price = R::dispense( 'price' ); + $price->tag = 5; + $products[0]->name = 'vase'; + $products[1]->name = 'candle'; + $products[2]->name = 'plate'; + $products[1]->ownPriceList[] = $price; + $shop->ownProduct[] = $products[0]; + $shop->ownProduct[] = $products[1]; + $shop2->ownProduct[] = $products[2]; + R::storeAll( array( $shop, $shop2 ) ); + $collection = R::findMulti( 'shop,product,price', ' + SELECT shop.*, product.*, price.* FROM shop + LEFT JOIN product ON product.shop_id = shop.id + LEFT JOIN price ON price.product_id = product.id + ', array(), array( + '0' => Finder::map( 'shop', 'product' ), + '1' => Finder::map( 'product', 'price' ), + )); + asrt( is_array( $collection ), TRUE ); + asrt( count( $collection ), 3 ); + asrt( count( $collection['shop'] ), 2 ); + asrt( count( $collection['product'] ), 3 ); + asrt( count( $collection['price'] ), 1 ); + $shop = reset( $collection['shop'] ); + asrt( count( $shop->ownProductList ), 2 ); + $shop2 = next( $collection['shop'] ); + asrt( count( $shop2->ownProductList ), 1 ); + $candle = NULL; + foreach( $shop->ownProduct as $product ) { + if ( $product->name == 'candle' ) { + $candle = $product; + } + } + asrt( is_null( $candle ), FALSE ); + asrt( count( $candle->ownPrice ), 1 ); + asrt( $candle->name, 'candle' ); + $price = reset( $candle->ownPrice ); + asrt( (int) $price->tag, 5 ); + } + + /** + * Test findMuli with self-made arrays. + * + * @return void + */ + public function testFindMultiDirectArray() + { + R::nuke(); + $collection = R::findMulti( 'shop,product', array( + array( 'shop__id' => 1, 'product__id' => 1, 'product__name' => 'vase', 'product__shop_id' => 1 ), + array( 'shop__id' => 1, 'product__id' => 2, 'product__name' => 'candle', 'product__shop_id' => 1 ), + array( 'shop__id' => 1, 'product__id' => 3, 'product__name' => 'plate', 'product__shop_id' => 1 ), + ) ); + asrt( is_array( $collection ), TRUE ); + asrt( isset( $collection['shop'] ), TRUE ); + asrt( isset( $collection['product'] ), TRUE ); + asrt( (int) $collection['shop'][1]->id, 1 ); + asrt( (int) $collection['product'][1]->id, 1 ); + asrt( (int) $collection['product'][2]->id, 2 ); + asrt( (int) $collection['product'][3]->id, 3 ); + asrt( (int) $collection['product'][1]->shopID, 1 ); + asrt( (int) $collection['product'][2]->shopID, 1 ); + asrt( (int) $collection['product'][3]->shopID, 1 ); + asrt( $collection['product'][1]->name, 'vase' ); + asrt( $collection['product'][2]->name, 'candle' ); + asrt( $collection['product'][3]->name, 'plate' ); + R::nuke(); + $shop = R::dispense('shop'); + $shop2 = R::dispense('shop'); + $products = R::dispense('product', 3); + $price = R::dispense('price'); + $price->tag = 5; + $products[0]->name = 'vase'; + $products[1]->name = 'candle'; + $products[2]->name = 'plate'; + $products[1]->ownPriceList[] = $price; + $shop->ownProduct = $products; + R::store($shop); + $collection = R::findMulti('shop,product,price', ' + SELECT shop.*, product.*, price.* FROM shop + LEFT JOIN product ON product.shop_id = shop.id + LEFT JOIN price ON price.product_id = product.id + ', array(), array( + '0' => Finder::map('shop', 'product'), + '1' => Finder::map('product', 'price'), + )); + $collection = R::findMulti( 'shop,product', array( + array( 'shop__id' => 1, 'product__id' => 1, 'product__name' => 'vase', 'product__shop_id' => 1 ), + array( 'shop__id' => 1, 'product__id' => 2, 'product__name' => 'candle', 'product__shop_id' => 1 ), + array( 'shop__id' => 1, 'product__id' => 3, 'product__name' => 'plate', 'product__shop_id' => 1 ), + array( 'shop__id' => 1, 'product__id' => 2, 'product__name' => 'candle', 'product__shop_id' => 1) + ), array(), array( + array( + 'a' => 'shop', + 'b' => 'product', + 'matcher' => function( $a, $b ) { return ( $b->shopID == $a->id ); }, + 'do' => function( $a, $b ) { return $a->noLoad()->ownProductList[] = $b; } + ) + ) ); + asrt( is_array( $collection ), TRUE ); + asrt( isset( $collection['shop'] ), TRUE ); + asrt( isset( $collection['product'] ), TRUE ); + asrt( (int) $collection['shop'][1]->id, 1 ); + asrt( (int) $collection['product'][1]->id, 1 ); + asrt( (int) $collection['product'][2]->id, 2 ); + asrt( (int) $collection['product'][3]->id, 3 ); + asrt( (int) $collection['product'][1]->shopID, 1 ); + asrt( (int) $collection['product'][2]->shopID, 1 ); + asrt( (int) $collection['product'][3]->shopID, 1 ); + asrt( $collection['product'][1]->name, 'vase' ); + asrt( $collection['product'][2]->name, 'candle' ); + asrt( $collection['product'][3]->name, 'plate' ); + asrt( isset( $collection['shop'][1]->ownProductList ), TRUE ); + asrt( is_array( $collection['shop'][1]->ownProductList ), TRUE ); + asrt( count( $collection['shop'][1]->ownProductList ), 3 ); + asrt( $collection['shop'][1]->ownProductList[0]->name, 'vase' ); + asrt( $collection['shop'][1]->ownProductList[1]->name, 'candle' ); + asrt( $collection['shop'][1]->ownProductList[2]->name, 'plate' ); + } + + /** + * Test findMulti() with manual crafted fields. + * + * @return void + */ + public function testFindMultiDIY() + { + R::nuke(); + $movie = R::dispense( 'movie' ); + $review = R::dispense( 'review' ); + $movie->ownReviewList[] = $review; + $review->stars = 5; + $movie->title = 'Gambit'; + R::store( $movie ); + $stuff = R::findMulti( 'movie,review', 'SELECT + movie.id AS movie__id, + movie.title AS movie__title, + review.id AS review__id, + review.stars AS review__stars, + review.movie_id AS review__movie_id + FROM movie + LEFT JOIN review ON review.movie_id = movie.id + ' ); + asrt( count( $stuff ), 2 ); + asrt( isset( $stuff['movie'] ), TRUE ); + asrt( isset( $stuff['review'] ), TRUE ); + asrt( is_array( $stuff['movie'] ), TRUE ); + asrt( is_array( $stuff['review'] ), TRUE ); + asrt( count( $stuff['movie'] ), 1 ); + asrt( count( $stuff['review'] ), 1 ); + $movie = reset( $stuff['movie'] ); + asrt( $movie->title, 'Gambit' ); + $review = reset( $stuff['review'] ); + asrt( (int) $review->stars, 5 ); + R::nuke(); + $movie = R::dispense( 'movie' ); + $review = R::dispense( 'review' ); + $movie->ownReviewList[] = $review; + $review->stars = 5; + $movie->title = 'Gambit'; + R::store( $movie ); + $stuff = R::findMulti( array( 'movie', 'review' ), 'SELECT + movie.id AS movie__id, + movie.title AS movie__title, + review.id AS review__id, + review.stars AS review__stars, + review.movie_id AS review__movie_id + FROM movie + LEFT JOIN review ON review.movie_id = movie.id + ' ); + asrt( count( $stuff ), 2 ); + asrt( isset( $stuff['movie'] ), TRUE ); + asrt( isset( $stuff['review'] ), TRUE ); + asrt( is_array( $stuff['movie'] ), TRUE ); + asrt( is_array( $stuff['review'] ), TRUE ); + asrt( count( $stuff['movie'] ), 1 ); + asrt( count( $stuff['review'] ), 1 ); + $movie = reset( $stuff['movie'] ); + asrt( $movie->title, 'Gambit' ); + $review = reset( $stuff['review'] ); + asrt( (int) $review->stars, 5 ); + } + + /** + * Test findMulti(). Basic version. + * + * @return void + */ + public function testFindMulti() + { + $book = R::dispense( 'book' ); + $book->title = 'My Book'; + $book->ownPageList = R::dispense( 'page', 3 ); + $no = 1; + foreach( $book->ownPageList as $page ) { + $page->num = $no++; + } + R::store( $book ); + $collection = R::findMulti( 'book,page', ' + SELECT book.*, page.* FROM book + LEFT JOIN page ON page.book_id = book.id + ' ); + asrt( count( $collection ), 2 ); + asrt( isset( $collection['book'] ), TRUE ); + asrt( isset( $collection['page'] ), TRUE ); + asrt( count( $collection['book'] ), 1 ); + asrt( count( $collection['page'] ), 3 ); + foreach( $collection['book'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + foreach( $collection['page'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + $book = reset( $collection['book'] ); + asrt( $book->title, 'My Book' ); + $no = 1; + foreach( $collection['page'] as $page ) asrt( (int) $page->num, $no++ ); + R::nuke(); + $book->noLoad()->ownPageList = $collection['page']; + asrt( count( $book->ownPageList ), 3 ); + } + + /** + * Tests the complex use case for findMulti(). + * + * @return void + */ + public function testMultiAdvanced() + { + $this->insertBookData(); + $collection = R::findMulti( 'book,page,text,category', ' + SELECT book.*, page.*, text.*, category.* + FROM book + LEFT JOIN page ON page.book_id = book.id + LEFT JOIN text ON text.page_id = page.id + LEFT JOIN book_category ON book_category.book_id = book.id + LEFT JOIN category ON book_category.category_id = category.id + ' ); + asrt( count( $collection ), 4 ); + asrt( isset( $collection['book'] ), TRUE ); + asrt( isset( $collection['page'] ), TRUE ); + asrt( isset( $collection['text'] ), TRUE ); + asrt( isset( $collection['category'] ), TRUE ); + asrt( count( $collection['book'] ), 5 ); + asrt( count( $collection['page'] ), 9 ); + asrt( count( $collection['text'] ), 11 ); + asrt( count( $collection['category'] ), 3 ); + foreach( $collection['book'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + foreach( $collection['page'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + foreach( $collection['text'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + foreach( $collection['category'] as $bean ) asrt( ( $bean instanceof OODBBean ), TRUE ); + foreach( $collection['book'] as $book ) $titles[] = $book->title; + asrt( in_array( 'Diehard C', $titles ), TRUE ); + asrt( in_array( 'Adventures in JavaScript', $titles ), TRUE ); + asrt( in_array( 'CSS ala Picasso', $titles ), TRUE ); + asrt( in_array( 'PHP Tips and Tricks', $titles ), TRUE ); + asrt( in_array( 'Secrets of SQL', $titles ), TRUE ); + $collection = R::findMulti( 'book,page,text,category,book_category', ' + SELECT book.*, page.*, text.*, category.*, book_category.* + FROM book + LEFT JOIN page ON page.book_id = book.id + LEFT JOIN text ON text.page_id = page.id + LEFT JOIN book_category ON book_category.book_id = book.id + LEFT JOIN category ON book_category.category_id = category.id + WHERE category_id > ? + ORDER BY book.title,page.id ASC + ', array( 0 ), array( + array( + 'b'=>'page', + 'a'=>'text', + 'do' => function( $a, $b ) { + $b->noLoad()->ownTextList[] = $a; + $b->clearHistory(); + }, + 'matcher' => function( $a, $b ){ return ($a->page_id == $b->id); } + ), + array( + 'b'=>'book', + 'a'=>'page', + 'do' => function( $a, $b ) { + $b->noLoad()->ownPageList[] = $a; + $b->clearHistory(); + }, + 'matcher' => function( $a, $b ){ return ($a->book_id == $b->id); } + ), + array( + 'b' => 'category', + 'a' => 'book', + 'do' => function($a, $b) { + $a->noLoad()->sharedCategoryList[] = $b; + $a->clearHistory(); + }, + 'matcher' => function( $a, $b, $beans ) { + foreach( $beans['book_category'] as $bean ) { + if ( $bean->book_id == $a->id && $bean->category_id == $b->id ) return TRUE; + } + return FALSE; + } + ), + ) + ); + $books = $collection['book']; + $book = reset( $books ); + asrt( $book->title, 'Adventures in JavaScript' ); + R::nuke(); + asrt( count( $book->ownPageList ), 3 ); + $page = reset( $book->ownPageList ); + asrt( count( $page->ownTextList ), 1 ); + asrt( count( $book->sharedCategoryList ), 2); + $categories = array(); + foreach( $book->sharedCategoryList as $category ) { + $categories[] = $category->name; + } + sort( $categories ); + asrt( implode( ',', $categories ), 'Programming,Web Development' ); + $book = next( $books ); + asrt( $book->title, 'CSS ala Picasso' ); + asrt( count( $book->ownPage ), 1 ); + $page = reset( $book->ownPage ); + asrt( count( $page->ownTextList ), 2 ); + $texts = array(); + foreach( $page->ownTextList as $text ) $texts[] = $text->content; + asrt( in_array( 'Now we use it for applications...', $texts ), TRUE ); + $categories = array(); + foreach( $book->sharedCategoryList as $category ) { + $categories[] = $category->name; + } + sort( $categories ); + asrt( implode( ',', $categories ), 'Design,Programming,Web Development' ); + $book = next( $books ); + asrt( $book->title, 'Diehard C' ); + asrt( count( $book->ownPageList ), 2 ); + $page = reset( $book->ownPageList ); + asrt( count( $page->ownTextList ), 2 ); + $page = next( $book->ownPageList ); + asrt( count( $page->ownTextList ), 1 ); + $categories = array(); + foreach( $book->sharedCategoryList as $category ) { + $categories[] = $category->name; + } + sort( $categories ); + asrt( implode( ',', $categories ), 'Programming' ); + //should have no effect, nothing should have changed + R::storeAll($books); + asrt( R::count('book'), 0 ); + asrt( R::count('page'), 0 ); + asrt( R::count('text'), 0 ); + } + + /** + * Test forming IN-clause using genSlots and flat. + * + * @return void + */ + public function testINClause() + { + list( $flowers, $shop ) = R::dispenseAll( 'flower*4,shop' ); + $flowers[0]->color = 'red'; + $flowers[1]->color = 'yellow'; + $flowers[2]->color = 'blue'; + $flowers[3]->color = 'purple'; + $flowers[0]->price = 10; + $flowers[1]->price = 15; + $flowers[2]->price = 20; + $flowers[3]->price = 25; + $shop->xownFlowerList = $flowers; + R::store( $shop ); + $colors = array( 'red', 'yellow' ); + $result = $this->getColors( R::find( 'flower', ' color IN ('.R::genSlots( $colors ).' ) AND price < ?' , R::flat( array( $colors, 100 ) ) ) ); + asrt( $result, 'red,yellow' ); + $colors = array( 'red', 'yellow' ); + $result = $this->getColors( R::find( 'flower', ' color IN ('.R::genSlots( $colors ).' ) AND price < ?' , R::flat( array( $colors, 10 ) ) ) ); + asrt( $result, '' ); + $colors = array( 'red', 'yellow' ); + $result = $this->getColors( R::find( 'flower', ' color IN ('.R::genSlots( $colors ).' ) AND price < ?' , R::flat( array( $colors, 15 ) ) ) ); + asrt( $result, 'red' ); + asrt( json_encode( R::flat( array( 'a', 'b', 'c' ) ) ), '["a","b","c"]' ); + asrt( json_encode( R::flat( array( 'a', array( 'b' ), 'c' ) ) ), '["a","b","c"]' ); + asrt( json_encode( R::flat( array( 'a', array( 'b', array( 'c' ) ) ) ) ), '["a","b","c"]' ); + asrt( json_encode( R::flat( array( array( 'a', array( 'b', array( array( 'c' ) ) ) ) ) ) ), '["a","b","c"]' ); + asrt( json_encode( R::flat( array( 'a', 'b', 'c', array() ) ) ), '["a","b","c"]' ); + asrt( genslots( array( 1, 2 ) ), '?,?' ); + asrt( json_encode( array_flatten( array( array( 'a', array( 'b', array( array( 'c' ) ) ) ) ) ) ), '["a","b","c"]' ); + asrt( genslots( array( 1, 2 ), 'IN (%s) AND' ), 'IN (?,?) AND' ); + asrt( genslots( array(), ' IN (%s) AND ' ), '' ); + $colors = array( 'blue', 'purple', 'red' ); + $flowers = R::find( 'flower', genslots( $colors, ' color IN (%s) AND ' ).' price > ? ', array_flatten( array( $colors, 11 ) ) ); + asrt( $this->getColors( $flowers ), 'blue,purple' ); + $flowers = R::find( 'flower', genslots( array(), ' color IN (%s) AND ' ).' price > ? ', array_flatten( array( array(), 11 ) ) ); + asrt( $this->getColors( $flowers ), 'blue,purple,yellow' ); + $flowers = R::find( 'flower', ' id > 0 AND '.genslots( $colors, ' color IN (%s) AND ' ).' price > ? ', array_flatten( array( $colors, 11 ) ) ); + asrt( $this->getColors( $flowers ), 'blue,purple' ); + $flowers = R::find( 'flower', ' id > 0 AND '.genslots( array(), ' color IN (%s) AND ' ).' price > ? ', array_flatten( array( array(), 11 ) ) ); + asrt( $this->getColors( $flowers ), 'blue,purple,yellow' ); + } + + /** + * Test findLike. + * + * @return void + */ + public function testFindLike2() + { + list( $flowers, $shop ) = R::dispenseAll( 'flower*4,shop' ); + $flowers[0]->color = 'red'; + $flowers[1]->color = 'yellow'; + $flowers[2]->color = 'blue'; + $flowers[3]->color = 'purple'; + $flowers[0]->price = 10; + $flowers[1]->price = 15; + $flowers[2]->price = 20; + $flowers[3]->price = 25; + $shop->xownFlowerList = $flowers; + R::store( $shop ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array( 'red', 'yellow' ) ), ' price < 20' ) ), 'red,yellow' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ), '' ) ), 'blue,purple,red,yellow' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ) ) ), 'blue,purple,red,yellow' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array('blue') ), ' OR price = 25' ) ), 'blue,purple' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ), ' price < 25' ) ), 'blue,red,yellow' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ), ' price < 20' ) ), 'red,yellow' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ), ' ORDER BY color DESC' ), TRUE ), 'yellow,red,purple,blue' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array() ), ' ORDER BY color LIMIT 1' ) ), 'blue' ); + asrt( $this->getColors( R::findLike( 'flower', array( 'color' => array( 'yellow', 'blue' ) ), ' ORDER BY color ASC LIMIT 1' ) ), 'blue' ); + } + + /** + * Tests the findOrCreate method. + * + * @return void + */ + public function testFindOrCreate() + { + R::nuke(); + $created = false; + $book = R::findOrCreate( 'book', array( 'title' => 'my book', 'price' => 50 ), '', $created ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( $created, true ); + $id = $book->id; + $book = R::findOrCreate( 'book', array( 'title' => 'my book', 'price' => 50 ), '', $created ); + asrt( $book->id, $id ); + asrt( $book->title, 'my book' ); + asrt( (int) $book->price, 50 ); + asrt( $created, false ); + } + + /** + * Tests OODBBean as conditions + * + * @return void + */ + public function testFindLikeWithOODBBeans() { + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $page->book = $book; + R::store( $page ); + $book2 = R::dispense( 'book' ); + $page2 = R::dispense( 'page' ); + $page2->book = $book2; + R::store( $page2 ); + $pages = R::findLike( 'page', array( 'book_id' => array( 1, 2 ) ) ); + $pagesWithOODB = R::findLike( 'page', array( 'book' => array( $book, $book2 ) ) ); + asrt( count( $pagesWithOODB ), 2 ); + asrt( json_encode($pagesWithOODB), json_encode($pages) ); + asrt( reset( $pagesWithOODB )->id, $page->id ); + asrt( end( $pagesWithOODB )->id, $page2->id ); + $pages = R::findLike( 'page', array( 'book' => array( $book, $book2->id ) ) ); + asrt( count( $pages ), 2 ); + asrt( reset( $pages )->id, $page->id ); + asrt( end( $pages )->id, $page2->id ); + $pages = R::findLike( 'page', array( 'book_id' => array( $book->id, $book2 ) ) ); + asrt( count( $pages ), 2 ); + asrt( reset( $pages )->id, $page->id ); + asrt( end( $pages )->id, $page2->id ); + $pagesFail = R::findLike( 'page', array( 'book' => array( $book->id, $book2 ) ) ); + asrt( count( $pagesFail ), 0 ); + $book3 = R::dispense( 'book' ); + $page3 = R::dispense( 'page' ); + R::store( $page3 ); + $page3->book = $book3; + $pagesFail = R::findLike( 'page', array( 'book' => $book3 ) ); + asrt( count( $pagesFail ), 0 ); + $pen = R::dispense( 'pen' ); + R::store( $pen ); + asrt( $pen->id, $book->id ); + $pagesFail = R::findLike( 'page', array( 'book' => $pen ) ); + asrt( count( $pagesFail ), 0 ); + } + + /** + * Tests the findLike method. + * + * @return void + */ + public function testFindLike() + { + R::nuke(); + $book = R::dispense( array( + '_type' => 'book', + 'title' => 'my book', + 'price' => 80 + ) ); + R::store( $book ); + $book = R::dispense( array( + '_type' => 'book', + 'title' => 'other book', + 'price' => 80 + ) ); + R::store( $book ); + $books = R::findLike( 'book', array( 'price' => 80 ) ); + asrt( count( $books ), 2 ); + foreach( $books as $book ) { + asrt( $book->getMeta( 'type' ), 'book' ); + } + $books = R::findLike( 'book' ); + asrt( count( $books ), 2 ); + $books = R::findLike( 'book', array( 'title' => 'my book' ) ); + asrt( count( $books ), 1 ); + $books = R::findLike( 'book', array( 'title' => array( 'my book', 'other book' ) ) ); + asrt( count( $books ), 2 ); + $books = R::findLike( 'book', array( 'title' => 'strange book') ); + asrt( is_array( $books ), TRUE ); + asrt( count( $books ), 0 ); + $books = R::findLike( 'magazine' ); + asrt( is_array( $books ), TRUE ); + asrt( count( $books ), 0 ); + } + + /** + * Can we find books based on associations with other + * entities? + * + * @return void + */ + public function testFindLikeBean() + { + R::nuke(); + $book1 = R::dispense( 'book' ); + $page1 = R::dispense( 'page' ); + $book2 = R::dispense( 'book' ); + $page2 = R::dispense( 'page' ); + $book1->page = $page1; + $book2->page = $page2; + R::storeAll( array( $book1, $book2 ) ); + $books = R::findLike( 'book', array( 'page' => array( $page2 ) ), ' AND id > ?', array( 0 ) ); + $book = reset( $books ); + asrt( $book->id, $book2->id ); + $books = R::findLike( 'book', array( 'page' => array( $page1 ) ), ' AND id > ?', array( 0 ) ); + $book = reset( $books ); + asrt( $book->id, $book1->id ); + } + + /** + * Test whether findOne gets a LIMIT 1 + * clause. + * + * @return void + */ + public function testFindOneLimitOne() + { + R::nuke(); + list( $book1, $book2 ) = R::dispense( 'book', 2 ); + $book1->title = 'a'; + $book2->title = 'b'; + R::storeAll( array( $book1, $book2 ) ); + $logger = R::debug( 1, 1 ); + $logger->clear(); + $found = R::findOne( 'book' ); + asrt( count( $logger->grep('LIMIT 1') ), 1 ); + asrt( ( $found instanceof \RedBeanPHP\OODBBean ), TRUE ); + $logger->clear(); + $found = R::findOne( 'book', ' title = ? ', array( 'a' ) ); + asrt( count( $logger->grep('LIMIT 1') ), 1 ); + asrt( ( $found instanceof \RedBeanPHP\OODBBean ), TRUE ); + $logger->clear(); + $found = R::findOne( 'book', ' title = ? LIMIT 1', array( 'b' ) ); + asrt( count( $logger->grep('LIMIT 1') ), 1 ); + $logger->clear(); + $found = R::findOne( 'book', ' title = ? limit 1', array( 'b' ) ); + asrt( count( $logger->grep('LIMIT 1') ), 0 ); + asrt( count( $logger->grep('limit 1') ), 1 ); + asrt( ( $found instanceof \RedBeanPHP\OODBBean ), TRUE ); + $found = R::findOne( 'book', ' title = ? LIMIT 2', array( 'b' ) ); + asrt( count( $logger->grep('LIMIT 2') ), 1 ); + asrt( ( $found instanceof \RedBeanPHP\OODBBean ), TRUE ); + } + + /** + * Begin testing. + * This method runs the actual test pack. + * + * @return void + */ + public function testFinding() + { + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + $page = $redbean->dispense( "page" ); + $page->name = "John's page"; + $idpage = $redbean->store( $page ); + $page2 = $redbean->dispense( "page" ); + $page2->name = "John's second page"; + $idpage2 = $redbean->store( $page2 ); + $a->associate( $page, $page2 ); + $pageOne = $redbean->dispense( "page" ); + $pageOne->name = "one"; + $pageMore = $redbean->dispense( "page" ); + $pageMore->name = "more"; + $pageEvenMore = $redbean->dispense( "page" ); + $pageEvenMore->name = "evenmore"; + $pageOther = $redbean->dispense( "page" ); + $pageOther->name = "othermore"; + set1toNAssoc( $a, $pageOther, $pageMore ); + set1toNAssoc( $a, $pageOne, $pageMore ); + set1toNAssoc( $a, $pageOne, $pageEvenMore ); + asrt( count( $redbean->find( "page", array(), " name LIKE '%more%' ", array() ) ), 3 ); + asrt( count( $redbean->find( "page", array(), " name LIKE :str ", array( ":str" => '%more%' ) ) ), 3 ); + asrt( count( $redbean->find( "page", array(), array( " name LIKE :str ", array( ":str" => '%more%' ) ) ) ), 3 ); + asrt( count( $redbean->find( "page", array(), " name LIKE :str ", array( ":str" => '%mxore%' ) ) ), 0 ); + asrt( count( $redbean->find( "page", array( "id" => array( 2, 3 ) ) ) ), 2 ); + $bean = $redbean->dispense( "wine" ); + $bean->name = "bla"; + for ( $i = 0; $i < 10; $i++ ) { + $redbean->store( $bean ); + } + $redbean->find( "wine", array( "id" => 5 ) ); // Finder:where call OODB::convertToBeans + $bean2 = $redbean->load( "anotherbean", 5 ); + asrt( $bean2->id, 0 ); + $keys = $adapter->getCol( "SELECT id FROM page WHERE " . $writer->esc( 'name' ) . " LIKE '%John%'" ); + asrt( count( $keys ), 2 ); + $pages = $redbean->batch( "page", $keys ); + asrt( count( $pages ), 2 ); + $p = R::findLast( 'page' ); + pass(); + $row = R::getRow( 'select * from page ' ); + asrt( is_array( $row ), TRUE ); + asrt( isset( $row['name'] ), TRUE ); + // Test findAll -- should not throw an exception + asrt( count( R::findAll( 'page' ) ) > 0, TRUE ); + asrt( count( R::findAll( 'page', ' ORDER BY id ' ) ) > 0, TRUE ); + $beans = R::findOrDispense( "page" ); + asrt( count( $beans ), 6 ); + asrt( is_null( R::findLast( 'nothing' ) ), TRUE ); + try { + R::find( 'bean', ' id > 0 ', 'invalid bindings argument' ); + fail(); + } catch ( RedException $exception ) { + pass(); + } + R::nuke(); + $bean = R::findOneOrDispense( 'jellybean' ); + asrt( is_object( $bean ), TRUE ); + } + + /** + * Test tree traversal with searchIn(). + * + * @return void + */ + public function testTreeTraversal() + { + testpack( 'Test Tree Traversal' ); + R::nuke(); + $page = R::dispense( 'page', 10 ); + //Setup the test data for this series of tests + $i = 0; + foreach( $page as $pageItem ) { + $pageItem->name = 'page' . $i; + $pageItem->number = $i; + $i++; + R::store( $pageItem ); + } + $page[0]->ownPage = array( $page[1], $page[2] ); + $page[1]->ownPage = array( $page[3], $page[4] ); + $page[3]->ownPage = array( $page[5] ); + $page[5]->ownPage = array( $page[7] ); + $page[9]->document = $page[8]; + $page[9]->book = R::dispense('book'); + R::store( $page[9] ); + $id = R::store( $page[0] ); + $book = $page[9]->book; + } + + /** + * Test find and export. + * + * @return void + */ + public function testFindAndExport() + { + R::nuke(); + $pages = R::dispense( 'page', 3 ); + $i = 1; + foreach( $pages as $page ) { + $page->pageNumber = $i++; + } + R::storeAll( $pages ); + $pages = R::findAndExport( 'page' ); + asrt( is_array( $pages ), TRUE ); + asrt( isset( $pages[0] ), TRUE ); + asrt( is_array( $pages[0] ), TRUE ); + asrt( count( $pages ), 3 ); + } + + /** + * Test error handling of SQL states. + * + * @return void + */ + public function testFindError() + { + R::freeze( FALSE ); + $page = R::dispense( 'page' ); + $page->title = 'abc'; + R::store( $page ); + //Column does not exist, in fluid mode no error! + try { + R::find( 'page', ' xtitle = ? ', array( 'x' ) ); + pass(); + } catch ( SQL $e ) { + fail(); + } + //Table does not exist, in fluid mode no error! + try { + R::find( 'pagex', ' title = ? ', array( 'x' ) ); + pass(); + } catch ( SQL $e ) { + fail(); + } + //Syntax error, error in fluid mode if possible to infer from SQLSTATE (MySQL/Postgres) + try { + R::find( 'page', ' invalid SQL ' ); + //In SQLite only get HY000 - not very descriptive so suppress more errors in fluid mode then. + if ( + $this->currentlyActiveDriverID === 'sqlite' + || $this->currentlyActiveDriverID === 'CUBRID' ) { + pass(); + } else { + fail(); + } + } catch ( SQL $e ) { + pass(); + } + //Frozen, always error... + R::freeze( TRUE ); + //Column does not exist, in frozen mode error! + try { + R::find( 'page', ' xtitle = ? ', array( 'x' ) ); + fail(); + } catch ( SQL $e ) { + pass(); + } + //Table does not exist, in frozen mode error! + try { + R::find( 'pagex', ' title = ? ', array( 'x' ) ); + fail(); + } catch ( SQL $e ) { + pass(); + } + //Syntax error, in frozen mode error! + try { + R::find( 'page', ' invalid SQL ' ); + fail(); + } catch ( SQL $e ) { + pass(); + } + R::freeze( FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Foreignkeys.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Foreignkeys.php new file mode 100644 index 0000000..54b149a --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Foreignkeys.php @@ -0,0 +1,329 @@ +sharedCategory[] = $category; + R::store( $book ); + asrt( count( get_uniques_for_type('book_category') ), 1 ); + asrt( are_cols_in_unique( 'book_category', array( 'book_id', 'category_id' ) ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $category = R::dispense( 'category' ); + $book->via( 'library' )->sharedCategory[] = $category; + R::store( $book ); + asrt( count( get_uniques_for_type('book_category') ), 0 ); + asrt( are_cols_in_unique( 'book_category', array( 'book_id', 'category_id' ) ), FALSE ); + asrt( count( get_uniques_for_type('library') ), 1 ); + asrt( are_cols_in_unique( 'library', array( 'book_id', 'category_id' ) ), TRUE ); + AQueryWriter::clearRenames(); + R::nuke(); + $book = R::dispense( 'book' ); + $category = R::dispense( 'category' ); + $book->sharedCategory[] = $category; + R::store( $book ); + asrt( count( get_uniques_for_type('book_category') ), 1 ); + asrt( are_cols_in_unique( 'book_category', array( 'book_id', 'category_id' ) ), TRUE ); + asrt( count( get_uniques_for_type('library') ), 0 ); + asrt( are_cols_in_unique( 'library', array( 'book_id', 'category_id' ) ), FALSE ); + R::nuke(); + $book = R::dispense( 'book' ); + $book2 = R::dispense( 'book' ); + $book->sharedBook[] = $book2; + R::store( $book ); + asrt( count( get_uniques_for_type('book_book') ), 1 ); + asrt( are_cols_in_unique( 'book_book', array( 'book_id', 'book2_id' ) ), TRUE ); + try { + $result = R::getWriter()->addUniqueConstraint( 'nonexistant', array( 'a', 'b' ) ); + } catch( \Exception $e ) { + print_r( $e ); exit; + } + pass(); //dont crash! + asrt( $result, FALSE ); + } + + /** + * Tests foreign keys but checks using ProxyWriter. + * + * @return void + */ + public function testFKInspect() + { + $faultyWriter = new \FaultyWriter( R::getDatabaseAdapter() ); + try { + $null = \ProxyWriter::callMethod( $faultyWriter, 'getForeignKeyForTypeProperty', 'test', 'test' ); + pass(); + } catch( \Exception $e ) { + fail(); + } + asrt( is_null( $null ), TRUE ); + $writer = R::getWriter(); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->xownPage[] = $page; + R::store( $book ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'book_id' ); + asrt( is_array( $keys ), TRUE ); + asrt( $keys['on_delete'], 'CASCADE' ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'id' ); + asrt( is_null( $keys ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->ownPage[] = $page; + R::store( $book ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'book_id' ); + asrt( is_array( $keys ), TRUE ); + asrt( $keys['on_delete'], 'SET NULL' ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'id' ); + asrt( is_null( $keys ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->alias('magazine')->xownPage[] = $page; + R::store( $book ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'magazine_id' ); + asrt( is_array( $keys ), TRUE ); + asrt( $keys['on_delete'], 'CASCADE' ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'book_id' ); + asrt( is_null( $keys ), TRUE ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'page', 'id' ); + asrt( is_null( $keys ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->cover= $page; + R::store( $book ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book', 'cover_id' ); + asrt( is_array( $keys ), TRUE ); + asrt( $keys['on_delete'], 'SET NULL' ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book', 'page_id' ); + asrt( is_null( $keys ), TRUE ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book', 'id' ); + asrt( is_null( $keys ), TRUE ); + R::nuke(); + $book = R::dispense( 'book' ); + $category = R::dispense( 'category' ); + $book->sharedTag[] = $category; + R::store( $book ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book_category', 'book_id' ); + asrt( is_array( $keys ), TRUE ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book_category', 'category_id' ); + asrt( is_array( $keys ), TRUE ); + $keys = \ProxyWriter::callMethod( $writer, 'getForeignKeyForTypeProperty', 'book_category', 'id' ); + asrt( is_null( $keys ), TRUE ); + } + + /** + * Test dependencies. + * + * @return void + */ + public function testDependency() + { + $can = $this->createBeanInCan( FALSE ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + // Bean stays + asrt( R::count( 'bean' ), 1 ); + } + + /** + * Test dependencies (variation). + * + * @return void + */ + public function testDependency2() + { + $can = $this->createBeanInCan( TRUE ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + // Bean gone + asrt( R::count( 'bean' ), 0 ); + $can = $this->createBeanInCan( FALSE ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + // Bean stays, constraint removed + asrt( R::count( 'bean' ), 0 ); + //need to recreate table to get rid of constraint! + R::nuke(); + $can = $this->createBeanInCan( FALSE ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + // Bean stays, constraint removed + asrt( R::count( 'bean' ), 1 ); + } + + /** + * Tests dependencies (variation). + * + * @return void + */ + public function testDependency3() + { + R::nuke(); + $can = $this->createCanForBean(); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + asrt( R::count( 'bean' ), 1 ); + } + + /** + * Tests dependencies (variation). + * + * @return void + */ + public function testDependency4() + { + R::nuke(); + $can = $this->createBeanInCan( TRUE ); + R::store( $can ); + R::trash( $can ); + $can = $this->createCanForBean(); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + asrt( R::count( 'bean' ), 0 ); + $can = $this->createBeanInCan( TRUE ); + R::store( $can ); + R::trash( $can ); + $can = $this->createCanForBean(); + asrt( R::count( 'bean' ), 1 ); + R::trash( $can ); + asrt( R::count( 'bean' ), 0 ); + } + + /** + * Issue #171 + * The index name argument is not unique in processEmbeddedBean etc. + * + * @return void + */ + public function testIssue171() + { + R::getDatabaseAdapter()->addEventListener( 'sql_exec', $this ); + $account = R::dispense( 'account' ); + $user = R::dispense( 'user' ); + $player = R::dispense( 'player' ); + $account->ownUser[] = $user; + R::store( $account ); + asrt( strpos( implode( ',', $this->queries ), 'index_foreignkey_user_account' ) !== FALSE, TRUE ); + $this->queries = array(); + $account->ownPlayer[] = $player; + R::store( $account ); + asrt( strpos( implode( ',', $this->queries ), 'index_foreignkey_player_accou' ) !== FALSE, TRUE ); + } + + /** + * Tests whether foreign keys are created correctly for certain + * relations. + * + * @return void + */ + public function testCreationOfForeignKeys() + { + $this->queries = array(); + $account = R::dispense( 'account' ); + $user = R::dispense( 'user' ); + $player = R::dispense( 'player' ); + $user->account = $account; + R::store( $user ); + asrt( strpos( implode( ',', $this->queries ), 'index_foreignkey_user_account' ) !== FALSE, TRUE ); + $this->queries = array(); + $player->account = $account; + R::store( $player ); + asrt( strpos( implode( ',', $this->queries ), 'index_foreignkey_player_accou' ) !== FALSE, TRUE ); + } + + /** + * Test helper method. + * Creates a bean in a can. The bean will get a reference + * to the can and can be made dependent. + * + * @return OODBBean $can + */ + private function createBeanInCan( $isExcl ) + { + $can = R::dispense( 'can' ); + $bean = R::dispense( 'bean' ); + $can->name = 'bakedbeans'; + $bean->taste = 'salty'; + if ($isExcl) { + $can->xownBean[] = $bean; + } else { + $can->ownBean[] = $bean; + } + R::store( $can ); + return $can; + } + + /** + * Test helper method. + * Creates a bean in a can beginning with the bean. The bean will get a reference + * to the can and can be made dependent. + * + * @return OODBBean $can + */ + private function createCanForBean() + { + $can = R::dispense( 'can' ); + $bean = R::dispense( 'bean' ); + $bean->can = $can; + R::store( $bean ); + return $can; + } + + /** + * Log queries + * + * @param string $event + * @param Adapter $info + */ + public function onEvent( $event, $info ) + { + $this->queries[] = $info->getSQL(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Frozen.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Frozen.php new file mode 100644 index 0000000..1c95f56 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Frozen.php @@ -0,0 +1,104 @@ +xownPageList[] = R::dispense( 'page' ); + $book->sharedTagList[] = R::dispense( 'tag' ); + R::store( $book ); + $book = $book->fresh(); + R::freeze( TRUE ); + $book->xownPageList = array(); + R::store( $book ); + $book = $book->fresh(); + asrt( R::count('page'), 0 ); + $book->xownPageList[] = R::dispense( 'page' ); + R::store( $book ); + $book = $book->fresh(); + asrt( R::count('page'), 1 ); + $book->xownPageList; + $book->sharedTagList; + R::trash( $book ); + asrt( R::count('book'), 0 ); + asrt( R::count('page'), 0 ); + asrt( R::count('tag'), 1 ); + asrt( R::count('book_tag'), 0 ); + R::freeze( FALSE ); + } + + /** + * Tests whether invalid list checks are + * operational in frozen mode. + * + * @return void + */ + public function testInvalidList() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->xownPageList[] = R::dispense( 'page' ); + $book->sharedTagList[] = R::dispense( 'tag' ); + R::store( $book ); + R::freeze( TRUE ); + $book = R::dispense( 'book' ); + $book->xownPageList[] = 'nonsense'; + try { + R::store( $book ); + fail(); + } catch( \Exception $e ) { + pass(); + } + R::freeze( FALSE ); + } + + /** + * Tests whether loading non-existant beans + * returns the same results in frozen mode. + * + * @return + */ + public function testLoadNonExistant() + { + R::nuke(); + R::store( R::dispense( 'bean' ) ); + R::freeze( TRUE ); + $bean = R::load( 'bean', 123 ); + R::freeze( FALSE ); + asrt( ( $bean instanceof OODBBean ), TRUE ); + asrt( $bean->id, 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Fuse.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Fuse.php new file mode 100644 index 0000000..c36c9dd --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Fuse.php @@ -0,0 +1,420 @@ +variant = 'Tropical'; + $coffee->strength = 4; + $json = json_encode( $coffee ); + $array = json_decode( $json, TRUE ); + asrt( isset( $array['description'] ), TRUE ); + asrt( $array['description'], 'Tropical.4' ); + } + + /** + * Test whether we can override the getModelForBean() method + * of the BeanHelper and use a custom BeanHelper to attach a model + * based on type. + * + * @return void + */ + public function testCustomBeanHelper() + { + $customBeanHelper = new \SoupBeanHelper( R::getToolbox() ); + $oldBeanHelper = R::getRedBean()->getBeanHelper(); + asrt( ( $oldBeanHelper instanceof SimpleFacadeBeanHelper ), TRUE ); + R::getRedBean()->setBeanHelper( $customBeanHelper ); + $meal = R::dispense( 'meal' ); + asrt( ( $meal->box() instanceof \Model_Soup ), TRUE ); + $cake = R::dispense( 'cake' ); + asrt( is_null( $cake->box() ), TRUE ); + $bean = R::dispense( 'coffee' ); + asrt( ( $bean->box() instanceof \Model_Coffee ), TRUE ); + $meal->setFlavour( 'tomato' ); + asrt( $meal->getFlavour(), 'tomato' ); + $meal->rating = 5; + R::store( $meal ); + asrt( $meal->getFlavour(), 'tomato' ); + $meal = $meal->unbox(); + asrt( $meal->getFlavour(), 'tomato' ); + $meal = R::findOne( 'meal' ); + asrt( ( $meal->box() instanceof \Model_Soup ), TRUE ); + asrt( $meal->getFlavour(), '' ); + $meal->setFlavour( 'tomato' ); + asrt( $meal->getFlavour(), 'tomato' ); + $meal = $meal->unbox(); + asrt( $meal->getFlavour(), 'tomato' ); + R::getRedBean()->setBeanHelper( $oldBeanHelper ); + } + + /** + * Test FUSE hooks (i.e. open, update, update_after etc..) + * + * @return void + */ + public function testHooks() + { + R::nuke(); + $probe = R::dispense( 'probe' ); + $probe->name = 'test'; + asrt( $probe->getLogActionCount(), 1 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 0 ); + asrt( $probe->getLogActionCount( 'after_update' ), 0 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 0, 'bean' ) === $probe ), TRUE ); + R::store( $probe ); + asrt( $probe->getLogActionCount(), 3 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 1 ); + asrt( $probe->getLogActionCount( 'after_update' ), 1 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 2, 'bean' ) === $probe ), TRUE ); + $probe = R::load( 'probe', $probe->id ); + asrt( $probe->getLogActionCount(), 2 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 1 ); + asrt( $probe->getLogActionCount( 'update' ), 0 ); + asrt( $probe->getLogActionCount( 'after_update' ), 0 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 0, 'bean' ) === $probe ), TRUE ); + asrt( ( $probe->getDataFromLog( 1, 'id' ) === $probe->id ), TRUE ); + $probe->clearLog(); + R::trash( $probe ); + asrt( $probe->getLogActionCount(), 2 ); + asrt( $probe->getLogActionCount( 'dispense' ), 0 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 0 ); + asrt( $probe->getLogActionCount( 'after_update' ), 0 ); + asrt( $probe->getLogActionCount( 'delete' ), 1 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 1 ); + asrt( ( $probe->getDataFromLog( 0, 'bean' ) === $probe ), TRUE ); + asrt( ( $probe->getDataFromLog( 1, 'bean' ) === $probe ), TRUE ); + //less 'normal scenarios' + $probe = R::dispense( 'probe' ); + $probe->name = 'test'; + asrt( $probe->getLogActionCount(), 1 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 0 ); + asrt( $probe->getLogActionCount( 'after_update' ), 0 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 0, 'bean' ) === $probe ), TRUE ); + R::store( $probe ); + asrt( $probe->getLogActionCount(), 3 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 1 ); + asrt( $probe->getLogActionCount( 'after_update' ), 1 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 2, 'bean' ) === $probe ), TRUE ); + asrt( $probe->getMeta( 'tainted' ), FALSE ); + asrt( $probe->getMeta( 'changed' ), FALSE ); + R::store( $probe ); //not tainted, no FUSE save! + asrt( $probe->getLogActionCount(), 3 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 1 ); + asrt( $probe->getLogActionCount( 'after_update' ), 1 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 2, 'bean' ) === $probe ), TRUE ); + $probe->xownProbeList[] = R::dispense( 'probe' ); + //tainted, not changed, triggers FUSE + asrt( $probe->getMeta( 'tainted' ), TRUE ); + asrt( $probe->getMeta( 'changed' ), FALSE ); + R::store( $probe ); + asrt( $probe->getMeta( 'tainted' ), FALSE ); + asrt( $probe->getMeta( 'changed' ), FALSE ); + asrt( $probe->getLogActionCount(), 5 ); + asrt( $probe->getLogActionCount( 'dispense' ), 1 ); + asrt( $probe->getLogActionCount( 'open' ), 0 ); + asrt( $probe->getLogActionCount( 'update' ), 2 ); + asrt( $probe->getLogActionCount( 'after_update' ), 2 ); + asrt( $probe->getLogActionCount( 'delete' ), 0 ); + asrt( $probe->getLogActionCount( 'after_delete' ), 0 ); + asrt( ( $probe->getDataFromLog( 2, 'bean' ) === $probe ), TRUE ); + } + + /** + * Tests the SimpleFacadeBeanHelper factory setter. + * + * @return void + */ + public function testFactory() + { + SimpleFacadeBeanHelper::setFactoryFunction( function( $name ) { + $model = new $name(); + $model->setNote( 'injected', 'dependency' ); + return $model; + } ); + $bean = R::dispense( 'band' )->box(); + asrt( ( $bean instanceof \Model_Band ), TRUE ); + asrt( ( $bean->getNote('injected') ), 'dependency' ); + SimpleFacadeBeanHelper::setFactoryFunction( NULL ); + } + + /** + * Make sure that beans of type book_page can be fused with + * models like BookPage (beautified) as well as Book_Page (non-beautified). + */ + public function testBeutificationOfLinkModel() + { + $page = R::dispense( 'page' ); + $widget = R::dispense( 'widget' ); + $page->sharedWidgetList[] = $widget; + R::store( $page ); + $testReport = \Model_PageWidget::getTestReport(); + asrt( $testReport, 'didSave' ); + $page = R::dispense( 'page' ); + $gadget = R::dispense( 'gadget' ); + $page->sharedGadgetList[] = $gadget; + R::store( $page ); + $testReport = \Model_Gadget_Page::getTestReport(); + asrt( $testReport, 'didSave' ); + } + + /** + * Only theoretical. + * + * @return void + */ + public function testTheoreticalBeautifications() + { + $bean = R::dispense('bean'); + $bean->setMeta('type', 'a_b_c'); + R::store($bean); + $testReport = \Model_A_B_C::getTestReport(); + asrt( $testReport, 'didSave' ); + } + + /** + * Test extraction of toolbox. + * + * @return void + */ + public function testGetExtractedToolBox() + { + $helper = new SimpleFacadeBeanHelper; + list( $redbean, $database, $writer, $toolbox ) = $helper->getExtractedToolbox(); + asrt( ( $redbean instanceof OODB ), TRUE ); + asrt( ( $database instanceof Adapter ), TRUE ); + asrt( ( $writer instanceof QueryWriter ), TRUE ); + asrt( ( $toolbox instanceof ToolBox ), TRUE ); + } + + /** + * Test FUSE and model formatting. + * + * @todo move tagging tests to tag tester. + * + * @return void + */ + public function testFUSE() + { + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $blog = R::dispense( 'blog' ); + $blog->title = 'testing'; + $blog->blog = 'tesing'; + R::store( $blog ); + $blogpost = R::load( "blog", 1 ); + $post = R::dispense( "post" ); + $post->message = "hello"; + $blog->sharedPost[] = $post; + R::store($blog); + $a = R::getAll( "select * from blog " ); + R::tag( $post, "lousy,smart" ); + asrt( implode( ',', R::tag( $post ) ), "lousy,smart" ); + R::tag( $post, "clever,smart" ); + $tagz = implode( ',', R::tag( $post ) ); + asrt( ( $tagz == "smart,clever" || $tagz == "clever,smart" ), TRUE ); + R::tag( $blog, array( "smart", "interesting" ) ); + asrt( implode( ',', R::tag( $blog ) ), "smart,interesting" ); + try { + R::tag( $blog, array( "smart", "interesting", "lousy!" ) ); + pass(); + } catch ( RedException $e ) { + fail(); + } + asrt( implode( ',', R::tag( $blog ) ), "smart,interesting,lousy!" ); + asrt( implode( ",", R::tag( $blog ) ), "smart,interesting,lousy!" ); + R::untag( $blog, array( "smart", "interesting" ) ); + asrt( implode( ",", R::tag( $blog ) ), "lousy!" ); + asrt( R::hasTag( $blog, array( "lousy!" ) ), TRUE ); + asrt( R::hasTag( $blog, array( "lousy!", "smart" ) ), TRUE ); + asrt( R::hasTag( $blog, array( "lousy!", "smart" ), TRUE ), FALSE ); + R::tag( $blog, FALSE ); + asrt( count( R::tag( $blog ) ), 0 ); + R::tag( $blog, array( "funny", "comic" ) ); + asrt( count( R::tag( $blog ) ), 2 ); + R::addTags( $blog, array( "halloween" ) ); + asrt( count( R::tag( $blog ) ), 3 ); + asrt( R::hasTag( $blog, array( "funny", "commic", "halloween" ), TRUE ), FALSE ); + R::unTag( $blog, array( "funny" ) ); + R::addTags( $blog, "horror" ); + asrt( count( R::tag( $blog ) ), 3 ); + asrt( R::hasTag( $blog, array( "horror", "commic", "halloween" ), TRUE ), FALSE ); + // No double tags + R::addTags( $blog, "horror" ); + asrt( R::hasTag( $blog, array( "horror", "commic", "halloween" ), TRUE ), FALSE ); + asrt( count( R::tag( $blog ) ), 3 ); + } + + /** + * Test error handling options FUSE. + */ + public function testModelErrorHandling() + { + $test = R::dispense( 'feed' ); + $test->nonExistantMethod(); + pass(); + $old = R::setErrorHandlingFUSE( OODBBean::C_ERR_LOG ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], FALSE ); + asrt( $old[1], NULL); + $test->nonExistantMethod(); //we cant really test this... :( + pass(); + $old = R::setErrorHandlingFUSE( OODBBean::C_ERR_NOTICE ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_LOG ); + asrt( $old[1], NULL); + set_error_handler(function($error, $str) { + asrt( $str, 'FUSE: method does not exist in model: nonExistantMethod' ); + }, E_USER_NOTICE); + $test->nonExistantMethod(); + restore_error_handler(); + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_WARN ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_NOTICE ); + asrt( $old[1], NULL); + set_error_handler(function($error, $str) { + asrt( $str, 'FUSE: method does not exist in model: nonExistantMethod' ); + }, E_USER_WARNING); + $test->nonExistantMethod(); + restore_error_handler(); + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_FATAL ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_WARN ); + asrt( $old[1], NULL); + set_error_handler(function($error, $str) { + asrt( $str, 'FUSE: method does not exist in model: nonExistantMethod' ); + }, E_USER_ERROR); + $test->nonExistantMethod(); + restore_error_handler(); + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_EXCEPTION ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_FATAL ); + asrt( $old[1], NULL); + try { + $test->nonExistantMethod(); + fail(); + } catch (\Exception $e) { + pass(); + } + global $test_bean; + $test_bean = $test; + global $has_executed_error_func_fuse; + $has_executed_error_func_fuse = FALSE; + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_FUNC, function( $info ){ + global $has_executed_error_func_fuse; + global $test_bean; + $has_executed_error_func_fuse = TRUE; + asrt( is_array( $info ), TRUE ); + asrt( $info['method'], 'nonExistantMethod' ); + asrt( json_encode( $info['bean']->export() ), json_encode( $test_bean->export() ) ); + asrt( $info['message'], 'FUSE: method does not exist in model: nonExistantMethod' ); + } ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_EXCEPTION ); + asrt( $old[1], NULL); + $test->nonExistantMethod(); + asrt( $has_executed_error_func_fuse, TRUE ); + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_IGNORE ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_FUNC ); + asrt( is_callable( $old[1] ), TRUE ); + $old = OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_IGNORE ); + asrt( is_array( $old ), TRUE ); + asrt( count( $old ), 2 ); + asrt( $old[0], OODBBean::C_ERR_IGNORE ); + asrt( $old[1], NULL); + try { + OODBBean::setErrorHandlingFUSE( 900 ); + fail(); + } catch (\Exception $e) { + pass(); + asrt( $e->getMessage(), 'Invalid error mode selected' ); + } + try { + OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_FUNC, 'hello' ); + fail(); + } catch (\Exception $e) { + pass(); + asrt( $e->getMessage(), 'Invalid error handler' ); + } + OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_EXCEPTION ); + //make sure ignore FUSE events + $test = R::dispense('feed'); + R::store( $test ); + $test = $test->fresh(); + R::trash( $test ); + pass(); + OODBBean::setErrorHandlingFUSE( OODBBean::C_ERR_IGNORE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Hybrid.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Hybrid.php new file mode 100644 index 0000000..1b23013 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Hybrid.php @@ -0,0 +1,175 @@ +pages = 123; + $id = R::store( $book ); + R::freeze( TRUE ); + R::setAllowHybridMode( FALSE ); + $book->title = 'Tales of a misfit'; + try { + R::store( $book, TRUE ); + fail(); + } catch(\Exception $e) { + pass(); + } + try { + R::store( $book, FALSE ); + fail(); + } catch(\Exception $e) { + pass(); + } + $book = $book->fresh(); + asrt( is_null( $book->title ), TRUE ); + R::setAllowHybridMode( TRUE ); + $book->title = 'Tales of a misfit'; + try { + R::store( $book ); + fail(); + } catch(\Exception $e) { + pass(); + } + try { + R::store( $book, FALSE ); + fail(); + } catch(\Exception $e) { + pass(); + } + try { + R::store( $book, TRUE ); + pass(); + } catch(\Exception $e) { + fail(); + } + $book = $book->fresh(); + asrt( $book->title, 'Tales of a misfit' ); + R::setAllowHybridMode( FALSE ); + R::freeze( FALSE ); + } + + /** + * Test whether we can use Hybrid mode to alter columns. + * This won't work for SQLite. + */ + public function testHybridDataType() + { + R::nuke(); + if ($this->currentlyActiveDriverID == 'mysql') { + R::exec('SET @@SESSION.sql_mode=\'STRICT_TRANS_TABLES\';'); + } + if ($this->currentlyActiveDriverID == 'sqlite') return; + $book = R::dispense('book'); + $book->pages = 1; + $id = R::store( $book, TRUE ); + R::freeze( TRUE ); + asrt( R::getRedBean()->isFrozen(), TRUE ); + R::setAllowHybridMode( FALSE ); + $book->pages = 'too many'; + try { + R::store( $book, TRUE ); + fail(); + } catch(\Exception $e) { + pass(); + } + asrt( R::getRedBean()->isFrozen(), TRUE ); + R::setAllowHybridMode( TRUE ); + asrt( R::getRedBean()->isFrozen(), TRUE ); + R::debug(1); + try { + R::store( $book, TRUE ); + pass(); + } catch(\Exception $e) { + fail(); + } + asrt( R::getRedBean()->isFrozen(), TRUE ); + $book = $book->fresh(); + echo $book; + asrt( $book->pages, 'too many' ); + R::setAllowHybridMode( FALSE ); + R::freeze( FALSE ); + if ($this->currentlyActiveDriverID == 'mysql') { + R::exec('SET @@SESSION.sql_mode=\'\';'); + } + } + + /** + * Other exceptions must fall through... + */ + public function testHybridNonSQLException() + { + R::nuke(); + $toy = R::dispense('brokentoy'); + R::freeze( TRUE ); + R::setAllowHybridMode( TRUE ); + try { + R::store( $toy, TRUE ); + fail(); + } catch(\Exception $e) { + pass(); + } + R::setAllowHybridMode( FALSE ); + R::nuke(); + $toy = R::dispense('toy'); + R::freeze( TRUE ); + R::setAllowHybridMode( TRUE ); + try { + R::store( $toy, TRUE ); + pass(); + } catch(\Exception $e) { + fail(); + } + R::setAllowHybridMode( FALSE ); + } + + /** + * Test whether Hybrid mode is only activated + * for latest or 5.4 without novice and ensure + * maintaining backward compatibility by not setting + * Hybrid allowed for 5.3 and earlier. + */ + public function testVersions() + { + R::useFeatureSet('novice/latest'); + asrt( R::setAllowHybridMode( FALSE ), FALSE ); + R::useFeatureSet('latest'); + asrt( R::setAllowHybridMode( FALSE ), TRUE ); + R::useFeatureSet('novice/5.4'); + asrt( R::setAllowHybridMode( FALSE ), FALSE ); + R::useFeatureSet('5.4'); + asrt( R::setAllowHybridMode( FALSE ), TRUE ); + R::useFeatureSet('novice/5.3'); + asrt( R::setAllowHybridMode( FALSE ), FALSE ); + R::useFeatureSet('5.3'); + asrt( R::setAllowHybridMode( FALSE ), FALSE ); + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Indexes.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Indexes.php new file mode 100644 index 0000000..f90538f --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Indexes.php @@ -0,0 +1,136 @@ +ownPageList[] = $page; + R::store( $book ); + $indexes = getIndexes( 'page' ); + asrt( in_array( 'index_foreignkey_page_book', $indexes ), TRUE ); + } + + /** + * Tests indexes on parent beans. + * + * @return void + */ + public function testIndexCreationParentBean() + { + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $page->book = $book; + R::store( $page ); + $indexes = getIndexes( 'page' ); + asrt( in_array( 'index_foreignkey_page_book', $indexes ), TRUE ); + } + + /** + * Tests indexes on link tables. + * + * @return void + */ + public function testIndexCreationMany2Many() + { + R::nuke(); + $book = R::dispense( 'book' ); + $category = R::dispense( 'category' ); + $book->sharedCategoryList[] = $category; + R::store( $book ); + $indexes = getIndexes( 'book_category' ); + asrt( in_array( 'index_foreignkey_book_category_book', $indexes ), TRUE ); + asrt( in_array( 'index_foreignkey_book_category_category', $indexes ), TRUE ); + R::nuke(); + R::nuke(); + $book = R::dispense( 'book' ); + $category = R::dispense( 'category' ); + $category->sharedBookList[] = $book; + R::store( $category ); + $indexes = getIndexes( 'book_category' ); + asrt( in_array( 'index_foreignkey_book_category_book', $indexes ), TRUE ); + asrt( in_array( 'index_foreignkey_book_category_category', $indexes ), TRUE ); + } + + /** + * Tests indexes on aliases. + * + * @return void + */ + public function testIndexCreationAlias() + { + R::nuke(); + $book = R::dispense( 'book' ); + $author = R::dispense( 'author' ); + $book->coAuthor = $author; + R::store( $book ); + $indexes = getIndexes( 'book' ); + asrt( in_array( 'index_foreignkey_book_co_author', $indexes ), TRUE ); + R::nuke(); + $project = R::dispense( 'project' ); + $person = R::dispense( 'person' ); + $person->alias( 'teacher' )->ownProject[] = $project; + $person2 = R::dispense( 'person' ); + $person2->alias( 'student' )->ownProject[] = $project; + R::store( $person ); + $indexes = getIndexes( 'project' ); + asrt( in_array( 'index_foreignkey_project_teacher', $indexes ), TRUE ); + R::store( $person2 ); + $indexes = getIndexes( 'project' ); + asrt( in_array( 'index_foreignkey_project_student', $indexes ), TRUE ); + } + + /** + * Tests index fails. + * + * @return void + */ + public function testIndexCreationFail() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->author_id = '999'; + R::store( $book ); + $indexes = getIndexes( 'book' ); + //should just work fine + asrt( in_array( 'index_foreignkey_book_author', $indexes ), TRUE ); + //these should just pass, no indexes but no errors as well + R::getWriter()->addIndex( 'book', 'bla', 'nonexist' ); + pass(); + R::getWriter()->addIndex( 'book', '@#$', 'nonexist' ); + pass(); + R::getWriter()->addIndex( 'nonexist', 'bla', 'nonexist' ); + pass(); + $indexesAfter = getIndexes( 'book' ); + asrt( count( $indexesAfter ), count( $indexes ) ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue259.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue259.php new file mode 100644 index 0000000..7fb538d --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue259.php @@ -0,0 +1,68 @@ +desc = 'I am mother'; + R::store( $mother ); + $child = R::dispense( 'child' ); + $child->mother = $mother; + $child->desc = 'I am child'; + $id = R::store( $child ); + R::findOne( 'child', ' id = ?', array( $id ) ); + R::find( 'child', ' id = ? ', array( $id ) ); + R::load( 'child', $id ); + } +} +/** + * Mock Model. + */ +class Model_Mother extends SimpleModel +{ + public function open() + { + $bean = $this->bean; + asrt( $this->bean->desc, 'I am mother' ); + } +} +/** + * Mock Model. + */ +class Model_Child extends SimpleModel +{ + public function open() + { + $this->bean->mother; + asrt( $this->bean->desc, 'I am child' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue303.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue303.php new file mode 100644 index 0000000..655eb47 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue303.php @@ -0,0 +1,55 @@ +setAttr( 'invalid.property', 'value' ) ); + fail(); + } catch (RedException $e ) { + asrt( $e->getMessage(), 'Invalid Bean property: property invalid.property' ); + } + try { + R::store( R::dispense( 'invalidbean' )->setAttr( 'property', array() ) ); + fail(); + } catch (RedException $e ) { + asrt( $e->getMessage(), 'Invalid Bean value: property property' ); + } + try { + R::store( R::dispense( 'invalidbean' )->setAttr( 'property', new \stdClass ) ); + fail(); + } catch (RedException $e ) { + asrt( $e->getMessage(), 'Invalid Bean value: property property' ); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue408.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue408.php new file mode 100644 index 0000000..8e389dd --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue408.php @@ -0,0 +1,82 @@ +post = array( + 'first', + 'second' + ); + R::store( $feed ); + $rows = R::getAll('SELECT * FROM feed'); + asrt( $rows[0]['post'], '["first","second"]' ); + $feed = $feed->fresh(); + asrt( is_array( $feed->post ), TRUE ); + asrt( $feed->post[0], 'first' ); + asrt( $feed->post[1], 'second' ); + R::store( $feed ); + $rows = R::getAll('SELECT * FROM feed'); + asrt( $rows[0]['post'], '["first","second"]' ); + $feed = R::load( 'feed', $feed->id ); + $feed->post[] = 'third'; + R::store( $feed ); + $rows = R::getAll('SELECT * FROM feed'); + asrt( $rows[0]['post'], '["first","second","third"]' ); + $feed = $feed->fresh(); + asrt( is_array( $feed->post ), TRUE ); + asrt( $feed->post[0], 'first' ); + asrt( $feed->post[1], 'second' ); + asrt( $feed->post[2], 'third' ); + //now the catch: can we use export? + //PHP Fatal error: Call to a member function export() on a non-object + $feeds = R::exportAll( R::find( 'feed' ) ); + asrt( is_array( $feeds ), TRUE ); + $feed = reset( $feeds ); + asrt( $feed['post'][0], 'first' ); + asrt( $feed['post'][1], 'second' ); + asrt( $feed['post'][2], 'third' ); + //can we also dup()? + $feedOne = R::findOne( 'feed' ); + R::store( R::dup( $feedOne ) ); + asrt( R::count( 'feed' ), 2 ); + //can we delete? + R::trash( $feedOne ); + asrt( R::count( 'feed' ), 1 ); + $feedTwo = R::findOne( 'feed' ); + $feed = $feedTwo->export(); + asrt( $feed['post'][0], 'first' ); + asrt( $feed['post'][1], 'second' ); + asrt( $feed['post'][2], 'third' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue841.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue841.php new file mode 100644 index 0000000..2334df7 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue841.php @@ -0,0 +1,52 @@ +name = 'TAG_'.$i; + R::store($tag); + } + $record = R::dispense('record'); + $record->point = rand(-100,-1); + $record->sharedTagList[] = R::load('tag',2); + R::store($record); + asrt(count($record->sharedTagList),1); + $record = R::load('record',1); + $record->sharedTagList = array(); + R::store($record); + $record = R::load('record',1); + asrt(count($record->sharedTagList),0); + R::bindFunc( 'read', 'record.point', null ); + R::bindFunc( 'write', 'record.point', null ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue90.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue90.php new file mode 100644 index 0000000..f9e367c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Issue90.php @@ -0,0 +1,44 @@ +name = 'a'; + $f = R::dispense( 'bottle' ); + $s->ownBottle[] = $f; + R::store( $s ); + $s2 = R::dispense( 'box' ); + $s2->name = 'a'; + R::store( $s2 ); + R::trash( $s2 ); + pass(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Joins.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Joins.php new file mode 100644 index 0000000..18f13a0 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Joins.php @@ -0,0 +1,988 @@ +{$property}; + } + sort($list); + return implode(',', $list); + } + + /** + * Test whether we can use [via] notation in parsed joins. + * + * @return void + */ + public function testParsedJoinsWithVia() + { + $project1 = R::dispense('project'); + $project2 = R::dispense('project'); + $project1->title = 'project1'; + $project2->title = 'project2'; + $employee1 = R::dispense('employee'); + $employee2 = R::dispense('employee'); + $employee1->name = 'a'; + $employee2->name = 'b'; + $participant1 = R::dispense('participant'); + $participant2 = R::dispense('participant'); + $participant3 = R::dispense('participant'); + $participant1->employee = $employee1; + $participant2->employee = $employee1; + $participant3->employee = $employee2; + $participant1->project = $project1; + $participant2->project = $project2; + $participant3->project = $project2; + R::storeAll(array($participant1,$participant2,$participant3)); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? ', array('a')); + asrt(count($projects),2); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? AND project.title = ? ', array('a','project1')); + asrt(count($projects),1); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? AND project.title = ? ', array('a','project2')); + asrt(count($projects),1); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? ', array('b')); + asrt(count($projects),1); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? AND project.title = ? ', array('b','project1')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.employee[via:participant].name LIKE ? AND project.title = ? ', array('b','project2')); + asrt(count($projects),1); + /* shouldnt work if no [via:...] and no global aliases */ + $projects = R::find('project', ' @shared.participant.name LIKE ? ', array('a')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.participant.name LIKE ? AND project.title = ? ', array('a','project1')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.participant.name LIKE ? AND project.title = ? ', array('a','project2')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.participant.name LIKE ? ', array('b')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.participant.name LIKE ? AND project.title = ? ', array('b','project1')); + asrt(count($projects),0); + $projects = R::find('project', ' @shared.participant.name LIKE ? AND project.title = ? ', array('b','project2')); + asrt(count($projects),0); + R::aliases(array('work'=>'project')); + $company1 = R::dispense('company'); + $company2 = R::dispense('company'); + $company1->work = $project1; + $company2->work = $project2; + R::storeAll( array( $company1, $company2 ) ); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? ', array('a')); + asrt(count($companies),2); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? AND @joined.work.title = ? ', array('a','project1')); + asrt(count($companies),1); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? AND @joined.work.title = ? ', array('a','project2')); + asrt(count($companies),1); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? ', array('b')); + asrt(count($companies),1); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? AND @joined.work.title = ?', array('b','project1')); + asrt(count($companies),0); + $companies = R::find('company', ' @joined.work.shared.employee[via:participant].name LIKE ? AND @joined.work.title = ? ', array('b','project2')); + asrt(count($companies),1); + } + + /** + * Can we use joins with aliases? + * Can we perform bean->alias->ownlist and bean->fetchas->other + * like operation in SQL as well? + * + * In this test: + * Albert: Almanac, Study Guide + * Bob: Poetry Bundle, Study Guide (coauthor) + * + * Texts: + * Example -> in Study Guide (as source) + * Essay -> in Study Guide (as book) + * Poem -> in Poetry Bundle (as book) + * Poem -> also in Almanac (as magazine) + * + * @return void + */ + public function testParsedJoinsWithAliasing() + { + R::nuke(); + R::aliases(array()); + $albert = R::dispense('person'); + $bob = R::dispense('person'); + $guide = R::dispense('book'); + $almanac= R::dispense('book'); + $poetry = R::dispense('book'); + $albert->firstname = 'Albert'; + $bob->firstname = 'Bob'; + $guide->title = 'Study Guide'; + $almanac->title = 'Almanac'; + $poetry->title = 'Poems'; + $guide->author = $albert; + $guide->coauthor = $bob; + $almanac->author = $albert; + $poetry->author = $bob; + $poem = R::dispense('text'); + $essay = R::dispense('text'); + $example = R::dispense('text'); + $poem->content = 'poem'; + $essay->content = 'essay'; + $example->content = 'example'; + $poem->magazine = $almanac; + $poem->book = $poetry; + $essay->book = $guide; + $example->source = $guide; + $fiction = R::dispense('tag'); + $nonfiction = R::dispense('tag'); + $fiction->description = 'fiction'; + $nonfiction->description = 'non-fiction'; + $example->sharedTag[] = $nonfiction; + $poem->sharedTag[] = $fiction; + $essay->sharedTag = array( $nonfiction, $fiction ); + R::storeAll( array( $poem, $essay, $example ) ); + $books = R::find('book', ' @joined.author.firstname = ? ', array('Bob')); + asrt(count($books), 0); + $books = R::find('book', ' @joined.author.firstname = ? ', array('Albert')); + asrt(count($books), 0); + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Bob')); + asrt(count($books), 1); + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Albert')); + asrt(count($books), 2); + R::freeze( TRUE ); + try { $books = R::find('book', ' @joined.author.firstname = ? ', array('Bob')); fail(); } catch ( \Exception $e ) { pass(); } + try { $books = R::find('book', ' @joined.author.firstname = ? ', array('Albert')); fail(); } catch ( \Exception $e ) {pass();} + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Bob')); + asrt(count($books), 1); + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Albert')); + asrt(count($books), 2); + R::aliases(array('author' => 'person','coauthor' => 'person')); + $books = R::find('book', ' @joined.author.firstname = ? ', array('Bob')); + asrt(count($books), 1); + $books = R::find('book', ' @joined.author.firstname = ? ', array('Albert')); + asrt(count($books), 2); + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Bob')); + asrt(count($books), 1); + $books = R::find('book', ' @joined.person[as:author].firstname = ? ', array('Albert')); + asrt(count($books), 2); + R::freeze( FALSE ); + R::aliases(array()); + //If we want to find all the people who authored books like X + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + //yields Almanac and Poems + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%m%' ) ); + asrt( count($authors), 2 ); + //yields Almanac and Poems + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%m%' ) ); + asrt( count($authors), 2 ); + //yields nothing + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%x%' ) ); + asrt( count($authors), 0 ); + //yields nothing + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%x%' ) ); + asrt( count($authors), 0 ); + //If we want to find all the people who authored books starting with X + $authors = R::find( 'person', ' @own.book[coauthor].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + R::freeze( TRUE ); + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + //yields Almanac and Poems + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%m%' ) ); + asrt( count($authors), 2 ); + //yields Almanac and Poems + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%m%' ) ); + asrt( count($authors), 2 ); + asrt( $this->quickTestBeans( $authors, 'firstname' ), 'Albert,Bob'); + //yields nothing + $authors = R::find( 'person', ' @own.book[author].title LIKE ? ', array( '%x%' ) ); + asrt( count($authors), 0 ); + //yields nothing + $authors = R::find( 'person', ' @own.book[alias:author].title LIKE ? ', array( '%x%' ) ); + asrt( count($authors), 0 ); + $authors = R::find( 'person', ' @own.book[coauthor].title LIKE ? ', array( '%Study%' ) ); + asrt( count($authors), 1 ); + asrt( $this->quickTestBeans( $authors, 'firstname' ), 'Bob'); + R::freeze(FALSE); + $books = R::find( 'book', ' @joined.person[author/coauthor].firstname = ?', array( 'Bob' ) ); + asrt( count($books), 2 ); + asrt( $this->quickTestBeans( $books, 'title' ), 'Poems,Study Guide'); + //If we want all books where the author or the coauthor is named 'Bob': + $books = R::find( 'book', ' @joined.person[as:author/coauthor].firstname = ?', array( 'Bob' ) ); + asrt( count($books), 2 ); + asrt( $this->quickTestBeans( $books, 'title' ), 'Poems,Study Guide'); + //If we want all books where the author or the coauthor is named 'Albert': + $books = R::find( 'book', ' @joined.person[as:author/].firstname = ?', array( 'Albert' ) ); + asrt( count($books), 2 ); + asrt( $this->quickTestBeans( $books, 'title' ), 'Almanac,Study Guide'); + $books = R::find( 'book', ' @joined.person[as:coauthor/author].firstname = ?', array( 'Albert' ) ); + asrt( count($books), 2 ); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Study%' ) ); + asrt( count($authors), 2 ); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Poem%' ) ); + asrt( count($authors), 1 ); + asrt( $this->quickTestBeans( $authors, 'firstname' ), 'Bob'); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Alm%' ) ); + asrt( $this->quickTestBeans( $authors, 'firstname' ), 'Albert'); + asrt( count($authors), 1 ); + R::freeze(TRUE); + $books = R::find( 'book', ' @joined.person[author/coauthor].firstname = ?', array( 'Bob' ) ); + asrt( count($books), 2 ); + $books = R::find( 'book', ' @joined.person[as:author/coauthor].firstname = ?', array( 'Bob' ) ); + asrt( count($books), 2 ); + $books = R::find( 'book', ' @joined.person[as:author/coauthor].firstname = ?', array( 'Albert' ) ); + asrt( count($books), 2 ); + $books = R::find( 'book', ' @joined.person[as:author/coauthor].firstname = ?', array( 'Albert' ) ); + asrt( count($books), 2 ); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Study%' ) ); + asrt( count($authors), 2 ); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Poem%' ) ); + asrt( count($authors), 1 ); + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].title LIKE ?', array( '%Alm%' ) ); + asrt( count($authors), 1 ); + R::freeze(FALSE); + //2 people as author/coauthor have written a book (study guide) that contains the essay + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text.content = ?', array( 'essay' ) ); + asrt( count($authors), 2 ); + //2 people as author/coauthor have written a book as source that contains the example + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:source].content = ?', array( 'example' ) ); + asrt( count($authors), 2 ); + //1 person as author/coauthor has written a book as magazine/source that contains the poem + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:source/magazine].content = ?', array( 'poem' ) ); + asrt( count($authors), 1 ); + //If we include book, we get 2 authors because the poem is also in the poetry bundle (book) + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:book/source/magazine].content = ?', array( 'poem' ) ); + asrt( count($authors), 2 ); + R::freeze(TRUE); + //2 people as author/coauthor have written a book (study guide) that contains the essay + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text.content = ?', array( 'essay' ) ); + asrt( count($authors), 2 ); + //2 people as author/coauthor have written a book as source that contains the example + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:source].content = ?', array( 'example' ) ); + asrt( count($authors), 2 ); + //1 person as author/coauthor has written a book as magazine/source that contains the poem + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:source/magazine].content = ?', array( 'poem' ) ); + asrt( count($authors), 1 ); + //If we include book, we get 2 authors because the poem is also in the poetry bundle (book) + $authors = R::find( 'person', ' @own.book[alias:author/coauthor].own.text[alias:book/source/magazine].content = ?', array( 'poem' ) ); + asrt( count($authors), 2 ); + R::freeze(FALSE); + //Get all texts in books authored by Bob + $texts = R::find('text', ' @joined.book.joined.person[as:author].firstname = ? ',array('Bob')); + asrt( count($texts), 1 ); + //Get all texts in books authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book.joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 2 ); + //Get all texts in books as magazines or sources authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source].joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 1 ); + //Get all texts in books as magazines or sources or books authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source/book].joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 3 ); + //Get all texts in books as magazines or sources or books authored by Albert as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source/book].joined.person[as:author/coauthor].firstname = ? ',array('Albert')); + asrt( count($texts), 3 ); + R::freeze(TRUE); + //Get all texts in books authored by Bob + $texts = R::find('text', ' @joined.book.joined.person[as:author].firstname = ? ',array('Bob')); + asrt( count($texts), 1 ); + //Get all texts in books authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book.joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 2 ); + //Get all texts in books as magazines or sources authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source].joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 1 ); + //Get all texts in books as magazines or sources or books authored by Bob as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source/book].joined.person[as:author/coauthor].firstname = ? ',array('Bob')); + asrt( count($texts), 3 ); + //Get all texts in books as magazines or sources or books authored by Albert as author/coauthor + $texts = R::find('text', ' @joined.book[as:magazine/source/book].joined.person[as:author/coauthor].firstname = ? ',array('Albert')); + asrt( count($texts), 3 ); + R::freeze(FALSE); + //Get all texts in books as magazines or sources or books authored by Albert as author/coauthor that have been tagged + //as non-fiction, i.e. the example text and the essay. + $texts = R::find('text', ' + @joined.book[as:magazine/source/book].joined.person[as:author/coauthor].firstname = ? + AND + @shared.tag.description = ? + ',array('Albert', 'non-fiction')); + asrt( count($texts), 2 ); + //Get all texts in books or sources or books authored by Albert as author/coauthor that have been tagged + //as non-fiction, i.e. nothing, only the essay + $texts = R::find('text', ' + @joined.book[as:source/book].joined.person[as:author/coauthor].firstname = ? + AND + @shared.tag.description = ? + ',array('Albert', 'fiction')); + asrt( count($texts), 1 ); + } + + /** + * Non-join keywords starting with @ should be + * left untouched. + */ + public function testNonJoinsLeftUntouched() + { + $writer = R::getWriter(); + $types = array( 'book', 'cafe', 'bean' ); + $sqls = array( + '@version', + '@oined.satire', + '@oined.satire.laugh', + '@hared.lookalike.title', + '@powned.by.a.hacker', + 'nothing here!', + 'shared.person.name', + 'owned.thing.name', + 'joined.thing.name', + 'shared.person.shared.tag.name', + 'owned.thing.shared.tag.name', + 'joined.thing.shared.tag.name', + 'shared.person.joined.tag.name', + 'owned.thing.joined.tag.name', + 'joined.thing.joined.tag.name', + 'shared.person.owned.tag.name', + 'owned.thing.owned.tag.name', + 'joined.thing.owned.tag.name' + ); + $ctes = array( TRUE, FALSE ); + foreach($types as $type) { + foreach($sqls as $sql) { + foreach($ctes as $cte) { + $same = $writer->parseJoin( $type, $sql, $cte ); + asrt( trim($same), trim($sql) ); + } + } + } + } + + /** + * Can we join multiple tables with the same parent? + * + * @return void + */ + public function testMultipleJoinsSameParent() + { + R::nuke(); + $TheHagueNorthEnd = R::dispense(array( + '_type' => 'painting', + 'title' => 'Northend, The Hague', + 'artist' => array( + '_type' => 'artist', + 'name' => 'Isaac Israels', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'Haagse School' ) + ) + ), + 'ownDetail' => array( + array( + '_type' => 'detail', + 'description' => 'awnings', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'object' ) + ) + ), + array( + '_type' => 'detail', + 'description' => 'sunlight', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'daytime' ) + ) + ), + ) + )); + $Nighthawks = R::dispense(array( + '_type' => 'painting', + 'title' => 'Nighthawks', + 'artist' => array( + '_type' => 'artist', + 'name' => 'Hopper', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'American Realism' ) + ) + ), + 'ownDetail' => array( + array( + '_type' => 'detail', + 'description' => 'percolator', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'object' ) + ) + ), + array( + '_type' => 'detail', + 'description' => 'cigarette', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'object' ) + ) + ), + array( + '_type' => 'detail', + 'description' => 'night', + 'sharedCategory' => array( + array( '_type' => 'category', 'label' => 'nocturnal' ) + ) + ) + ) + )); + R::storeAll( array( $Nighthawks, $TheHagueNorthEnd ) ); + $paintings = R::find('painting', ' + @joined.artist.shared.category.label = ? + OR + @own.detail.shared.category.label= ? + ', array('American Realism', 'nocturnal')); + asrt(count($paintings),1); + $painting = reset($paintings); + asrt($painting->title, 'Nighthawks'); + $paintings = R::find('painting', ' + @joined.artist.shared.category.label = ? + OR + @own.detail.shared.category.label= ? + ', array('Haagse School', 'daytime')); + asrt(count($paintings),1); + $painting = reset($paintings); + asrt($painting->title, 'Northend, The Hague'); + $paintings = R::find('painting', ' + @own.detail.shared.category.label= ? + ', array('object')); + asrt(count($paintings),2); + $paintings = R::find('painting', ' + @own.detail.shared.category.label= ? + AND @own.detail.description= ? + ', array('object', 'percolator')); + asrt(count($paintings),1); + $painting = reset($paintings); + asrt($painting->title, 'Nighthawks'); + $paintings = R::find('painting', ' + @own.detail.shared.category.label= ? + AND @own.detail.description= ? + ', array('object', 'ashtray')); + asrt(count($paintings),0); + } + + /** + * Complex tests for the parsed joins featured. + * + * @return void + */ + public function testComplexParsedJoins() + { + R::nuke(); + $other = R::dispense('book'); + R::store( $other ); + $book = R::dispense('book'); + $page = R::dispense('page'); + $paragraph = R::dispense('paragraph'); + $paragraph->title = 'hello'; + $book->title = 'book'; + $book->ownPage[] = $page; + $page->ownParagraph[] = $paragraph; + $figure = R::dispense('figure'); + $chart = R::dispense('chart'); + $chart->title = 'results'; + $page->ownFigure[] = $figure; + $figure->ownChart[] = $chart; + R::store($book); + $books = R::find('book',' @own.page.own.paragraph.title = ? OR @own.page.own.figure.own.chart.title = ?', array('hello','results')); + asrt(count($books),1); + $book = reset($books); + asrt($book->title, 'book'); + R::nuke(); + R::aliases(array( 'author' => 'person' )); + $book = R::dispense('book'); + $author = R::dispense('person'); + $detail = R::dispense('detail'); + $shop = R::dispense('shop'); + $shop->name = 'Books4you'; + $shop2 = R::dispense('shop'); + $shop2->name = 'Readers Delight'; + $author->name = 'Albert'; + $detail->title = 'Book by Albert'; + $book->ownDetailList[] = $detail; + $book->author = $author; + $shop->sharedBookList[] = $book; + $book2 = R::dispense('book'); + $author2 = R::dispense('person'); + $detail2 = R::dispense('detail'); + $author2->name = 'Bert'; + $detail2->title = 'Book by Bert'; + $book2->ownDetailList[] = $detail2; + $book2->author = $author2; + $shop->sharedBookList[] = $book2; + $shop2->sharedBookList[] = $book2; + R::store($shop); + R::store($shop2); + //joined+own + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? ', array('Albert', 'Book by Albert')); + asrt(count($books),1); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? ', array('%ert%', '%Book by%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? ', array('%ert%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? ', array('%ert%', 'Old Bookshop')); + asrt(count($books),0); + //joined+shared + $books = R::find('book', ' @joined.author.name LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Books%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Read%')); + asrt(count($books),1); + $books = R::find('book', ' @joined.author.name LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', 'Old Bookshop')); + asrt(count($books),0); + //own+shared + $books = R::find('book', ' @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Read%')); + asrt(count($books),1); + $books = R::find('book', ' @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Book%')); + asrt(count($books),2); + $books = R::find('book', ' @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', 'Old Bookshop')); + asrt(count($books),0); + //joined+own+shared + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Book by%', 'Books%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Book by%', 'Read%')); + asrt(count($books),1); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Book by%', 'Old')); + asrt(count($books),0); + $books = R::find('book', ' @joined.author.name LIKE ? AND @own.detail.title LIKE ? AND @shared.shop.name LIKE ? ', array('%ert%', '%Book by%', '%')); + asrt(count($books),2); + //joined+joined + $page = R::dispense('page'); + $page->text = 'Lorem Ipsum'; + $category = R::dispense('category'); + $category->name = 'biography'; + $publisher = R::dispense('publisher'); + $publisher->name = 'Good Books'; + $book->publisher = $publisher; + $book->ownPageList[] = $page; + $category->sharedBookList[] = $book; + $page2 = R::dispense('page'); + $page2->text = 'Blah Blah'; + $category2 = R::dispense('category'); + $category2->name = 'fiction'; + $publisher2 = R::dispense('publisher'); + $publisher2->name = 'Gutenberg'; + $book2->publisher = $publisher2; + $book2->ownPageList = array($page2); + $category2->sharedBookList[] = $book2; + R::store($category); + R::store($category2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @joined.publisher.name LIKE ?', array('%ert%', 'Good Books')); + asrt(count($books),1); + $books = R::find('book', ' @joined.author.name LIKE ? AND @joined.publisher.name LIKE ?', array('%ert%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @joined.author.name LIKE ? AND @joined.publisher.name LIKE ?', array('Unknown', '%')); + asrt(count($books),0); + $books = R::find('book', ' @joined.author.name LIKE ? AND @joined.publisher.name LIKE ?', array('%', '%')); + asrt(count($books),2); + //shared+shared + $books = R::find('book', ' @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('Reader%', 'fiction')); + asrt(count($books),1); + $books = R::find('book', ' @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('Book%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('Book%', 'biography')); + asrt(count($books),1); + $books = R::find('book', ' @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('Old Bookshop', '%')); + asrt(count($books),0); + $books = R::find('book', ' @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('%', 'horror')); + asrt(count($books),0); + //own+own + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('Book%', 'Blah%')); + asrt(count($books),1); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('Book%', 'Lorem%')); + asrt(count($books),1); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('Book%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('%', '%')); + asrt(count($books),2); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('%', 'Nah')); + asrt(count($books),0); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ?', array('Nah', '%')); + asrt(count($books),0); + //joined+joined+shared+shared+own+own + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ? + AND @joined.publisher.name LIKE ? AND @joined.author.name LIKE ? + AND @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('Book%', 'Lorem%','Good%','Albert','Books4%','bio%')); + asrt(count($books),1); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ? + AND @joined.publisher.name LIKE ? AND @joined.author.name LIKE ? + AND @shared.shop.name LIKE ? AND @shared.category.name LIKE ?', array('%', '%','%','%','%','%')); + asrt(count($books),2); + //in order clause + $book = R::findOne('book', ' ORDER BY @shared.category.name ASC LIMIT 1'); + asrt($book->author->name, 'Albert'); + $book = R::findOne('book', ' ORDER BY @shared.category.name DESC LIMIT 1'); + asrt($book->author->name, 'Bert'); + $book = R::findOne('book', ' ORDER BY @own.detail.title ASC LIMIT 1'); + asrt($book->author->name, 'Albert'); + $book = R::findOne('book', ' ORDER BY @own.detail.title DESC LIMIT 1'); + asrt($book->author->name, 'Bert'); + //order+criteria + $book = R::findOne('book', ' @joined.publisher.name LIKE ? ORDER BY @shared.category.name ASC LIMIT 1', array('%')); + asrt($book->author->name, 'Albert'); + $book = R::findOne('book', ' @joined.publisher.name LIKE ? ORDER BY @shared.category.name DESC LIMIT 1', array('%')); + asrt($book->author->name, 'Bert'); + $book = R::findOne('book', ' @joined.publisher.name LIKE ? ORDER BY @own.detail.title ASC LIMIT 1', array('%')); + asrt($book->author->name, 'Albert'); + $book = R::findOne('book', ' @joined.publisher.name LIKE ? ORDER BY @own.detail.title DESC LIMIT 1', array('%')); + asrt($book->author->name, 'Bert'); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ? + AND @joined.publisher.name LIKE ? AND @joined.author.name LIKE ? + AND @shared.shop.name LIKE ? AND @shared.category.name LIKE ? + ORDER BY @own.detail.title ASC + ', array('%', '%','%','%','%','%')); + asrt(count($books),2); + $first = reset($books); + $last = end($books); + asrt($first->author->name, 'Albert'); + asrt($last->author->name, 'Bert'); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ? + AND @joined.publisher.name LIKE ? AND @joined.author.name LIKE ? + AND @shared.shop.name LIKE ? AND @shared.category.name LIKE ? + ORDER BY + @shared.shop.name DESC, + @own.detail.title ASC + ', array('%', '%','%','%','%','%')); + asrt(count($books),2); + $first = reset($books); + $last = end($books); + asrt($first->author->name, 'Bert'); + asrt($last->author->name, 'Albert'); + $books = R::find('book', ' @own.detail.title LIKE ? AND @own.page.text LIKE ? + AND @joined.publisher.name LIKE ? AND @joined.author.name LIKE ? + AND @shared.shop.name LIKE ? AND @shared.category.name LIKE ? + ORDER BY + @joined.publisher.name ASC, + @shared.shop.name DESC, + @own.detail.title ASC + ', array('%', '%','%','%','%','%')); + asrt(count($books),2); + $first = reset($books); + $last = end($books); + asrt($first->author->name, 'Albert'); + asrt($last->author->name, 'Bert'); + } + + /** + * Tests new parsed joins. + * + * @return void + */ + public function testParsedJoins() + { + $person = R::dispense('person'); + $person->name = 'Albert'; + $book = R::dispense('book'); + $book->title = 'Book by Albert.'; + $book->author = $person; + $person->movement = R::dispense(array('_type'=>'movement','name'=>'romanticism')); + R::store( $book ); + $albert = $person; + $person = R::dispense('person'); + $person->name = 'Bert'; + $book = R::dispense('book'); + $book->title = 'Book by Bert.'; + $book->author = $person; + $bert = $person; + $person->movement = R::dispense(array('_type'=>'movement','name'=>'gothic')); + R::store( $book ); + R::aliases(array( 'author' => 'person' )); + $books = R::find( 'book', ' @joined.author.name LIKE ? ', array('A%')); + asrt(count($books), 1); + $book = reset($books); + asrt($book->title,'Book by Albert.'); + asrt($book->fetchAs('person')->author->name,'Albert'); + $people = R::find( 'person', ' @joined.movement.name = ? ', array('romanticism')); // This works (aliases not involved) + asrt(count($people), 1); + $people = R::find( 'person', ' @joined.movement.name = ? ', array('gothic')); // This works (aliases not involved) + asrt(count($people), 1); + $people = R::find( 'person', ' @joined.movement.name = ? ', array('popscience')); // This works (aliases not involved) + asrt(count($people), 0); + $movements = R::find( 'movement', ' @own.author.name LIKE ? ', array( 'A%' )); // This works + asrt(count($movements), 1); + $movement = reset($movements); + asrt($movement->name, 'romanticism'); + R::freeze(TRUE); + try{ + R::find( 'person', ' @own.book.title LIKE ? ', array( 'A%' )); // This doesn't work as RedBean cannot guess which column it should bind the person to in the book table. + fail(); + } catch(\Exception $e) { + pass(); + } + R::freeze(FALSE); + $group = R::dispense('group'); + $group->name = 'a'; + $group->sharedAuthorList = array($bert, $albert); + R::store($group); + $group = R::dispense('group'); + $group->name = 'b'; + $group->sharedAuthorList = array($bert); + R::store($group); + $groups = R::find( 'group', ' @shared.author.name = ? ', array( 'Bert' )); // This works + asrt(count($groups),2); + R::tag($albert, array('male','writer')); + R::tag($bert, array('male')); + $people = R::find( 'person', ' @shared.tag.title = ? ', array( 'writer' )); // This works (aliases not involved) + asrt(count($people),1); + R::tag($albert, array('male','writer')); + R::tag($bert, array('male')); + $people = R::find( 'person', ' @shared.tag.title = ? ', array( 'male' )); // This works (aliases not involved) + asrt(count($people),2); + $user1 = R::dispense('user'); + $user1->name = 'user1'; + $user2 = R::dispense('user'); + $user2->name = 'user2'; + $status = R::dispense('status'); + $status->whitelist = TRUE; + $user1->status = $status; + $status2 = R::dispense('status'); + $status2->whitelist = FALSE; + $user2->status = $status2; + R::storeAll(array($user1,$user2)); + $whitelisted = R::find( 'user', ' @joined.status.whitelist = ? ', array( 1 ) ); + asrt(count($whitelisted), 1); + $user = reset($whitelisted); + asrt($user->name, 'user1'); + $whitelisted = R::find( 'user', ' @joined.status.whitelist = ? ', array( 0 ) ); + R::debug(0); + asrt(count($whitelisted), 1); + $user = reset($whitelisted); + asrt($user->name, 'user2'); + } + + /** + * Tests joins with ownCount(). + * + * @return void + */ + public function testJoinsInCount() + { + R::nuke(); + $author = R::dispense( 'author' ); + $book = R::dispense( 'book' ); + $info = R::dispense( 'info' ); + $info->title = 'x'; + $author->xownBookList[] = $book; + $book->info = $info; + $book = R::dispense( 'book' ); + $info = R::dispense( 'info' ); + $info->title = 'y'; + $author->xownBookList[] = $book; + $book->info = $info; + R::store( $author ); + $author = $author->fresh(); + $books = $author->withCondition(' @joined.info.title != ? ', array('x'))->countOwn('book'); + asrt($books, 1); + $books = $author->withCondition(' @joined.info.title != ? ', array('y'))->countOwn('book'); + asrt($books, 1); + $books = $author->withCondition(' @joined.info.title IN (?,?) ', array('x','y'))->countOwn('book'); + asrt($books, 2); + } + + /** + * Test Joins. + * + * @return void + */ + public function testJoins() + { + R::nuke(); + list($a1, $a2, $a3) = R::dispense('area', 3); + list($p1, $p2) = R::dispense('person', 2); + list($v1, $v2, $v3, $v4) = R::dispense('visit', 4); + $a1->name = 'Belgium'; + $a2->name = 'Arabia'; + $a3->name = 'France'; + $v1->person = $p1; + $v2->person = $p1; + $v3->person = $p2; + $v4->person = $p2; + $v1->area = $a3; + $v2->area = $a2; + $v3->area = $a2; + $v4->area = $a1; + $v1->label = 'v1 to France'; + $v2->label = 'v2 to Arabia'; + $v3->label = 'v3 to Arabia'; + $v4->label = 'v4 to Belgium'; + R::storeAll( array($v1,$v2,$v3,$v4) ); + $visits = $p1->ownVisit; + asrt( is_array( $visits ), TRUE ); + asrt( count( $visits ), 2 ); + $names = array(); + foreach( $visits as $visit ) { + asrt( isset( $visit->label ), TRUE ); + asrt( isset( $visit->name ), FALSE ); + asrt( isset( $visit->visit_id ), FALSE ); + $names[] = $visit->label; + } + $labelList = implode( ',', $names ); + asrt( $labelList, 'v1 to France,v2 to Arabia' ); + $visits = $p1 + ->with('ORDER BY @joined.area.name ASC')->ownVisit; + asrt( is_array( $visits ), TRUE ); + asrt( count( $visits ), 2 ); + $names = array(); + foreach( $visits as $visit ) { + asrt( isset( $visit->label ), TRUE ); + asrt( isset( $visit->name ), FALSE ); + asrt( isset( $visit->visit_id ), FALSE ); + $names[] = $visit->label; + } + $labelList = implode( ',', $names ); + asrt( $labelList, 'v2 to Arabia,v1 to France' ); + } + + /** + * Helper for the next test. + * + * @param array $books the books we are going to check + * @param string $numberList the numbers that are expected + * + * @return void + */ + private function checkBookNumbers( $books, $numberList ) + { + $numbers = explode( ',', $numberList ); + asrt( is_array( $books ), TRUE ); + asrt( count( $books ), count( $numbers ) ); + $bookNumbers = ''; + $bookNumberArray = array(); + foreach( $books as $book ) { + asrt( isset( $book->num ), TRUE ); + asrt( isset( $book->title), FALSE ); + $bookNumberArray[] = $book->num; + } + $bookNumbers = implode( ',', $bookNumberArray); + asrt( $bookNumbers, $numberList ); + } + + /** + * Tests the more complicated scenarios for + * with-joins. + * + * @return void + */ + private function testComplexCombinationsJoins() + { + $author = R::dispense( 'author' ); + $books = R::dispense( 'book', 4 ); + $books[0]->num = 0; + $books[1]->num = 1; + $books[2]->num = 2; + $books[3]->num = 3; + $books[0]->info = R::dispense('info')->setAttr('title', 'Learning PHP'); + $books[1]->info = R::dispense('info')->setAttr('title', 'Learning PHP and JavaScript'); + $books[2]->info = R::dispense('info')->setAttr('title', 'Learning Cobol'); + $books[3]->info = R::dispense('info')->setAttr('title','Gardening for Beginners'); + $books[0]->category = R::dispense('category')->setAttr('title', 'computers'); + $books[1]->category = R::dispense('category')->setAttr('title', 'computers'); + $books[2]->category = R::dispense('category')->setAttr('title', 'computers'); + $books[3]->category = R::dispense('category')->setAttr('title','gardening'); + $author->ownBookList = $books; + R::store($author); + //Base test... + $books = $author->ownBookList; + $this->checkBookNumbers( $books, '0,1,2,3' ); + //Just a basic Join... + $books = $author->withCondition(' @joined.info.title LIKE ? ORDER BY book.num ASC ', array( '%PHP%' ) )->ownBookList; + $this->checkBookNumbers( $books, '0,1' ); + //Mix Join and criteria + $books = $author->withCondition(' @joined.info.title LIKE ? AND num > 0 ORDER BY book.num ASC ', array( '%PHP%' ) )->ownBookList; + $this->checkBookNumbers( $books, '1' ); + //Basic join + $books = $author->withCondition(' @joined.info.title LIKE ? ORDER BY book.num ASC', array( '%ing%' ) )->ownBookList; + $this->checkBookNumbers( $books, '0,1,2,3' ); + //Two joins + $books = $author->withCondition(' @joined.info.title LIKE ? AND @joined.category.title = ? ORDER BY book.num ASC', array( '%ing%', 'computers' ) )->ownBookList; + $this->checkBookNumbers( $books, '0,1,2' ); + //Join the same type twice... and order + $books = $author->withCondition(' @joined.info.title LIKE ? AND @joined.category.title = ? ORDER BY @joined.info.title ASC ', array( '%ing%', 'computers' ) )->ownBookList; + $this->checkBookNumbers( $books, '2,0,1' ); + //Join the same type twice + $books = $author->withCondition(' @joined.info.title LIKE ? AND @joined.info.title LIKE ? ORDER BY book.num ASC', array( '%ing%', '%Learn%' ) )->ownBookList; + $this->checkBookNumbers( $books, '0,1,2' ); + //Join the same type 3 times and order + $books = $author->withCondition(' @joined.info.title LIKE ? AND @joined.info.title LIKE ? ORDER BY @joined.info.title DESC', array( '%ing%', '%Learn%' ) )->ownBookList; + $this->checkBookNumbers( $books, '1,0,2' ); + //Join the same type 3 times and order and limit + $books = $author->withCondition(' @joined.info.title LIKE ? AND @joined.info.title LIKE ? ORDER BY @joined.info.title DESC LIMIT 1', array( '%ing%', '%Learn%' ) )->ownBookList; + $this->checkBookNumbers( $books, '1' ); + //Other combinations I can think of... + $books = $author->withCondition(' @joined.category.title LIKE ? ORDER BY @joined.info.title DESC', array( '%ing%' ) )->ownBookList; + $this->checkBookNumbers( $books, '3' ); + $books = $author->withCondition(' @joined.category.title LIKE ? AND num < 4 ORDER BY @joined.info.title DESC', array( '%ing%' ) )->ownBookList; + $this->checkBookNumbers( $books, '3' ); + //multiple ordering + $books = $author->with(' ORDER BY @joined.category.title ASC, @joined.info.title ASC' )->ownBookList; + $this->checkBookNumbers( $books, '2,0,1,3' ); + $books = $author->with(' ORDER BY @joined.category.title DESC, @joined.info.title ASC' )->ownBookList; + $this->checkBookNumbers( $books, '3,2,0,1' ); + $books = $author->with(' ORDER BY @joined.category.title DESC, @joined.info.title ASC LIMIT 2' )->ownBookList; + $this->checkBookNumbers( $books, '3,2' ); + } + + /** + * Tests the more complicated scenarios for + * with-joins. + * + * @return void + */ + public function testComplexInFrozenMode() + { + R::freeze( FALSE ); + $this->testComplexCombinationsJoins(); + R::freeze( TRUE ); + $this->testComplexCombinationsJoins(); + R::freeze( FALSE ); + } + + /** + * Tests R::setNarrowFieldMode() and + * OODBBean::ignoreJoinFeature(). + */ + public function testSystemWideSettingsForJoins() + { + R::nuke(); + $author = R::dispense( 'author' ); + $book = R::dispense( 'book' ); + $info = R::dispense( 'info' ); + $info->title = 'x'; + $author->xownBookList[] = $book; + $book->info = $info; + R::store( $author ); + $author = $author->fresh(); + $books = $author->withCondition(' @joined.info.title != ? ', array('y1') )->xownBookList; + $firstBook = reset( $books ); + asrt( isset( $firstBook->title ), FALSE ); + R::setNarrowFieldMode( FALSE ); + $author = $author->fresh(); + $books = $author->withCondition(' @joined.info.title != ? ', array('y2') )->xownBookList; + $firstBook = reset( $books ); + asrt( isset( $firstBook->title ), TRUE ); + R::setNarrowFieldMode( TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Keywords.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Keywords.php new file mode 100644 index 0000000..1281fcd --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Keywords.php @@ -0,0 +1,71 @@ +$k = $k; + $id = R::store( $bean ); + $bean = R::load( $k, $id ); + $bean2 = R::dispense( 'other' ); + $bean2->name = $k; + $bean->bean = $bean2; + $bean->ownBean[] = $bean2; + $bean->sharedBean[] = $bean2; + $id = R::store( $bean ); + R::trash( $bean ); + pass(); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Largenum.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Largenum.php new file mode 100644 index 0000000..66d3fbf --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Largenum.php @@ -0,0 +1,42 @@ +name = 'big number'; + R::store( $number ); + //This should not cause an error... (some people use LIMIT 0, HUGE to simulate OFFSET on MYSQL). + $beans = R::findAll( 'number', ' LIMIT ? ', array( PHP_INT_MAX ) ); + asrt( is_array( $beans ), TRUE ); + asrt( count( $beans ), 1 ); + pass(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Logging.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Logging.php new file mode 100644 index 0000000..b5d40e5 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Logging.php @@ -0,0 +1,256 @@ + 0 ), TRUE ); + asrt( ( R::getLogger() instanceof Logger ), TRUE ); + R::stopLogging(); + R::store( R::dispense( 'book' ) ); + $logs = R::getLogs(); + asrt( ( count( $logs ) === 0 ), TRUE ); + } + + /** + * Test for Issue #751 (Update Logger to accept parameter typed bindings): + * While debugging some of our queries, we noticed + * the logger would often display + * 'Array' as value for the bindings, + * even when the SQL query seemed to work correctly. + * Debugging this, it appeared the debug logger did + * not support the new parameter type bindings added in 5.3. + * This merge request adds support for + * the PDO::PARAM_INT and PDO::PARAM_STR + * to the Debug logger, as well as a visible support + * for the RPDO flagUseStringOnlyBinding flag. + * + * @return void + */ + public function testIssue751() + { + R::nuke(); + $debugger = new Debug; + $database = R::getDatabaseAdapter()->getDatabase(); + $database->setLogger( $debugger ); + asrt( $database->getLogger(), $debugger ); + $database->setEnableLogging( TRUE ); + $debugger->setMode( RDefault::C_LOGGER_ARRAY ); + /* debug logger with nostringonlybinding should have unquoted ints */ + R::store( R::dispense( 'book' ) ); + R::getAll( 'SELECT * FROM book WHERE id < ?', array( array( 999, \PDO::PARAM_INT ) ) ); + asrt( count( $debugger->grep('999') ), 1 ); + asrt( count( $debugger->grep('\'999\'') ), 0 ); + asrt( count( $debugger->grep('rray') ), 0 ); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->clear(); + R::getAll( 'SELECT * FROM book WHERE id < ?', array( array( 999, \PDO::PARAM_STR ) ) ); + /* ...but quoted strings */ + asrt( count( $debugger->grep('\'999\'') ), 1 ); + asrt( count( $debugger->grep('rray') ), 0 ); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->clear(); + /* even if PARAM INT if stringonlybinding then override */ + $debugger->setUseStringOnlyBinding( TRUE ); + R::getAll( 'SELECT * FROM book WHERE id < ?', array( array( 999, \PDO::PARAM_INT ) ) ); + asrt( count( $debugger->grep('\'999\'') ), 1 ); + asrt( count( $debugger->grep('rray') ), 0 ); + /* if no type and stringonlybinding always quote */ + $debugger->clear(); + R::getAll( 'SELECT * FROM book WHERE id < ?', array( 999 ) ); + asrt( count( $debugger->grep('\'999\'') ), 1 ); + asrt( count( $debugger->grep('rray') ), 0 ); + /* a more closer inspection */ + /* log implicit INT param without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( 123 ) ); + asrt( count( $debugger->grep('123') ), 1 ); + asrt( count( $debugger->grep('\'123\'') ), 0 ); + /* log implicit STR param without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( 'abc' ) ); + asrt( count( $debugger->grep('\'abc\'') ), 1 ); + /* log NULL param without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( NULL ) ); + asrt( count( $debugger->grep('NULL') ), 1 ); + asrt( count( $debugger->grep('\'NULL\'') ), 0 ); + /* log explicit INT param without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( 123, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('123') ), 1 ); + asrt( count( $debugger->grep('\'123\'') ), 0 ); + /* log explicit STR param without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( 'abc', \PDO::PARAM_STR ) ); + asrt( count( $debugger->grep('\'abc\'') ), 1 ); + /* log NULL with explicit param type without StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( NULL, \PDO::PARAM_STR ) ); + asrt( count( $debugger->grep('NULL') ), 1 ); + asrt( count( $debugger->grep('\'NULL\'') ), 0 ); + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( NULL, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('NULL') ), 1 ); + asrt( count( $debugger->grep('\'NULL\'') ), 0 ); + /* log implicit INT param with StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( TRUE ); + $debugger->log(' Hello ? ', array( 123 ) ); + asrt( count( $debugger->grep('\'123\'') ), 1 ); + /* log implicit STR param with StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( TRUE ); + $debugger->log(' Hello ? ', array( 'abc' ) ); + asrt( count( $debugger->grep('\'abc\'') ), 1 ); + /* log NULL param with StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( TRUE ); + $debugger->log(' Hello ? ', array( NULL ) ); + asrt( count( $debugger->grep('\'NULL\'') ), 1 ); + /* log explicit INT param with StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( TRUE ); + $debugger->log(' Hello ? ', array( 123, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('\'123\'') ), 1 ); + /* log explicit STR param with StringOnly */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( TRUE ); + $debugger->log(' Hello ? ', array( 'abc', \PDO::PARAM_STR ) ); + asrt( count( $debugger->grep('\'abc\'') ), 1 ); + /* log NULL with explicit param type with StringOnly - remains just NULL */ + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( NULL, \PDO::PARAM_STR ) ); + asrt( count( $debugger->grep('NULL') ), 1 ); + asrt( count( $debugger->grep('\'NULL\'') ), 0 ); + $debugger->clear(); + $debugger->setUseStringOnlyBinding( FALSE ); + $debugger->log(' Hello ? ', array( NULL, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('NULL') ), 1 ); + asrt( count( $debugger->grep('\'NULL\'') ), 0 ); + $debugger->setUseStringOnlyBinding( FALSE ); + /* Does stringonly mode switch along with Database mode ? */ + $database->setUseStringOnlyBinding( TRUE ); + $debugger->clear(); + $debugger->log(' Hello ? ', array( 123, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('\'123\'') ), 1 ); + $database->setUseStringOnlyBinding( FALSE ); + $debugger->clear(); + $debugger->log(' Hello ? ', array( 123, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('\'123\'') ), 0 ); + asrt( count( $debugger->grep('123') ), 1 ); + $database->setUseStringOnlyBinding( TRUE ); + $debugger->clear(); + $debugger->log(' Hello ? ', array( 123, \PDO::PARAM_INT ) ); + asrt( count( $debugger->grep('\'123\'') ), 1 ); + $database->setUseStringOnlyBinding( FALSE ); + $debugger->setUseStringOnlyBinding( FALSE ); + } + + /** + * Can we manually set a logger and enable logging? + * + * @return void + */ + public function testCanSetLogger() + { + R::nuke(); + R::store( R::dispense( 'bean' ) ); + $logger = new RDefault; + $logger->setMode( RDefault::C_LOGGER_ARRAY ); + $database = R::getDatabaseAdapter()->getDatabase(); + $database->setLogger( $logger ); + asrt( $database->getLogger(), $logger ); + $database->setEnableLogging( FALSE ); + $logs = $logger->getLogs(); + asrt( is_array( $logs ), TRUE ); + asrt( count( $logs ), 0 ); + $database->setEnableLogging( TRUE ); + $logs = $logger->getLogs(); + asrt( is_array( $logs ), TRUE ); + asrt( count( $logs ), 0 ); + R::findOne( 'bean' ); //writes 3 log entries + $logs = $logger->getLogs(); + asrt( is_array( $logs ), TRUE ); + asrt( count( $logs ), 3 ); + } + + /** + * Test query counter. + * + * @return void + */ + public function testQueryCount() + { + R::nuke(); + R::store( R::dispense( 'bean' ) ); + R::resetQueryCount(); + asrt( R::getQueryCount(), 0 ); + R::findOne( 'bean' ); + asrt( R::getQueryCount(), 1 ); + R::resetQueryCount(); + asrt( R::getQueryCount(), 0 ); + R::findOne( 'bean' ); + R::findOne( 'bean' ); + R::findOne( 'bean' ); + asrt( R::getQueryCount(), 0 ); + R::store( R::dispense( 'bean2' ) ); + R::resetQueryCount(); + R::findOne( 'bean' ); + R::findOne( 'bean2' ); + asrt( R::getQueryCount(), 2 ); + R::resetQueryCount(); + R::findOne( 'bean', ' id < 100' ); + R::findOne( 'bean', ' id < 101' ); + R::findOne( 'bean', ' id < 102' ); + R::findOne( 'bean', ' id < 103' ); + asrt( R::getQueryCount(), 4 ); + R::findOne( 'bean', ' id < 100' ); + R::findOne( 'bean', ' id < 101' ); + R::findOne( 'bean', ' id < 102' ); + R::findOne( 'bean', ' id < 103' ); + asrt( R::getQueryCount(), 4 ); + R::findOne( 'bean', ' id < 104' ); + asrt( R::getQueryCount(), 5 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Misc.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Misc.php new file mode 100644 index 0000000..22d2f9b --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Misc.php @@ -0,0 +1,653 @@ +setDDLTemplate( 'createTable', 'joke', $writer->getDDLTemplate( 'createTable', 'joke' ) . ' /* haha */ ' ); + $writer->setDDLTemplate( 'addColumn', 'joke', $writer->getDDLTemplate( 'addColumn', 'joke' ) . ' /* hihi */ ' ); + $writer->setDDLTemplate( 'widenColumn', 'joke', $writer->getDDLTemplate( 'widenColumn', 'joke' ) . ' /* hoho */ ' ); + $joke = R::dispense('joke'); + R::store( $joke ); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( 'haha' ); + asrt( count( $logs ), 1 ); + $joke->punchline = 1; + R::store( $joke ); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( 'hihi' ); + asrt( count( $logs ), 1 ); + $joke->punchline = '...'; + R::store( $joke ); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( 'hoho' ); + asrt( count( $logs ), 1 ); + R::debug( FALSE ); + } + + /** + * Github issue: + * Remove $NULL to directly return NULL #625 + * @@ -1097,8 +1097,7 @@ public function &__get( $property ) + * $this->all = FALSE; + * $this->via = NULL; + * + * - $NULL = NULL; + * - return $NULL; + * + return NULL; + * + * leads to regression: + * PHP Stack trace: + * PHP 1. {main}() testje.php:0 + * PHP 2. RedBeanPHP\OODBBean->__get() testje.php:22 + * Notice: Only variable references should be returned by reference in rb.php on line 2529 + */ + public function testReferencedGetInBeans() + { + $bean = R::dispense( 'bean' ); + //this will trigger notice if &__get() returns NULL instead of $NULL.#625 + $x = $bean->hello; + pass(); + $x = $bean->reference; + pass(); + $x = $bean->nullvalue; + pass(); + } + + + public static $setupPartialBeansTestDone = 0; + /** + * Check partial beans at setup() + */ + public function testPartialBeansAtSetup() + { + if (self::$setupPartialBeansTestDone) return; /* only needs to be tested once */ + $currentDB = R::$currentDB; + $key = 'partialBeanBase' . time(); + $dsn = 'sqlite:/tmp/test.txt'; + $user = ''; + $pass = ''; + $frozen = FALSE; + $partialBeans = TRUE; + R::addDatabase( $key, $dsn, $user, $pass, $frozen, $partialBeans); + $redbean = R::getRedBean(); + $wasItSet = $redbean->getCurrentRepository()->usePartialBeans( FALSE ); + R::selectDatabase( $key ); + $redbean = R::getRedBean(); + $wasItSet = $redbean->getCurrentRepository()->usePartialBeans( FALSE ); + asrt( $wasItSet, TRUE ); + self::$setupPartialBeansTestDone = 1; + R::selectDatabase( $currentDB ); + } + + /** + * Test whether we can set the 'auto clear' + * option in OODB. + * + * @return void + */ + public function testAutoClearHistory() + { + testpack( 'Auto clear history' ); + $book = R::dispense( 'book' ); + $book->pages = 100; + $book->title = 'book'; + R::store( $book ); + $book = R::findOne( 'book' ); + asrt( $book->hasChanged( 'title' ), FALSE ); + $book->title = 'yes'; + R::store( $book ); + asrt( $book->hasChanged( 'title' ), TRUE ); + OODB::autoClearHistoryAfterStore( TRUE ); + $book = R::findOne( 'book' ); + asrt( $book->hasChanged( 'title' ), FALSE ); + $book->title = 'yes2'; + R::store( $book ); + asrt( $book->hasChanged( 'title' ), FALSE ); + OODB::autoClearHistoryAfterStore( FALSE ); + $book = R::findOne( 'book' ); + asrt( $book->hasChanged( 'title' ), FALSE ); + $book->title = 'yes'; + R::store( $book ); + asrt( $book->hasChanged( 'title' ), TRUE ); + } + + /** + * Tests the R::inspect() method on the Facade. + * + * @return void + */ + public function testInspect() { + + testpack( 'Test R::inspect() ' ); + R::nuke(); + R::store( R::dispense( 'book' )->setAttr( 'title', 'book' ) ); + $info = R::inspect(); + asrt( count( $info ), 1 ); + asrt( strtolower( $info[0] ), 'book' ); + $info = R::inspect( 'book' ); + asrt( count( $info ), 2 ); + $keys = array_keys( $info ); + sort($keys); + asrt( strtolower( $keys[0] ), 'id' ); + asrt( strtolower( $keys[1] ), 'title' ); + } + + /** + * Test whether we can use the tableExist() method in OODB + * instances directly to help us determine + * the existance of a table. + * + * @return void + */ + public function testTableExist() + { + R::nuke(); + R::store( R::dispense( 'book' ) ); + R::freeze( FALSE ); + asrt( R::getRedBean()->tableExists( 'book' ), TRUE ); + asrt( R::getRedBean()->tableExists( 'book2' ), FALSE ); + R::freeze( TRUE ); + asrt( R::getRedBean()->tableExists( 'book' ), TRUE ); + asrt( R::getRedBean()->tableExists( 'book2' ), FALSE ); + R::freeze( FALSE ); + } + + /** + * Normally the check() method is always called indirectly when + * dealing with beans. This test ensures we can call check() + * directly. Even though frozen repositories do not rely on + * bean checking to improve performance the method should still + * offer the same functionality when called directly. + * + * @return void + */ + public function testCheckDirectly() + { + $bean = new OODBBean; + $bean->setProperty('id', 0); + $bean->setMeta( 'type', 'book' ); + R::getRedBean()->check( $bean ); + $bean->setMeta( 'type', '.' ); + try { + R::getRedBean()->check( $bean ); + fail(); + } catch ( \Exception $e ) { + pass(); + } + //check should remain the same even if frozen repo is used, method is public after all! + //we dont want to break the API! + R::freeze( TRUE ); + try { + R::getRedBean()->check( $bean ); + fail(); + } catch ( \Exception $e ) { + pass(); + } + R::freeze( FALSE ); + } + + /** + * Test Backward compatibility writer ESC-method. + * + * @return void + */ + public function testLegacyCode() + { + testpack( 'Test Backward compatibility methods in writer.' ); + asrt( R::getWriter()->safeColumn( 'column', TRUE ), R::getWriter()->esc( 'column', TRUE ) ); + asrt( R::getWriter()->safeColumn( 'column', FALSE ), R::getWriter()->esc( 'column', FALSE ) ); + asrt( R::getWriter()->safeTable( 'table', TRUE ), R::getWriter()->esc( 'table', TRUE ) ); + asrt( R::getWriter()->safeTable( 'table', FALSE ), R::getWriter()->esc( 'table', FALSE ) ); + } + + /** + * Test beautification and array functions. + * + * @return void + */ + public function testBeauficationAndArrayFunctions() + { + $bean = R::dispense( 'bean' ); + $bean->isReallyAwesome = TRUE; + asrt( isset( $bean->isReallyAwesome ), TRUE ); + asrt( isset( $bean->is_really_awesome ), TRUE ); + unset( $bean->is_really_awesome ); + asrt( isset( $bean->isReallyAwesome ), FALSE ); + asrt( isset( $bean->is_really_awesome ), FALSE ); + } + + /** + * Test beautification of column names. + * + * @return void + */ + public function testBeautifulColumnNames() + { + testpack( 'Beautiful column names' ); + $town = R::dispense( 'town' ); + $town->isCapital = FALSE; + $town->hasTrainStation = TRUE; + $town->name = 'BeautyVille'; + $houses = R::dispense( 'house', 2 ); + $houses[0]->isForSale = TRUE; + $town->ownHouse = $houses; + R::store( $town ); + $town = R::load( 'town', $town->id ); + asrt( ( $town->isCapital == FALSE ), TRUE ); + asrt( ( $town->hasTrainStation == TRUE ), TRUE ); + asrt( ( $town->name == 'BeautyVille' ), TRUE ); + testpack( 'Accept datetime objects.' ); + $cal = R::dispense( 'calendar' ); + $cal->when = new\DateTime( '2000-01-01', new\DateTimeZone( 'Pacific/Nauru' ) ); + asrt( $cal->when, '2000-01-01 00:00:00' ); + testpack( 'Affected rows test' ); + $currentDriver = $this->currentlyActiveDriverID; + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $bean = $redbean->dispense( 'bean' ); + $bean->prop = 3; //make test run with strict mode as well + $redbean->store( $bean ); + $adapter->exec( 'UPDATE bean SET prop = 2' ); + asrt( $adapter->getAffectedRows(), 1 ); + testpack( 'Testing Logger' ); + R::getDatabaseAdapter()->getDatabase()->setLogger( new RDefault ); + asrt( ( R::getDatabaseAdapter()->getDatabase()->getLogger() instanceof Logger ), TRUE ); + asrt( ( R::getDatabaseAdapter()->getDatabase()->getLogger() instanceof RDefault ), TRUE ); + $bean = R::dispense( 'bean' ); + $bean->property = 1; + $bean->unsetAll( array( 'property' ) ); + asrt( $bean->property, NULL ); + asrt( ( $bean->setAttr( 'property', 2 ) instanceof OODBBean ), TRUE ); + asrt( $bean->property, 2 ); + asrt( preg_match( '/\d\d\d\d\-\d\d\-\d\d/', R::isoDate() ), 1 ); + asrt( preg_match( '/\d\d\d\d\-\d\d\-\d\d\s\d\d:\d\d:\d\d/', R::isoDateTime() ), 1 ); + $redbean = R::getRedBean(); + $adapter = R::getDatabaseAdapter(); + $writer = R::getWriter(); + asrt( ( $redbean instanceof OODB ), TRUE ); + asrt( ( $adapter instanceof Adapter ), TRUE ); + asrt( ( $writer instanceof QueryWriter ), TRUE ); + R::setRedBean( $redbean ); + pass(); //cant really test this + R::setDatabaseAdapter( $adapter ); + pass(); //cant really test this + R::setWriter( $writer ); + pass(); //cant really test this + $u1 = R::dispense( 'user' ); + $u1->name = 'Gabor'; + $u1->login = 'g'; + $u2 = R::dispense( 'user' ); + $u2->name = 'Eric'; + $u2->login = 'e'; + R::store( $u1 ); + R::store( $u2 ); + $list = R::getAssoc( 'select login,' . R::getWriter()->esc( 'name' ) . ' from ' . R::getWriter()->esc( 'user' ) . ' ' ); + asrt( $list['e'], 'Eric' ); + asrt( $list['g'], 'Gabor' ); + $painting = R::dispense( 'painting' ); + $painting->name = 'Nighthawks'; + $id = R::store( $painting ); + testpack( 'Testing SQL Error Types' ); + foreach ( $writer->typeno_sqltype as $code => $text ) { + asrt( is_integer( $code ), TRUE ); + asrt( is_string( $text ), TRUE ); + } + foreach ( $writer->sqltype_typeno as $text => $code ) { + asrt( is_integer( $code ), TRUE ); + asrt( is_string( $text ), TRUE ); + } + testpack( 'Testing Nowhere Pt. 1 (unfrozen)' ); + foreach ( + array( + 'exec', 'getAll', 'getCell', 'getAssoc', 'getRow', 'getCol' + ) + as $method ) { + R::$method( 'select * from nowhere' ); + pass(); + } + testpack( 'Testing Nowhere Pt. 2 (frozen)' ); + R::freeze( TRUE ); + foreach ( + array( + 'exec', 'getAll', 'getCell', 'getAssoc', 'getRow', 'getCol' + ) + as $method ) { + try { + R::$method( 'select * from nowhere' ); + fail(); + } catch ( SQL $e ) { + pass(); + } + } + R::freeze( FALSE ); + } + + /** + * Test reflectional functions of database. + * + * @return void + */ + public function testDatabaseProperties() + { + testpack( 'Testing Database Properties' ); + $adapter = R::getDatabaseAdapter(); + if ( method_exists( R::getDatabaseAdapter()->getDatabase(), 'getPDO' ) ){ + asrt( $adapter->getDatabase()->getPDO() instanceof \PDO, TRUE ); + } + asrt( strlen( $adapter->getDatabase()->getDatabaseVersion() ) > 0, TRUE ); + asrt( strlen( $adapter->getDatabase()->getDatabaseType() ) > 0, TRUE ); + } + + /** + * Test Transactions. + * + * @return void + */ + public function testTransactions() + { + testpack( 'transactions' ); + $false = R::begin(); + asrt( $false, FALSE ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + R::commit(); + asrt( R::count( 'bean' ), 1 ); + R::trash( $bean ); + R::setAllowFluidTransactions( TRUE ); + asrt( R::begin(), TRUE ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + asrt( R::commit(), TRUE ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $bean ); + asrt( R::begin(), TRUE ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + R::rollback(); + asrt( R::count( 'bean' ), 0 ); + R::setAllowFluidTransactions( FALSE ); + R::wipe('bean'); + R::freeze( TRUE ); + R::begin(); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + R::rollback(); + asrt( R::count( 'bean' ), 0 ); + R::freeze( FALSE ); + testpack( 'genSlots' ); + asrt( R::genSlots( array( 'a', 'b' ) ), '?,?' ); + asrt( R::genSlots( array( 'a' ) ), '?' ); + asrt( R::genSlots( array() ), '' ); + asrt( R::genSlots( array('a', 'b'), ' IN( %s ) ' ), ' IN( ?,? ) ' ); + } + + /** + * Test nested FUSE scenarios. + * + * @return void + */ + public function testFUSEnested() + { + testpack( 'FUSE models cant touch nested beans in update() - issue 106' ); + $spoon = R::dispense( 'spoon' ); + $spoon->name = 'spoon for test bean'; + $deep = R::dispense( 'deep' ); + $deep->name = 'deepbean'; + $item = R::dispense( 'item' ); + $item->val = 'Test'; + $item->deep = $deep; + $test = R::dispense( 'test' ); + $test->item = $item; + $test->sharedSpoon[] = $spoon; + $test->isnowtainted = TRUE; + $id = R::store( $test ); + $test = R::load( 'test', $id ); + asrt( $test->item->val, 'Test2' ); + $can = reset( $test->ownCan ); + $spoon = reset( $test->sharedSpoon ); + asrt( $can->name, 'can for bean' ); + asrt( $spoon->name, 'S2' ); + asrt( $test->item->deep->name, '123' ); + asrt( count( $test->ownCan ), 1 ); + asrt( count( $test->sharedSpoon ), 1 ); + asrt( count( $test->sharedPeas ), 10 ); + asrt( count( $test->ownChip ), 9 ); + } + + /** + * Tests FUSE and lists, FUSE enforces no more than + * 3 sugar cubes in coffee. + * + * @return void + */ + public function testCoffeeWithSugarAndFUSE() + { + $coffee = R::dispense( 'coffee' ); + $coffee->size = 'XL'; + $coffee->ownSugar = R::dispense( 'sugar', 5 ); + $id = R::store( $coffee ); + $coffee = R::load( 'coffee', $id ); + asrt( count( $coffee->ownSugar ), 3 ); + $coffee->ownSugar = R::dispense( 'sugar', 2 ); + $id = R::store( $coffee ); + $coffee = R::load( 'coffee', $id ); + asrt( count( $coffee->ownSugar ), 2 ); + $cocoa = R::dispense( 'cocoa' ); + $cocoa->name = 'Fair Cocoa'; + list( $taste1, $taste2 ) = R::dispense( 'taste', 2 ); + $taste1->name = 'sweet'; + $taste2->name = 'bitter'; + $cocoa->ownTaste = array( $taste1, $taste2 ); + R::store( $cocoa ); + $cocoa->name = 'Koko'; + R::store( $cocoa ); + if ( method_exists( R::getDatabaseAdapter()->getDatabase(), 'getPDO' ) ) { + $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + $driver = new RPDO( $pdo ); + pass(); + asrt( $pdo->getAttribute(\PDO::ATTR_ERRMODE ),\PDO::ERRMODE_EXCEPTION ); + asrt( $pdo->getAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE ),\PDO::FETCH_ASSOC ); + asrt( strval( $driver->GetCell( 'select 123' ) ), '123' ); + } + $a = new SQL; + $a->setSqlState( 'test' ); + $b = strval( $a ); + asrt( ( strpos( $b, '[test] - ' ) === 0 ), TRUE ); + } + + /** + * ENUM Basic tests. + * + * @return void + */ + public function testENUMBasics() { + asrt( R::enum( 'gender:male' )->name, 'MALE' ); + asrt( R::enum( 'country:South-Africa' )->name, 'SOUTH_AFRICA' ); + asrt( R::enum( 'tester:T@E S_t' )->name, 'T_E_S_T' ); + } + + /** + * Test ENUM in Queries and with short hand notation. + * + * @return void + */ + public function testENUMInQuery() + { + testpack('Test ENUM in Query and test ENUM short notation'); + R::nuke(); + $coffee = R::dispense( 'coffee' ); + $coffee->taste = R::enum( 'flavour:mocca' ); + R::store( $coffee ); + $coffee = R::dispense( 'coffee' ); + $coffee->taste = R::enum( 'flavour:banana' ); + R::store( $coffee ); + $coffee = R::dispense( 'coffee' ); + $coffee->taste = R::enum( 'flavour:banana' ); + R::store( $coffee ); + //now we have two flavours + asrt( R::count('flavour'), 2 ); + //use in query + asrt( R::count( 'coffee', ' taste_id = ? ', array( R::enum( 'flavour:mocca' )->id ) ), 1); + //use in quer with short notation + asrt( R::count( 'coffee', ' taste_id = ? ', array( EID( 'flavour:mocca' ) ) ), 1); + //use in query + asrt( R::count( 'coffee', ' taste_id = ? ', array( R::enum( 'flavour:banana' )->id ) ), 2); + //use in quer with short notation + asrt( R::count( 'coffee', ' taste_id = ? ', array( EID( 'flavour:banana' ) ) ), 2); + //use in query + asrt( R::count( 'coffee', ' taste_id = ? ', array( R::enum( 'flavour:strawberry' )->id ) ), 0); + //use in quer with short notation + asrt( R::count( 'coffee', ' taste_id = ? ', array( EID( 'flavour:strawberry' ) ) ), 0); + } + + /** + * Test ENUM functionality offered by Label Maker. + * + * @return void + */ + public function testENUM() { + testpack('test ENUM'); + $coffee = R::dispense( 'coffee' ); + $coffee->taste = R::enum( 'flavour:mocca' ); + //did we create an enum? + asrt( implode( '', R::gatherLabels( R::enum( 'flavour' ) ) ), 'MOCCA' ); + R::store( $coffee ); + $coffee = $coffee->fresh(); + //test enum identity check - with alias + asrt( $coffee->fetchAs( 'flavour' )->taste->equals( R::enum('flavour:mocca') ), TRUE ); + asrt( $coffee->fetchAs( 'flavour' )->taste->equals( R::enum('flavour:banana') ), FALSE ); + //now we have two flavours + asrt( R::count( 'flavour' ), 2 ); + asrt( implode( ',', R::gatherLabels( R::enum( 'flavour') ) ), 'BANANA,MOCCA' ); + $coffee->flavour = R::enum( 'flavour:mocca' ); + R::store($coffee); + //same results, can we have multiple flavours? + asrt( $coffee->fetchAs( 'flavour' )->taste->equals( R::enum( 'flavour:mocca' ) ), TRUE ); + asrt( $coffee->fetchAs( 'flavour' )->taste->equals( R::enum( 'flavour:banana' ) ), FALSE ); + asrt( $coffee->flavour->equals( R::enum( 'flavour:mocca' ) ), TRUE ); + //no additional mocca enum... + asrt( R::count( 'flavour' ), 2 ); + $drink = R::dispense( 'drink' ); + $drink->flavour = R::enum( 'flavour:choco' ); + R::store( $drink ); + //now we have three! + asrt( R::count('flavour'), 3 ); + $drink = R::load( 'drink', $drink->id ); + asrt( $drink->flavour->equals( R::enum('flavour:mint') ), FALSE ); + asrt( $drink->flavour->equals( R::enum('flavour:choco') ), TRUE ); + asrt( R::count( 'flavour' ), 4 ); + //trash should not affect flavour! + R::trash( $drink ); + asrt( R::count( 'flavour' ), 4 ); + } + + /** + * Test trashAll(). + */ + public function testMultiDeleteUpdate() + { + testpack( 'test multi delete and multi update' ); + $beans = R::dispenseLabels( 'bean', array( 'a', 'b' ) ); + $ids = R::storeAll( $beans ); + asrt( (int) R::count( 'bean' ), 2 ); + R::trashAll( R::batch( 'bean', $ids ) ); + asrt( (int) R::count( 'bean' ), 0 ); + testpack( 'test assocManager check' ); + $rb = new OODB( R::getWriter() ); + try { + $rb->getAssociationManager(); + fail(); + } catch ( RedException $e ) { + pass(); + } + } + + /** + * Test Bean identity equality. + */ + public function testBeanIdentityEquality() { + $beanA = R::dispense( 'bean' ); + $beanB = R::dispense( 'bean' ); + $beanA->id = 1; + $beanB->id = 1; + asrt( $beanA->equals( $beanB ), TRUE ); + asrt( $beanB->equals( $beanA ), TRUE ); + asrt( $beanA->equals( $beanA ), TRUE ); + asrt( $beanB->equals( $beanB ), TRUE ); + $beanB->id = 2; + asrt( $beanA->equals( $beanB ), FALSE ); + asrt( $beanB->equals( $beanA ), FALSE ); + $beanA->id = '2'; + asrt( $beanA->equals( $beanB ), TRUE ); + asrt( $beanB->equals( $beanA ), TRUE ); + $beanB = R::dispense( 'carrot' ); + $beanB->id = $beanA->id; + asrt( $beanA->equals( $beanB ), FALSE ); + asrt( $beanB->equals( $beanA ), FALSE ); + } + + /** + * Test if adding SimpleModles to a shared list will auto unbox them. + */ + public function testSharedListsAutoUnbox() + { + $boxedBean = R::dispense( 'boxedbean' ); + $bean = R::dispense( 'bean' ); + $model = new SimpleModel(); + $model->loadBean($boxedBean); + $bean->ownBoxedbeanList[] = $model; + try { + R::store( $bean ); + pass(); + } catch ( \Exception $e ) { + fail(); + } + } + + /** + * Test if we can obtain a database server version string + * from the Facade. + */ + public function testGetDatabaseServerVersion() + { + $version = R::getDatabaseServerVersion(); + asrt(is_string($version), TRUE); + asrt(strlen($version)>0, TRUE); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Namedparams.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Namedparams.php new file mode 100644 index 0000000..95d4b31 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Namedparams.php @@ -0,0 +1,103 @@ +title = 'book'; + $book->sharedPage[] = $page; + R::store($book); + //should not give error like: Uncaught [HY093] - SQLSTATE[HY093]: Invalid parameter number: mixed named and positional parameters + $books = $page->withCondition(' title = :title ', array( ':title' => 'book' ) )->sharedBook; + asrt( count( $books ), 1 ); + //should not give error... + $books = $page->withCondition( ' title = :title ', array( ':title' => 'book' ) )->sharedBook; + asrt( count( $books ), 1 ); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->title = 'book'; + $book->comment = 'comment'; + $page->title = 'page'; + $book->ownPage[] = $page; + R::store( $book ); + //should also not give error.. + $count = $book->countOwn( 'page' ); + asrt( $count, 1 ); + $book = $book->fresh(); + //should also not give error.. + $count = $book->withCondition( ' title = ? ', array( 'page' ) )->countOwn( 'page' ); + asrt( $count, 1 ); + $book = $book->fresh(); + //should also not give error.. + $count = $book->withCondition( ' title = :title ', array( ':title' => 'page' ) )->countOwn( 'page' ); + asrt( $count, 1 ); + $book = $book->fresh(); + $pages = $book->withCondition( ' title = :title ', array( ':title' => 'page' ) )->ownPage; + asrt( count( $pages ), 1 ); + //test with duplicate slots... + $page = reset( $pages ); + $page2 = R::dispense( 'page' ); + $page2->ownPage[] = $page; + R::store( $page2 ); + $page2 = $page2->fresh(); + $pages = $page2->withCondition( ' title = :title ', array( ':title' => 'page' ) )->ownPage; + asrt( count( $pages ), 1 ); + //test with find() + $books = R::getRedBean()->find( 'book', + array( + 'title' => array('book')), + ' AND title = :title ', array(':title'=>'book')); + asrt( count( $books ), 1 ); + $books = R::getRedBean()->find( 'book', + array( + 'title' => array('book', 'book2'), + 'comment' => array('comment', 'comment2')), + ' AND title = :title ', array(':title'=>'book')); + asrt( count( $books ), 1 ); + //just check numeric works as well... + $books = R::getRedBean()->find( 'book', + array( + 'title' => array('book', 'book2'), + 'comment' => array('comment', 'comment2')), + ' AND title = ? ', array('book')); + asrt( count( $books ), 1 ); + //just extra check to verify glue works + $books = R::getRedBean()->find( 'book', + array( + 'title' => array('book', 'book2'), + 'comment' => array('comment', 'comment2')), + ' ORDER BY id '); + asrt( count( $books ), 1 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Nuke.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Nuke.php new file mode 100644 index 0000000..ec700f2 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Nuke.php @@ -0,0 +1,91 @@ +getTables() ), 1 ); + R::nuke(); + asrt( count( R::getWriter()->getTables() ), 0 ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + asrt( count( R::getWriter()->getTables() ), 1 ); + R::freeze(); + R::nuke(); + // No effect + asrt( count( R::getWriter()->getTables() ), 1 ); + R::freeze( FALSE ); + } + + /** + * Test noNuke(). + * + * @return void + */ + public function testNoNuke() { + $bean = R::dispense( 'bean' ); + R::store( $bean ); + asrt( count( R::getWriter()->getTables() ), 1 ); + R::noNuke( TRUE ); + try { + R::nuke(); + fail(); + } catch( \Exception $e ) { + pass(); + } + asrt( count( R::getWriter()->getTables() ), 1 ); + R::noNuke( FALSE ); + R::nuke(); + asrt( count( R::getWriter()->getTables() ), 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Observers.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Observers.php new file mode 100644 index 0000000..ecd9b9d --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Observers.php @@ -0,0 +1,55 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + asrt( ( $adapter instanceof DBAdapter ), TRUE ); + asrt( ( $writer instanceof QueryWriter ), TRUE ); + asrt( ( $redbean instanceof OODB ), TRUE ); + $observable = new \ObservableMock(); + $observer = new \ObserverMock(); + $observable->addEventListener( "event1", $observer ); + $observable->addEventListener( "event3", $observer ); + $observable->test( "event1", "testsignal1" ); + asrt( $observer->event, "event1" ); + asrt( $observer->info, "testsignal1" ); + $observable->test( "event2", "testsignal2" ); + asrt( $observer->event, "event1" ); + asrt( $observer->info, "testsignal1" ); + $observable->test( "event3", "testsignal3" ); + asrt( $observer->event, "event3" ); + asrt( $observer->info, "testsignal3" ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Partial.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Partial.php new file mode 100644 index 0000000..bd2044c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Partial.php @@ -0,0 +1,262 @@ + 'strong', + 'beans' => 'Arabica', + 'preparation' => 'Kettle' + ); + $coffee = R::dispense('coffee'); + $changelist = $coffee->getMeta('changelist'); + asrt( count( $changelist), 3 ); + $coffee->preparation = 'Espresso'; + $changelist = $coffee->getMeta('changelist'); + asrt( count( $changelist), 4 ); + $id = R::store( $coffee ); + $coffee = R::load( 'coffee', $id ); + $changelist = $coffee->getMeta('changelist'); + asrt( count( $changelist), 0 ); + } + + /** + * Github Issue #754. + * The importRow() function should clear the changeList. + * + * @return void + */ + public function testChangeListImportRow() + { + R::usePartialBeans( TRUE ); + $bean = R::dispense( 'bean' ); + asrt( count( $bean->getMeta('changelist') ), 0 ); + $bean->property = 'abc'; + asrt( count( $bean->getMeta('changelist') ), 1 ); + $bean->importRow( array( 'property' => 123 ) ); + asrt( count( $bean->getMeta('changelist') ), 0 ); + } + + /** + * Tests the basic scenarios for Partial Beans. + * + * @return void + */ + public function testPartialBeans() + { + R::nuke(); + R::usePartialBeans( FALSE ); + $book = R::dispense( 'book' ); + $book->title = 'A book about half beans'; + $book->price = 99; + $book->pages = 60; + $id = R::store( $book ); + /* test baseline condition */ + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 60 ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 61 ); + /* now test partial beans mode */ + R::usePartialBeans( TRUE ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->pages, 62 ); + /* mask should be cleared... */ + R::exec( 'UPDATE book SET pages = ? ', array( 64 ) ); + $book->price = 92; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( (integer) $book->pages, 64 ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->price, 92 ); + R::usePartialBeans( FALSE ); + } + + /** + * Tests whether we can pass a list of specific bean types + * to apply partial saving to. + * + * @return void + */ + public function testPartialBeansTypeList() + { + R::nuke(); + R::usePartialBeans( array( 'notbook' ) ); + $book = R::dispense( 'book' ); + $book->title = 'A book about half beans'; + $book->price = 99; + $book->pages = 60; + $id = R::store( $book ); + /* test baseline condition */ + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 60 ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 61 ); + /* now test partial beans mode */ + R::usePartialBeans( array( 'book', 'more' ) ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->pages, 62 ); + /* mask should be cleared... */ + R::exec( 'UPDATE book SET pages = ? ', array( 64 ) ); + $book->price = 92; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( (integer) $book->pages, 64 ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->price, 92 ); + R::usePartialBeans( FALSE ); + } + + /** + * Tests the basic scenarios for Partial Beans. + * Frozen. + * + * @return void + */ + public function testPartialBeansFrozen() + { + R::nuke(); + R::usePartialBeans( FALSE ); + $book = R::dispense( 'book' ); + $book->title = 'A book about half beans'; + $book->price = 99; + $book->pages = 60; + $id = R::store( $book ); + /* test baseline condition */ + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 60 ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 61 ); + /* now test partial beans mode */ + R::freeze( TRUE ); + R::usePartialBeans( TRUE ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->pages, 62 ); + /* mask should be cleared... */ + R::exec( 'UPDATE book SET pages = ? ', array( 64 ) ); + $book->price = 92; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( (integer) $book->pages, 64 ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->price, 92 ); + R::usePartialBeans( FALSE ); + R::freeze( FALSE ); + } + + /** + * Tests whether we can pass a list of specific bean types + * to apply partial saving to. + * Frozen. + * + * @return void + */ + public function testPartialBeansTypeListFrozen() + { + R::nuke(); + R::usePartialBeans( array( 'notbook' ) ); + $book = R::dispense( 'book' ); + $book->title = 'A book about half beans'; + $book->price = 99; + $book->pages = 60; + $id = R::store( $book ); + /* test baseline condition */ + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 60 ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'A book about half beans' ); + asrt( (integer) $book->pages, 61 ); + /* now test partial beans mode */ + R::freeze( TRUE ); + R::usePartialBeans( array( 'book', 'more' ) ); + $book->pages++; + R::exec( 'UPDATE book SET title = ? ', array( 'Another Title' ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->pages, 62 ); + /* mask should be cleared... */ + R::exec( 'UPDATE book SET pages = ? ', array( 64 ) ); + $book->price = 92; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( (integer) $book->pages, 64 ); + asrt( $book->title, 'Another Title' ); + asrt( (integer) $book->price, 92 ); + R::usePartialBeans( FALSE ); + R::freeze( FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Performance.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Performance.php new file mode 100644 index 0000000..58fa332 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Performance.php @@ -0,0 +1,126 @@ +title = 'book'; + $pages = R::dispense( 'page', 10 ); + foreach( $pages as $page ) { + $page->content = 'lorem ipsum'; + $page->title = 'data'; + $page->sequence = 'data'; + $page->order = 'data'; + $page->columns = 'data'; + $page->paragraphs = 'data'; + $page->paragraphs1 = 'data'; + $page->paragraphs2 = 'data'; + $page->paragraphs3 = 'data'; + $page->paragraphs4 = 'data'; + } + $book->xownPageList = $pages; + $tags = R::dispense( 'tag', 6 ); + foreach( $tags as $tag ) { + $tag->label = 'tag'; + } + $book->sharedTagList = $tags; + R::store( $book ); + } + + /** + * CRUD performance. + * + * @return void + */ + public function crud() + { + R::freeze( TRUE ); + + $book = R::dispense( 'book' ); + $book->title = 'Book'; + $page = R::dispense('page'); + $page->content = 'Content'; + $page->title = 'data'; + $page->sequence = 'data'; + $page->order = 'data'; + $page->columns = 'data'; + $page->paragraphs = 'data'; + $page->paragraphs1 = 'data'; + $page->paragraphs2 = 'data'; + $page->paragraphs3 = 'data'; + $page->paragraphs4 = 'data'; + $tag = R::dispense('tag'); + $tag->label = 'Tag '; + $book->noLoad()->ownPage[] = $page; + $book->noLoad()->sharedTag[] = $tag; + R::store( $book ); + $book = $book->fresh(); + $book->ownPage; + $book->sharedTag; + R::trash( $book ); + + } + + /** + * CRUD performance Array Access. + * + * @return void + */ + public function crudaa() + { + R::freeze( TRUE ); + + $book = R::dispense( 'book' ); + $book['title'] = 'Book'; + $page = R::dispense('page'); + $page['content'] = 'Content'; + $page['title'] = 'data'; + $page['sequence'] = 'data'; + $page['order'] = 'data'; + $page['columns'] = 'data'; + $page['paragraphs'] = 'data'; + $page['paragraphs1'] = 'data'; + $page['paragraphs2'] = 'data'; + $page['paragraphs3'] = 'data'; + $page['paragraphs4'] = 'data'; + $tag = R::dispense('tag'); + $tag['label'] = 'Tag '; + $book->ownPage[] = $page; + $book->noLoad()->sharedTag[] = $tag; + R::store( $book ); + $book = $book->fresh(); + $book->ownPage; + $book->sharedTag; + R::trash( $book ); + + } +} \ No newline at end of file diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Prefixes.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Prefixes.php new file mode 100644 index 0000000..73d2564 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Prefixes.php @@ -0,0 +1,269 @@ +dispense($type); +}); + +define('BOOK', 'tbl_book'); +define('AUTHOR', 'tbl_author'); +define('COAUTHOR', 'coAuthor'); +define('FRIEND', 'tbl_friend'); +define('PUBLISHER', 'tbl_publisher'); +define('BOOKLIST', 'ownTbl_book'); +define('FRIENDLIST', 'sharedTbl_friend'); + +/** + * Prefixes + * + * Tests whether we can use tables with prefixes. + * Some people seem to like that. + * + * @file RedUNIT/Base/Prefixes.php + * @desc Tests whether you can use RedBeanPHP with table prefixes. + * @author Gabor de Mooij and the RedBeanPHP Community + * @license New BSD/GPLv2 + * + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the New BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Prefixes extends Base +{ + /** + * Test prerequisites. + */ + public function testPrerequisites() + { + R::nuke(); + $bean = R::xdispense( 'type_with_underscore' ); + asrt( ( $bean instanceof OODBBean ), TRUE ); + asrt( constant( 'BOOK' ), 'tbl_book' ); + asrt( constant( 'AUTHOR' ), 'tbl_author' ); + asrt( constant( 'PUBLISHER' ), 'tbl_publisher' ); + asrt( constant( 'FRIEND' ), 'tbl_friend' ); + asrt( constant( 'BOOKLIST' ), 'ownTbl_book' ); + asrt( constant( 'FRIENDLIST' ), 'sharedTbl_friend' ); + asrt( constant( 'COAUTHOR' ), 'coAuthor' ); + } + + /** + * Test basic CRUD operations. + */ + public function testBasicOperations() + { + //Can we dispense a naughty bean? (with underscore) + $author = R::xdispense( AUTHOR ); + asrt( ( $author instanceof OODBBean ), TRUE ); + asrt( $author->getMeta('type'), AUTHOR ); + $author->name = 'Mr. Quill'; + $book = R::xdispense( BOOK ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( $book->getMeta('type'), BOOK ); + $book->title = 'Good Stories'; + $friend = R::xdispense( FRIEND ); + $friend->name = 'Muse'; + asrt( ( $friend instanceof OODBBean ), TRUE ); + asrt( $friend->getMeta('type'), FRIEND ); + $publisher = R::xdispense( PUBLISHER ); + $publisher->name = 'Good Books'; + asrt( ( $publisher instanceof OODBBean ), TRUE ); + asrt( $publisher->getMeta('type'), PUBLISHER ); + asrt( is_array( $author->{BOOKLIST} ), TRUE ); + //add books to the book list using the constant + $author->{BOOKLIST}[] = $book; + asrt( count( $author->{BOOKLIST} ), 1 ); + //can we also add friends? (N-M) + $author->{FRIENDLIST}[] = $friend; + $author->{PUBLISHER} = $publisher; + $id = R::store( $author ); + asrt( ( $id > 0 ), TRUE ); + $author = $author->fresh(); + //Can we add another friend after reload? + $author->{FRIENDLIST}[] = R::xdispense( FRIEND )->setAttr( 'name', 'buddy' ); + R::store($author); + $author = $author->fresh(); + //Now check the contents of the bean, its lists (books,friends) and parent (publisher) + asrt( $author->name, 'Mr. Quill' ); + asrt( count( $author->{BOOKLIST} ), 1 ); + $firstBook = reset( $author->{BOOKLIST} ); + asrt( $firstBook->title, 'Good Stories' ); + asrt( count( $author->{FRIENDLIST} ), 2 ); + $firstFriend = reset( $author->{FRIENDLIST} ); + $parent = $author->{PUBLISHER}; + asrt( ( $parent instanceof OODBBean ), TRUE ); + $tables = R::inspect(); + //have all tables been prefixed? + foreach( $tables as $table ) asrt( strpos( $table, 'tbl_' ), 0 ); + //Can we make an export? + $export = R::exportAll( R::findOne( AUTHOR ), TRUE ); + $export = reset( $export ); + asrt( isset( $export[ PUBLISHER ] ), TRUE ); + asrt( isset( $export[ BOOKLIST ] ), TRUE ); + asrt( isset( $export[ FRIENDLIST ] ), TRUE ); + asrt( isset( $export[ 'ownBook' ] ), FALSE ); + asrt( isset( $export[ 'sharedFriend' ] ), FALSE ); + asrt( isset( $export[ 'publisher' ] ), FALSE ); + //Can we duplicate? + $copy = R::dup( $author ); + $copy->name = 'Mr. Clone'; + R::store( $copy ); + $copy = $copy->fresh(); + asrt( $copy->name, 'Mr. Clone' ); + asrt( count( $copy->{BOOKLIST} ), 1 ); + $firstBook = reset( $copy->{BOOKLIST} ); + asrt( $firstBook->title, 'Good Stories' ); + asrt( count( $copy->{FRIENDLIST} ), 2 ); + $firstFriend = reset( $copy->{FRIENDLIST} ); + $parent = $copy->{PUBLISHER}; + asrt( ( $parent instanceof OODBBean ), TRUE ); + //Can we count? + asrt( R::count( AUTHOR ), 2 ); + $copy = $copy->fresh(); + asrt( $copy->countOwn( BOOK ), 1 ); + asrt( $copy->countShared( FRIEND ), 2 ); + //Can we delete? + R::trash( $author ); + asrt( R::count( AUTHOR ), 1 ); + //Can we nuke? + R::nuke(); + asrt( R::count( AUTHOR ), 0 ); + asrt( count( R::inspect() ), 0 ); + } + + /** + * Test basic operations in frozen mode. + */ + public function testBasicOperationsFrozen() + { + R::nuke(); + $author = R::xdispense( AUTHOR ); + $author->name = 'Mr. Quill'; + $book = R::xdispense( BOOK ); + $book->title = 'Good Stories'; + $book2 = R::xdispense( BOOK ); + $book2->title = 'Good Stories 2'; + $friend = R::xdispense( FRIEND ); + $friend->name = 'Muse'; + $publisher = R::xdispense( PUBLISHER ); + $publisher->name = 'Good Books'; + $author->{BOOKLIST} = array( $book, $book2 ); + $author->{FRIENDLIST}[] = $friend; + $author->{PUBLISHER} = $publisher; + $coAuthor = R::xdispense( AUTHOR ); + $coAuthor->name = 'Xavier'; + $book2->{COAUTHOR} = $coAuthor; + R::store( $author ); + R::freeze( TRUE ); + asrt( $author->name, 'Mr. Quill' ); + asrt( count( $author->{BOOKLIST} ), 2 ); + $firstBook = reset( $author->{BOOKLIST} ); + asrt( $firstBook->title, 'Good Stories' ); + asrt( count( $author->{FRIENDLIST} ), 1 ); + $firstFriend = reset( $author->{FRIENDLIST} ); + $parent = $author->{PUBLISHER}; + asrt( ( $parent instanceof OODBBean ), TRUE ); + $tables = R::inspect(); + //have all tables been prefixed? + foreach( $tables as $table ) asrt( strpos( $table, 'tbl_' ), 0 ); + //Can we make an export? + $export = R::exportAll( R::findOne( AUTHOR ), TRUE ); + $export = reset( $export ); + asrt( isset( $export[ PUBLISHER ] ), TRUE ); + asrt( isset( $export[ BOOKLIST ] ), TRUE ); + asrt( isset( $export[ FRIENDLIST ] ), TRUE ); + asrt( isset( $export[ 'ownBook' ] ), FALSE ); + asrt( isset( $export[ 'sharedFriend' ] ), FALSE ); + asrt( isset( $export[ 'publisher' ] ), FALSE ); + R::freeze( FALSE ); + } + + /** + * Test conditions and aliases. + */ + public function testConditionsAndAliases() + { + R::nuke(); + $author = R::xdispense( AUTHOR ); + $author->name = 'Mr. Quill'; + $book = R::xdispense( BOOK ); + $book->title = 'Good Stories'; + $book2 = R::xdispense( BOOK ); + $book2->title = 'Good Stories 2'; + $friend = R::xdispense( FRIEND ); + $friend->name = 'Muse'; + $publisher = R::xdispense( PUBLISHER ); + $publisher->name = 'Good Books'; + $author->{BOOKLIST} = array( $book, $book2 ); + $author->{FRIENDLIST}[] = $friend; + $author->{PUBLISHER} = $publisher; + $coAuthor = R::xdispense( AUTHOR ); + $coAuthor->name = 'Xavier'; + $book2->{COAUTHOR} = $coAuthor; + R::store( $author ); + $author = $author->fresh(); + asrt( R::count( AUTHOR ), 2 ); + //Can we use with and withCondition? + asrt( count( $author->{BOOKLIST} ), 2 ); + asrt( count( $author->with(' LIMIT 1 ')->{BOOKLIST} ), 1 ); + asrt( count( $author->withCondition(' title LIKE ? ', array( '%2%' ) )->{BOOKLIST} ), 1 ); + //Can we use an alias? + $book2 = $book2->fresh(); + asrt( $book2->fetchAs( AUTHOR )->{COAUTHOR}->name, 'Xavier' ); + $coAuthor = $book2->fetchAs( AUTHOR )->{COAUTHOR}->fresh(); + asrt( count( $coAuthor->alias( COAUTHOR )->{BOOKLIST} ), 1 ); + } + + /** + * Test prettier tables using via(). + */ + public function testViaPrettification() + { + R::nuke(); + R::renameAssociation( 'tbl_author_tbl_friend', 'tbl_author_friend' ); + $author = R::xdispense( AUTHOR ); + $author->name = 'Mr. Quill'; + $friend = R::xdispense( FRIEND ); + $friend->name = 'Muse'; + $author->{FRIENDLIST}[] = $friend; + $id = R::store( $author ); + //print_r(R::inspect()); exit; + $author = R::load( AUTHOR, $id ); + $tables = array_flip( R::inspect() ); + asrt( isset( $tables[ 'tbl_author_friend' ] ), TRUE ); + asrt( isset( $tables[ 'tbl_author_tbl_friend' ] ), FALSE ); + asrt( count( $author->{FRIENDLIST} ), 1 ); + AQueryWriter::clearRenames(); + } + + /** + * Test self-referential N-M relations. + */ + public function testSelfRefNM() + { + R::nuke(); + $friend1 = R::xdispense( FRIEND ); + $friend1->name = 'f1'; + $friend2 = R::xdispense( FRIEND ); + $friend2->name = 'f2'; + $friend3 = R::xdispense( FRIEND ); + $friend3->name = 'f3'; + $friend1->{FRIENDLIST} = array( $friend2, $friend3 ); + $friend3->{FRIENDLIST} = array( $friend1 ); + R::storeAll( array( $friend1, $friend2, $friend3 ) ); + $friend1 = $friend1->fresh(); + $friend2 = $friend2->fresh(); + $friend3 = $friend3->fresh(); + asrt( count( $friend1->{FRIENDLIST} ), 2 ); + asrt( count( $friend2->{FRIENDLIST} ), 1 ); + asrt( count( $friend3->{FRIENDLIST} ), 1 ); + $friend = reset( $friend3->{FRIENDLIST} ); + asrt( $friend->name, 'f1' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Productivity.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Productivity.php new file mode 100644 index 0000000..a442124 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Productivity.php @@ -0,0 +1,233 @@ +uname = 'Shawn'; + $account->pass = sha1( 'sheep' ); + $account->archived = 0; + $account->attempts = 1; + + R::store( $account ); + $matchUp = new MatchUp( R::getToolbox() ); + + /* simulate a token generation script */ + $account = NULL; + $didGenToken = $matchUp->matchUp( 'account', ' uname = ? AND archived = ?', array('Shawn',0), array( + 'token' => sha1(rand(0,9000) . time()), + 'tokentime' => time() + ), NULL, $account ); + + asrt( $didGenToken, TRUE ); + asrt( !is_null( $account->token ) , TRUE ); + asrt( !is_null( $account->tokentime ) , TRUE ); + + /* simulate a password reset script */ + $newpass = '1234'; + $didResetPass = $matchUp->matchUp( 'account', ' token = ? AND tokentime > ? ', array( $account->token, time()-100 ), array( + 'pass' => $newpass, + 'token' => '' + ), NULL, $account ); + asrt( $account->pass, '1234' ); + asrt( $account->token, '' ); + + /* simulate a login */ + $didFindUsr = $matchUp->matchUp( 'account', ' uname = ? ', array( 'Shawn' ), array( + 'attempts' => function( $acc ) { + return ( $acc->pass !== '1234' ) ? ( $acc->attempts + 1 ) : 0; + } + ), NULL, $account); + + asrt( $didFindUsr, TRUE ); + asrt( $account->attempts, 0 ); + + /* Login failure */ + $didFindUsr = $matchUp->matchUp( 'account', ' uname = ? ', array( 'Shawn' ), array( + 'attempts' => function( $acc ) { + return ( $acc->pass !== '1236' ) ? ( $acc->attempts + 1 ) : 0; + } + ), NULL, $account); + + /* Create user if not exists */ + $didFindUsr = R::matchUp( 'account', ' uname = ? ', array( 'Anonymous' ), array( + ), array( + 'uname' => 'newuser' + ), $account); + asrt( $didFindUsr, FALSE ); + asrt( $account->uname, 'newuser' ); + } + + /** + * Tests the look function. + */ + public function testLook() + { + R::nuke(); + $beans = R::dispenseAll( 'color*3' ); + list( $red, $green, $blue ) = $beans[0]; + $red->name = 'red'; + $green->name = 'green'; + $blue->name = 'blue'; + $red->thevalue = 'r'; + $green->thevalue = 'g'; + $blue->thevalue = 'b'; + R::storeAll( array( $red, $green, $blue ) ); + $look = R::getLook(); + asrt( ( $look instanceof Look ), TRUE ); + $str = R::getLook()->look( 'SELECT * FROM color WHERE thevalue != ? ORDER BY thevalue ASC', array( 'g' ), array( 'thevalue', 'name' ), + '', 'strtoupper', "\n" + ); + asrt( $str, + "\n" + ); + $str = R::look( 'SELECT * FROM color WHERE thevalue != ? ORDER BY thevalue ASC', array( 'g' ), array( 'thevalue', 'name' ), + '', 'strtoupper', "\n" + ); + asrt( $str, + "\n" + ); + } + + /** + * Test Bean differ. + */ + public function testDiff() + { + R::nuke(); + $ad = R::dispense( 'ad' ); + $ad->title = 'dog looking for new home'; + $ad->created = time(); + $ad->modified = time(); + $ad->ownDog[] = R::dispense( 'dog' ); + $ad->ownDog[0]->name = 'Dweep'; + $ad->ownDog[0]->color = 'green'; + $ad->author = R::dispense('user'); + $ad->author->name = 'John'; + R::store( $ad ); + $ad->title = 'green dog'; + $diff = R::diff( $ad->fresh(), $ad ); + /* simple case, property changed */ + var_dump( $diff ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 1 ); + asrt( $diff['ad.1.title'][0], 'dog looking for new home' ); + asrt( $diff['ad.1.title'][1], 'green dog' ); + /* test use specific format */ + $diff = R::diff( $ad->fresh(), $ad, array(), '%1$s.%3$s' ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 1 ); + asrt( $diff['ad.title'][0], 'dog looking for new home' ); + asrt( $diff['ad.title'][1], 'green dog' ); + /* skip created modified */ + $ad = $ad->fresh(); + $ad->modified = 111; + $ad->created = 111; + $diff = R::diff( $ad->fresh(), $ad ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 0 ); + /* unless we set anothe filter */ + $ad = $ad->fresh(); + $ad->modified = 111; + $ad->created = 111; + $ad->name = 'x'; + $diff = R::diff( $ad->fresh(), $ad, array( 'name' ) ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 2 ); + asrt( $diff['ad.1.modified'][1], 111 ); + asrt( $diff['ad.1.created'][1], 111 ); + $ad = $ad->fresh(); + /* also diff changes in related beans */ + $ad->fetchAs('user')->author->name = 'Fred'; + $dog = reset( $ad->ownDog ); + $dog->color = 999; + $old = $ad->fresh(); + $old->ownDog; + $old->fetchAs('user')->author; + $diff = R::diff( $ad, $old ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 2 ); + asrt( $diff['ad.1.ownDog.1.color'][1], 'green' ); + asrt( $diff['ad.1.ownDog.1.color'][0], 999 ); + asrt( $diff['ad.1.author.1.name'][1], 'John' ); + asrt( $diff['ad.1.author.1.name'][0], 'Fred' ); + $diff = R::diff( $ad, null ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 0 ); + $diff = R::diff( null, $ad ); + asrt( is_array( $diff ), TRUE ); + asrt( count( $diff ), 0 ); + + /* demo case */ + list($book,$pages) = R::dispenseAll('book,page*2'); + $book->title = 'Old Book'; + $book->price = 999; + $book->ownPageList = $pages; + $pages[0]->text = 'abc'; + $pages[1]->text = 'def'; + R::store($book); + $book->title = 'new Book'; + $page = end($book->ownPageList); + $page->text = 'new'; + $oldBook = $book->fresh(); + $oldBook->ownPageList; + $diff = R::diff($oldBook, $book); + } + + /** + * Test misc. matchUp scenarios. + * + * @return void + */ + public function testMatchUpMisc() + { + R::nuke(); + asrt( R::count( 'bean' ), 0 ); + $found = R::matchUp( 'bean', ' id = ? ', array(1), array(), array( + 'notfound' => function( $bean ) { + $bean->status = 'not found'; + } + ) ); + asrt( $found, FALSE ); + asrt( R::count( 'bean' ), 1 ); + $bean = R::findOne( 'bean' ); + asrt( $bean->status, 'not found' ); + $null = R::matchUp( 'bean', ' id = ? ', array( $bean->id ) ); + asrt( is_null( $null ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/PullRequest530.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/PullRequest530.php new file mode 100644 index 0000000..36d2d60 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/PullRequest530.php @@ -0,0 +1,51 @@ +if ($bean->$linkField != $id) $bean->$linkField = $id; + * + * @return void + */ + public function testPullRequest530() + { + testpack( 'Testing Pull Request #530 - OODBBean __set() checks if $property is a field link' ); + R::freeze( FALSE ); + $linkedObjects = R::dispense('linked', 2); + R::storeAll($linkedObjects); + $tester = R::dispense('parent'); + $tester->linked = $linkedObjects[0]; + R::store($tester); + $tester = R::findOne('parent'); + asrt($tester->linked->id, $linkedObjects[0]->id); + $tester->linked_id = $linkedObjects[1]->id; + R::store($tester); + asrt($tester->linked->id, $linkedObjects[1]->id); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Quickexport.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Quickexport.php new file mode 100644 index 0000000..c811995 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Quickexport.php @@ -0,0 +1,43 @@ +'bean', 'a' => 1, 'b' => 2, 'c' => 3 ) ) ); + $path = '/tmp/redbeantest.txt'; + R::csv( 'SELECT a,b,c FROM bean', array(), array( 'A', 'B', 'C' ), $path, FALSE ); + $csv = file_get_contents( $path ); + $expected = "A,B,C\n1,2,3"; + asrt( strpos($csv, $expected) !== FALSE, TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Relations.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Relations.php new file mode 100644 index 0000000..861ae34 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Relations.php @@ -0,0 +1,1250 @@ +author = $author; + $book->author->name = 'x'; + $book->author->setMeta( 'tainted', FALSE ); + R::store( $book ); + $author = $author->fresh(); + asrt( isset( $author->name ), FALSE ); + } + + /** + * Tests whether via() applies camelcase-to-snakecase + * conversion. Although most of the time you do not need + * this since via() is meant to remap typical link tables + * to bean - but alas. + * + * @return void + */ + public function testCamelCasingVia() + { + R::nuke(); + $book = R::dispense('book'); + $book->sharedPage[] = R::dispense('page'); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->via('bookPage')->sharedPage ), 1 ); + $book = $book->fresh(); + asrt( count( $book->via('book_page')->sharedPage ), 1 ); + } + + /** + * Test whether we can't add more than one FK. + */ + public function testDuplicateFK() + { + R::nuke(); + list( $book, $page ) = R::dispenseAll( 'book,page' ); + $book->sharedPage[] = $page; + R::store( $page ); + R::store( $book ); + $writer = R::getWriter(); + $added1 = $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $added2 = $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $added = ( $added1 && $added2 ); + asrt( $added, FALSE ); + } + + /** + * Test whether ->all() reloads a list. + * + * @return void + */ + public function testAllPrefix() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->ownPage = R::dispense( 'page', 10 ); + $book->sharedTag = R::dispense( 'tag', 2 ); + $i = 0; + foreach( $book->ownPage as $page ) { + $page->pageno = $i++; + } + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 10 ); + asrt( count( $book->withCondition(' pageno < 5 ')->ownPage ), 5 ); + asrt( count( $book->ownPage ), 5 ); + asrt( count( $book->all()->ownPage ), 10 ); + asrt( count( $book->with(' LIMIT 3 ')->ownPage ), 3 ); + asrt( count( $book->ownPage ), 3 ); + asrt( count( $book->all()->ownPage ), 10 ); + asrt( count( $book->sharedTag ), 2 ); + asrt( count( $book->with( ' LIMIT 1 ' )->sharedTag ), 1 ); + asrt( count( $book->sharedTag ), 1 ); + asrt( count( $book->all()->sharedTag ), 2 ); + } + + /** + * Test Relations and conditions. + * + * @return void + */ + public function testRelationsAndConditions() + { + list( $book1, $book2 ) = R::dispense( 'book', 2 ); + list( $page1, $page2, $page3, $page4 ) = R::dispense( 'page', 4 ); + list( $author1, $author2 ) = R::dispense( 'author', 2 ); + $book1->title = 'a'; + $book2->title = 'b'; + $page1->thename = '1'; + $page2->thename = '2'; + $page3->thename = '3'; + $page3->thename = '4'; + $book1->ownPage = array( $page1, $page2 ); + $book2->ownPage = array( $page3, $page4 ); + $author1->sharedBook = array( $book1, $book2 ); + $author2->sharedBook = array( $book2 ); + R::storeAll( array( $author1, $author2 ) ); + asrt( count( $author1->sharedBook ), 2 ); + asrt( count( $author1->withCondition( ' title = ? ', array( 'a' ) )->sharedBook ), 1 ); + R::store( $author1 ); + asrt( count( $author1->sharedBook ), 2 ); + asrt( count( $author1->withCondition( ' xtitle = ? ', array( 'a' ) )->sharedBook ), 0 ); + R::store( $author1 ); + asrt( count( $author1->sharedBook ), 2 ); + $book1 = R::load( 'book', $book1->id ); + $book2 = $book2->fresh(); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->with( ' LIMIT 1 ' )->ownPage ), 1 ); + $book1 = $book1->fresh(); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->withCondition( ' thename = ? ', array( '1' ) )->ownPage ), 1 ); + } + + /** + * Test filtering relations on links (using columns in the link table). + * + * @return void + */ + public function testSharedLinkCond() + { + testpack( 'Test new shared relations with link conditions' ); + $w = R::getWriter(); + list( $b1, $b2 ) = R::dispense( 'book', 2 ); + $b1->name = 'book1'; + $b2->name = 'book2'; + list( $p1, $p2, $p3 ) = R::dispense( 'page', 3 ); + $p1->text = 'page1'; + $p1->number = 3; + $p2->text = 'page2'; + $p3->text = 'page3'; + $b1->link( 'book_page', array( 'order' => 1 ) )->page = $p1; + $b1->link( 'bookPage', array( 'order' => 2 ) )->page = $p2; + $b2->link( 'book_page', array( 'order' => 1 ) )->page = $p3; + $b2->link( 'bookPage', array( 'order' => 2 ) )->page = $p2; + $b2->link( 'book_page', array( 'order' => 3 ) )->page = $p1; + R::storeAll( array( $b1, $b2 ) ); + $b1 = R::load( 'book', $b1->id ); + $b2 = R::load( 'book', $b2->id ); + $pages = $b1->withCondition( ' book_page.' . $w->esc( 'order' ) . ' = 2 ' )->sharedPage; + $page = reset( $pages ); + asrt( $page->text, 'page2' ); + $pages = $b2->withCondition( ' ' . $w->esc( 'order' ) . ' = 3 ' )->sharedPage; + $page = reset( $pages ); + asrt( $page->text, 'page1' ); + $b1 = R::load( 'book', $b1->id ); + $b2 = R::load( 'book', $b2->id ); + $pages = $b1->withCondition( ' book_page.' . $w->esc( 'order' ) . ' < 3 AND page.number = 3' )->sharedPage; + $page = reset( $pages ); + asrt( $page->text, 'page1' ); + $pages = $b2->withCondition( ' ' . $w->esc( 'order' ) . ' > 1 ORDER BY book_page.' . $w->esc( 'order' ) . ' ASC ' )->sharedPage; + $page = array_shift( $pages ); + asrt( $page->text, 'page2' ); + $page = array_shift( $pages ); + asrt( $page->text, 'page1' ); + testpack( 'Test new shared relations and cache' ); + + /** + * why does this not destroy cache in psql? + * ah: An error occurred: SQLSTATE[42703]: Undefined column: 7 + * ERROR: column "page" of relation "page" does not exist + */ + R::exec( 'UPDATE page SET ' . $w->esc( 'number' ) . ' = 1 ' ); + R::getWriter()->setUseCache( TRUE ); + $p1 = R::load( 'page', (int) $p1->id ); + // Someone else changes the records. Cache remains. + R::exec( ' UPDATE page SET ' . $w->esc( 'number' ) . ' = 9 -- keep-cache' ); + $b1 = R::load( 'book', $b1->id ); + $p1 = R::load( 'page', (int) $p1->id ); + // Yupz a stale cache, phantom read! + asrt( (int) $p1->number, 1 ); + $pages = $b1->withCondition( ' book_page.' . $w->esc( 'order' ) . ' = 1 ' )->sharedPage; + $page = reset( $pages ); + // Inconsistent, sad but TRUE, different query -> cache key is different + asrt( (int) $page->number, 9 ); + // However, cache must have been invalidated by this query + $p1 = R::load( 'page', (int) $p1->id ); + // Yes! we're consistent again! -- as if the change just happened later! + asrt( (int) $page->number, 9 ); + // By doing this we keep getting 9 instead of 8 + $b1->fresh()->withCondition( ' book_page.' . $w->esc( 'order' ) . ' = 1 ' )->sharedPage; + // Someone else is busy again... + R::exec( ' UPDATE page SET ' . $w->esc( 'number' ) . ' = 8 -- keep-cache' ); + $b1 = R::load( 'book', $b1->id ); + $pages = $b1->withCondition( ' book_page.' . $w->esc( 'order' ) . ' = 1 ' )->sharedPage; + $page = reset( $pages ); + + /** + * yes! we get 9 instead of 8, why because the cache key has not changed, + * our last query was PAGE-BOOK-RELATION and now we ask for + * PAGE-BOOK-RELATION again. if we would have used just a load page + * query we would have gotten the new value (8).... let's test that! + */ + asrt( (int) $page->number, 9 ); + R::exec( ' UPDATE page SET ' . $w->esc( 'number' ) . ' = 9' ); + $p1 = R::load( 'page', (int) $p1->id ); + asrt( (int) $page->number, 9 ); + // Someone else is busy again... + R::exec( ' UPDATE page SET ' . $w->esc( 'number' ) . ' = 8 -- keep-cache' ); + $b1 = R::load( 'book', $b1->id ); + $pages = $b1->withCondition( ' book_page.' . $w->esc( 'order' ) . ' = 1 ' )->sharedPage; + $page = reset( $pages ); + // Yes, keep-cache wont help, cache key changed! + asrt( (int) $page->number, 8 ); + R::getWriter()->setUseCache( FALSE ); + } + + /** + * Test related count using via(). + * + * @return void + */ + public function testRelatedCountVia() + { + testpack( 'Test relatedCount with via()' ); + $shop = R::dispense( 'shop' ); + $shop->ownRelation = R::dispense( 'relation', 13 ); + foreach ( $shop->ownRelation as $relation ) { + $relation->shop = $shop; + $relation->customer = R::dispense( 'customer' ); + } + R::store( $shop ); + $shop = $shop->fresh(); + asrt( $shop->via( 'relation' )->countShared( 'customer' ), 13 ); + } + + /** + * Test counting and aliasing. + * + * @return void + */ + public function testCountingAndAliasing() + { + $book = R::dispense( 'book' ); + $book->ownPage = R::dispense( 'page', 10 ); + $book2 = R::dispense( 'book' ); + $book2->ownPage = R::dispense( 'page', 4 ); + list( $Bill, $James, $Andy ) = R::dispense( 'person', 3 ); + $book->author = $Bill; + $book->coAuthor = $James; + $book2->author = $Bill; + $book2->coAuthor = $Andy; + $book->price = 25; + $book2->price = 50; + $notes = R::dispense( 'note', 10 ); + $book->sharedNote = array( $notes[0], $notes[1], $notes[2] ); + $book2->sharedNote = array( $notes[3], $notes[4], $notes[1], $notes[0] ); + $books = R::dispense( 'book', 5 ); + $books[2]->title = 'boe'; + $book->sharedBook = array( $books[0], $books[1] ); + $book2->sharedBook = array( $books[0], $books[2], $books[4] ); + R::storeAll( array( $book, $book2 ) ); + asrt( $book->countOwn( 'page' ), 10 ); + asrt( $book->withCondition( ' id < 5 ' )->countOwn( 'page' ), 4 ); + asrt( $Bill->alias( 'author' )->countOwn( 'book' ), 2 ); + asrt( $Andy->alias( 'coAuthor' )->countOwn( 'book' ), 1 ); + asrt( $James->alias( 'coAuthor' )->countOwn( 'book' ), 1 ); + asrt( $Bill->alias( 'author' )->countOwn( 'book' ), 2 ); + asrt( $book->countShared( 'note' ), 3 ); + asrt( $book2->countShared( 'note' ), 4 ); + asrt( $book2->countShared( 'book' ), 3 ); + $book2 = $book2->fresh(); + asrt( $book2->withCondition( ' title = ? ', array( 'boe' ) )->countShared( 'book' ), 1 ); + } + + /** + * Test via. + * + * @return void + */ + public function testVia() + { + testpack( 'Test via()' ); + $d = R::dispense( 'doctor' )->setAttr( 'name', 'd1' ); + $p = R::dispense( 'patient' )->setAttr( 'name', 'p1' ); + $d->via( 'consult' )->sharedPatient[] = $p; + R::store( $d ); + $d = R::load( 'doctor', $d->id ); + asrt( count( $d->sharedPatient ), 1 ); + asrt( in_array( 'consult', R::getWriter()->getTables() ), TRUE ); + } + + /** + * Issue #348 via() should reload shared list + * + * @return void + */ + public function testIssue348() + { + $product = R::dispense( 'product' ); + $product->name = 'test'; + $color = R::dispense( 'color' ); + $color->name = 'cname'; + $color->code = 'ccode'; + R::store( $product ); + R::store( $color ); + $product->link( 'productColor', array( + 'stock' => 1, + 'position' => 0 + ) )->color = $color; + R::store( $product ); + asrt( count( $product->sharedColor ), 0 ); + asrt( count( $product->via( 'product_color' )->sharedColor ), 1 ); + asrt( count( $product->sharedColor ), 1 ); + R::renameAssociation( 'color_product', NULL ); + } + + /** + * Test creation of link table. + * + * @return void + */ + public function testCreationOfLinkTable() + { + asrt( in_array( 'consult', R::getWriter()->getTables() ), FALSE ); + $d = R::dispense( 'doctor' )->setAttr( 'name', 'd1' ); + $p = R::dispense( 'patient' )->setAttr( 'name', 'p1' ); + $d->sharedPatient[] = $p; + R::store($d); + asrt( in_array( 'consult', R::getWriter()->getTables() ), TRUE ); + } + + /** + * Fast track link block code should not affect self-referential N-M relations. + * + * @return void + */ + public function testFastTrackRelations() + { + testpack( 'Test fast-track linkBlock exceptions' ); + list( $donald, $mickey, $goofy, $pluto ) = R::dispense( 'friend', 4 ); + $donald->name = 'D'; + $mickey->name = 'M'; + $goofy->name = 'G'; + $pluto->name = 'P'; + $donald->sharedFriend = array( $mickey, $goofy ); + $mickey->sharedFriend = array( $pluto, $goofy ); + $mickey->sharedBook = array( R::dispense( 'book' ) ); + R::storeAll( array( $mickey, $donald, $goofy, $pluto ) ); + $donald = R::load( 'friend', $donald->id ); + $mickey = R::load( 'friend', $mickey->id ); + $goofy = R::load( 'friend', $goofy->id ); + $pluto = R::load( 'friend', $pluto->id ); + $names = implode( ',', R::gatherLabels( $donald->sharedFriend ) ); + asrt( $names, 'G,M' ); + $names = implode( ',', R::gatherLabels( $goofy->sharedFriend ) ); + asrt( $names, 'D,M' ); + $names = implode( ',', R::gatherLabels( $mickey->sharedFriend ) ); + asrt( $names, 'D,G,P' ); + $names = implode( ',', R::gatherLabels( $pluto->sharedFriend ) ); + asrt( $names, 'M' ); + // Now in combination with with() conditions... + $donald = R::load( 'friend', $donald->id ); + $names = implode( ',', R::gatherLabels( $donald->withCondition( ' name = ? ', array( 'M' ) )->sharedFriend ) ); + asrt( $names, 'M' ); + // Now in combination with with() conditions... + $donald = R::load( 'friend', $donald->id ); + $names = implode( ',', R::gatherLabels( $donald->with( ' ORDER BY name ' )->sharedFriend ) ); + asrt( $names, 'G,M' ); + // Now counting + $goofy = R::load( 'friend', $goofy->id ); + asrt( (int) $goofy->countShared( 'friend' ), 2 ); + asrt( (int) $donald->countShared( 'friend' ), 2 ); + asrt( (int) $mickey->countShared( 'friend' ), 3 ); + asrt( (int) $pluto->countShared( 'friend' ), 1 ); + } + + /** + * Test list beautifications. + * + * @return void + */ + public function testListBeautifications() + { + testpack( 'Test list beautifications' ); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' )->setAttr( 'name', 'a' ); + $book->sharedPage[] = $page; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + $p = reset( $book->ownBookPage ); + asrt( $p->page->name, 'a' ); + $bean = R::dispense( 'bean' ); + $bean->sharedAclRole[] = R::dispense( 'role' )->setAttr( 'name', 'x' ); + R::store( $bean ); + asrt( R::count( 'role' ), 1 ); + $aclrole = R::getRedBean()->dispense( 'acl_role' ); + $aclrole->name = 'role'; + $bean = R::dispense( 'bean' ); + $bean->sharedAclRole[] = $aclrole; + R::store( $bean ); + asrt( count( $bean->sharedAclRole ), 1 ); + } + + /** + * Test list add and delete. + * + * @return void + */ + public function testListAddDelete() + { + testpack( 'Test list add/delete scenarios.' ); + R::nuke(); + $b = R::dispense( 'book' ); + $p = R::dispense( 'page' ); + $b->title = 'a'; + $p->name = 'b'; + $b->xownPage[] = $p; + R::store( $b ); + $b->xownPage = array(); + R::store( $b ); + asrt( R::count( 'page' ), 0 ); + $p = R::dispense( 'page' ); + $z = R::dispense( 'paper' ); + $z->xownPage[] = $p; + R::store( $z ); + asrt( R::count( 'page' ), 1 ); + $z->xownPage = array(); + R::store( $z ); + asrt( R::count( 'page' ), 0 ); + $i = R::dispense( 'magazine' ); + $i->ownPage[] = R::dispense( 'page' ); + R::store( $i ); + asrt( R::count( 'page' ), 1 ); + $i->ownPage = array(); + R::store( $i ); + asrt( R::count( 'page' ), 1 ); + } + + /** + * Test basic and complex common usage scenarios for + * relations and associations. + * + * @return void + */ + public function testScenarios() + { + list( $q1, $q2 ) = R::dispense( 'quote', 2 ); + list( $pic1, $pic2 ) = R::dispense( 'picture', 2 ); + list( $book, $book2, $book3 ) = R::dispense( 'book', 4 ); + list( $topic1, $topic2, $topic3, $topic4, $topic5 ) = R::dispense( 'topic', 5 ); + list( $page1, $page2, $page3, $page4, $page5, $page6, $page7 ) = R::dispense( 'page', 7 ); + $q1->text = 'lorem'; + $q2->text = 'ipsum'; + $book->title = 'abc'; + $book2->title = 'def'; + $book3->title = 'ghi'; + $page1->title = 'pagina1'; + $page2->title = 'pagina2'; + $page3->title = 'pagina3'; + $page4->title = 'pagina4'; + $page5->title = 'pagina5'; + $page6->title = 'cover1'; + $page7->title = 'cover2'; + $topic1->name = 'holiday'; + $topic2->name = 'cooking'; + $topic3->name = 'gardening'; + $topic4->name = 'computing'; + $topic5->name = 'christmas'; + // Add one page to the book + $book->ownPage[] = $page1; + $id = R::store( $book ); + asrt( count( $book->ownPage ), 1 ); + asrt( reset( $book->ownPage )->getMeta( 'type' ), 'page' ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 1 ); + asrt( reset( $book->ownPage )->getMeta( 'type' ), 'page' ); + // Performing an own addition + $book->ownPage[] = $page2; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 2 ); + // Performing a deletion + $book = R::load( 'book', $id ); + unset( $book->ownPage[1] ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 1 ); + asrt( reset( $book->ownPage )->getMeta( 'type' ), 'page' ); + asrt( R::count( 'page' ), 2 ); //still exists + asrt( reset( $book->ownPage )->id, '2' ); + // Doing a change in one of the owned items + $book->ownPage[2]->title = 'page II'; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( reset( $book->ownPage )->title, 'page II' ); + // Change by reference now... don't copy! + $refToPage2 = $book->ownPage[2]; + $refToPage2->title = 'page II b'; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( reset( $book->ownPage )->title, 'page II b' ); + // Doing all actions combined + $book->ownPage[] = $page3; + R::store( $book ); + $book = R::load( 'book', $id ); + unset( $book->ownPage[2] ); + // And test custom key + $book->ownPage['customkey'] = $page4; + $book->ownPage[3]->title = "THIRD"; + R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 2 ); + $p4 = $book->ownPage[4]; + $p3 = $book->ownPage[3]; + asrt( $p4->title, 'pagina4' ); + asrt( $p3->title, 'THIRD' ); + // Test replacing an element + $book = R::load( 'book', $id ); + $book->ownPage[4] = $page5; + R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 2 ); + $p5 = $book->ownPage[5]; + asrt( $p5->title, 'pagina5' ); + // Other way around - single bean + asrt( $p5->book->title, 'abc' ); + asrt( R::load( 'page', 5 )->book->title, 'abc' ); + asrt( R::load( 'page', 3 )->book->title, 'abc' ); + // Add the other way around - single bean + $page1->id = 0; + $page1->book = $book2; + $page1 = R::load( 'page', R::store( $page1 ) ); + asrt( $page1->book->title, 'def' ); + $b2 = R::load( 'book', $id ); + asrt( count( $b2->ownPage ), 2 ); + // Remove the other way around - single bean + unset( $page1->book ); + R::store( $page1 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 1 ); //does not work + $page1->book = NULL; + R::store( $page1 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 0 ); //works + // Re-add the page + $b2->ownPage[] = $page1; + R::store( $b2 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 1 ); + // Different, less elegant way to remove + $page1 = reset( $b2->ownPage ); + $page1->book_id = NULL; + R::store( $page1 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 0 ); + // Re-add the page + $b2->ownPage[] = $page1; + R::store( $b2 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 1 ); + // Another less elegant way to remove + $page1->book = NULL; + R::store( $page1 ); + $cols = R::getColumns( 'page' ); + asrt( isset( $cols['book'] ), FALSE ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 0 ); + // Re-add the page + $b2->ownPage[] = $page1; + R::store( $b2 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 1 ); + // Another less elegant... just plain ugly... way to remove + $page1->book = FALSE; + R::store( $page1 ); + $cols = R::getColumns( 'page' ); + asrt( isset( $cols['book'] ), FALSE ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 0 ); + // Re-add the page + $b2->ownPage[] = $page1; + R::store( $b2 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 1 ); + // You are not allowed to re-use the field for something else + foreach ( + array( + 1, -2.1, array(), + TRUE, 'NULL', new \stdClass, + 'just a string', array( 'a' => 1 ), 0 + ) as $value + ) { + try { + $page1->book = $value; + fail(); + } catch ( RedException $e ) { + pass(); + } + } + // Test fk, not allowed to set to 0 + $page1 = reset( $b2->ownPage ); + $page1->book_id = 0; + // Even uglier way, but still needs to work + $page1 = reset( $b2->ownPage ); + $page1->book_id = NULL; + R::store( $b2 ); + $b2 = R::load( 'book', $book2->id ); + asrt( count( $b2->ownPage ), 0 ); + // Test shared items + $book = R::load( 'book', $id ); + $book->sharedTopic[] = $topic1; + $id = R::store( $book ); + // Add an item + asrt( count( $book->sharedTopic ), 1 ); + asrt( reset( $book->sharedTopic )->name, 'holiday' ); + $book = R::load( 'book', $id ); + asrt( count( $book->sharedTopic ), 1 ); + asrt( reset( $book->sharedTopic )->name, 'holiday' ); + // Add another item + $book->sharedTopic[] = $topic2; + $id = R::store( $book ); + $tidx = R::store( R::dispense( 'topic' ) ); + $book = R::load( 'book', $id ); + asrt( count( $book->sharedTopic ), 2 ); + $t1 = $book->sharedTopic[1]; + $t2 = $book->sharedTopic[2]; + asrt( $t1->name, 'holiday' ); + asrt( $t2->name, 'cooking' ); + // Remove an item + unset( $book->sharedTopic[2] ); + asrt( count( $book->sharedTopic ), 1 ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->sharedTopic ), 1 ); + asrt( reset( $book->sharedTopic )->name, 'holiday' ); + // Add and change + $book->sharedTopic[] = $topic3; + $book->sharedTopic[1]->name = 'tropics'; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->sharedTopic ), 2 ); + asrt( $book->sharedTopic[1]->name, 'tropics' ); + testids( $book->sharedTopic ); + R::trash( R::load( 'topic', $tidx ) ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + // Delete without save + unset( $book->sharedTopic[1] ); + $book = R::load( 'book', $id ); + asrt( count( $book->sharedTopic ), 2 ); + $book = R::load( 'book', $id ); + // Delete without init + asrt( ( R::count( 'topic' ) ), 3 ); + unset( $book->sharedTopic[1] ); + $id = R::store( $book ); + asrt( ( R::count( 'topic' ) ), 3 ); + asrt( count( $book->sharedTopic ), 1 ); + asrt( count( $book2->sharedTopic ), 0 ); + // Add same topic to other book + $book2->sharedTopic[] = $topic3; + asrt( count( $book2->sharedTopic ), 1 ); + $id2 = R::store( $book2 ); + asrt( count( $book2->sharedTopic ), 1 ); + $book2 = R::load( 'book', $id2 ); + asrt( count( $book2->sharedTopic ), 1 ); + // Get books for topic + asrt( $topic3->countShared('book'), 2 ); + $t3 = R::load( 'topic', $topic3->id ); + asrt( count( $t3->sharedBook ), 2 ); + // Nuke an own-array, replace entire array at once without getting first + $page2->id = 0; + $page2->title = 'yet another page 2'; + $page4->id = 0; + $page4->title = 'yet another page 4'; + $book = R::load( 'book', $id ); + $book->ownPage = array( $page2, $page4 ); + R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->ownPage ), 2 ); + asrt( reset( $book->ownPage )->title, 'yet another page 2' ); + asrt( end( $book->ownPage )->title, 'yet another page 4' ); + testids( $book->ownPage ); + // Test with alias format + $book3->cover = $page6; + $idb3 = R::store( $book3 ); + $book3 = R::load( 'book', $idb3 ); + $justACover = $book3->fetchAs( 'page' )->cover; + asrt( ( $book3->cover instanceof OODBBean ), TRUE ); + asrt( $justACover->title, 'cover1' ); + // No page property + asrt( isset( $book3->page ), FALSE ); + // Test doubling and other side effects ... should not occur.. + $book3->sharedTopic = array( $topic1, $topic2 ); + $book3 = R::load( 'book', R::store( $book3 ) ); + $book3->sharedTopic = array(); + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->sharedTopic ), 0 ); + $book3->sharedTopic[] = $topic1; + $book3 = R::load( 'book', R::store( $book3 ) ); + // Added only one, not more? + asrt( count( $book3->sharedTopic ), 1 ); + asrt( intval( R::getCell( "select count(*) from book_topic where book_id = $idb3" ) ), 1 ); + // Add the same + $book3->sharedTopic[] = $topic1; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->sharedTopic ), 1 ); + asrt( intval( R::getCell( "select count(*) from book_topic where book_id = $idb3" ) ), 1 ); + $book3->sharedTopic['differentkey'] = $topic1; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->sharedTopic ), 1 ); + asrt( intval( R::getCell( "select count(*) from book_topic where book_id = $idb3" ) ), 1 ); + // Ugly assign, auto array generation + $book3->ownPage[] = $page1; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->ownPage ), 1 ); + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 1 ); + $book3 = R::load( 'book', $idb3 ); + $book3->ownPage = array(); + // No change until saved + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 1 ); + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 0 ); + asrt( count( $book3->ownPage ), 0 ); + $book3 = R::load( 'book', $idb3 ); + /** + * Why do I need to do this ---> why does trash() not set id -> 0? + * Because you unset() so trash is done on origin not bean + */ + $page1->id = 0; + $page2->id = 0; + $page3->id = 0; + $book3->ownPage[] = $page1; + $book3->ownPage[] = $page2; + $book3->ownPage[] = $page3; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 3 ); + asrt( count( $book3->ownPage ), 3 ); + unset( $book3->ownPage[$page2->id] ); + $book3->ownPage[] = $page3; + $book3->ownPage['try_to_trick_ya'] = $page3; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 2 ); + asrt( count( $book3->ownPage ), 2 ); + // Delete and re-add + $book3 = R::load( 'book', $idb3 ); + unset( $book3->ownPage[10] ); + $book3->ownPage[] = $page1; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->ownPage ), 2 ); + $book3 = R::load( 'book', $idb3 ); + unset( $book3->sharedTopic[1] ); + $book3->sharedTopic[] = $topic1; + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->sharedTopic ), 1 ); + // Test performance + $logger = R::debug( TRUE, 1 ); + $book = R::load( 'book', 1 ); + $book->sharedTopic = array(); + R::store( $book ); + // No more than 1 update + asrt( count( $logger->grep( 'UPDATE' ) ), 1 ); + $book = R::load( 'book', 1 ); + $logger->clear(); + print_r( $book->sharedTopic, 1 ); + // No more than 1 select + asrt( count( $logger->grep( 'SELECT' ) ), 1 ); + $logger->clear(); + $book->sharedTopic[] = $topic1; + $book->sharedTopic[] = $topic2; + asrt( count( $logger->grep( 'SELECT' ) ), 0 ); + R::store( $book ); + $book->sharedTopic[] = $topic3; + // Now do NOT clear all and then add one, just add the one + $logger->clear(); + R::store( $book ); + $book = R::load( 'book', 1 ); + asrt( count( $book->sharedTopic ), 3 ); + // No deletes + asrt( count( $logger->grep( "DELETE FROM" ) ), 0 ); + $book->sharedTopic['a'] = $topic3; + unset( $book->sharedTopic['a'] ); + R::store( $book ); + $book = R::load( 'book', 1 ); + asrt( count( $book->sharedTopic ), 3 ); + // No deletes + asrt( count( $logger->grep( "DELETE FROM" ) ), 0 ); + $book->ownPage = array(); + R::store( $book ); + asrt( count( $book->ownPage ), 0 ); + $book->ownPage[] = $page1; + $book->ownPage['a'] = $page2; + asrt( count( $book->ownPage ), 2 ); + R::store( $book ); + unset( $book->ownPage['a'] ); + asrt( count( $book->ownPage ), 2 ); + unset( $book->ownPage[11] ); + R::store( $book ); + $book = R::load( 'book', 1 ); + asrt( count( $book->ownPage ), 1 ); + $aPage = $book->ownPage[10]; + unset( $book->ownPage[10] ); + $aPage->title .= ' changed '; + $book->ownPage['anotherPage'] = $aPage; + $logger->clear(); + R::store( $book ); + $book = R::load( 'book', 1 ); + asrt( count( $book->ownPage ), 1 ); + $ap = reset( $book->ownPage ); + asrt( $ap->title, "pagina1 changed " ); + // Fix udiff instead of diff + $book3->ownPage = array( $page3, $page1 ); + $i = R::store( $book3 ); + $book3 = R::load( 'book', $i ); + asrt( intval( R::getCell( "select count(*) from page where book_id = $idb3 " ) ), 2 ); + asrt( count( $book3->ownPage ), 2 ); + $pic1->name = 'aaa'; + $pic2->name = 'bbb'; + R::store( $pic1 ); + R::store( $q1 ); + $book3->ownPicture[] = $pic1; + $book3->ownQuote[] = $q1; + $book3 = R::load( 'book', R::store( $book3 ) ); + // two own-arrays -->forgot array_merge + asrt( count( $book3->ownPicture ), 1 ); + asrt( count( $book3->ownQuote ), 1 ); + asrt( count( $book3->ownPage ), 2 ); + $book3 = R::load( 'book', R::store( $book3 ) ); + unset( $book3->ownPicture[1] ); + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->ownPicture ), 0 ); + asrt( count( $book3->ownQuote ), 1 ); + asrt( count( $book3->ownPage ), 2 ); + $book3 = R::load( 'book', R::store( $book3 ) ); + $NOTE = 0; + $quotes = R::dispense( 'quote', 10 ); + foreach ( $quotes as &$justSomeQuote ) { + $justSomeQuote->note = 'note' . ( ++$NOTE ); + } + $pictures = R::dispense( 'picture', 10 ); + foreach ( $pictures as &$justSomePic ) { + $justSomePic->note = 'note' . ( ++$NOTE ); + } + $topics = R::dispense( 'topic', 10 ); + foreach ( $topics as &$justSomeTopic ) { + $justSomeTopic->note = 'note' . ( ++$NOTE ); + } + for ( $j = 0; $j < 10; $j++ ) { + // Do several mutations + for ( $x = 0; $x < rand( 1, 20 ); $x++ ) { + modgr( $book3, $quotes, $pictures, $topics ); + } + $qbefore = count( $book3->ownQuote ); + $pbefore = count( $book3->ownPicture ); + $tbefore = count( $book3->sharedTopic ); + $qjson = json_encode( $book->ownQuote ); + $pjson = json_encode( $book->ownPicture ); + $tjson = json_encode( $book->sharedTopic ); + $book3 = R::load( 'book', R::store( $book3 ) ); + asrt( count( $book3->ownQuote ), $qbefore ); + asrt( count( $book3->ownPicture ), $pbefore ); + asrt( count( $book3->sharedTopic ), $tbefore ); + asrt( json_encode( $book->ownQuote ), $qjson ); + asrt( json_encode( $book->ownPicture ), $pjson ); + asrt( json_encode( $book->sharedTopic ), $tjson ); + testids( $book->ownQuote ); + testids( $book->ownPicture ); + testids( $book->sharedTopic ); + } + } + + /** + * Test parent bean relations. + * + * @return void + */ + public function testParentBean() + { + $village = R::dispense( 'village' ); + $village->name = 'village'; + $home = R::dispense( 'building' ); + $home->village = $village; + $id = R::store( $home ); + $home = R::load( 'building', $id ); + asrt( $home->village->name, 'village' ); + asrt( R::count( 'village' ), 1 ); + asrt( R::count( 'building' ), 1 ); + R::trash( $home ); + pass(); + asrt( R::count( 'village' ), 1 ); + asrt( R::count( 'building' ), 0 ); + } + + /** + * test N-M relations through intermediate beans + * + * @return void + */ + public function testNMRelationsIntermediate() + { + list( $mrA, $mrB, $mrC ) = R::dispense( 'person', 3 ); + list( $projA, $projB, $projC ) = R::dispense( 'project', 3 ); + $projA->title = 'A'; + $projB->title = 'B'; + $projC->title = 'C'; + $participant = R::dispense( 'participant' ); + $projA->link( 'participant', array( 'role' => 'manager' ) )->person = $mrA; + $projA->link( $participant->setAttr( 'role', 'developer' ) )->person = $mrB; + $projB->link( R::dispense( 'participant' )->setAttr( 'role', 'developer' ) )->person = $mrB; + $projB->link( 'participant', '{"role":"helpdesk"}' )->person = $mrC; + $projC->link( 'participant', '{"role":"sales"}' )->person = $mrC; + R::storeAll( array( $projA, $projB, $projC ) ); + $a = R::findOne( 'project', ' title = ? ', array( 'A' ) ); + $b = R::findOne( 'project', ' title = ? ', array( 'B' ) ); + $c = R::findOne( 'project', ' title = ? ', array( 'C' ) ); + asrt( count( $a->ownParticipant ), 2 ); + asrt( count( $b->ownParticipant ), 2 ); + asrt( count( $c->ownParticipant ), 1 ); + $managers = $developers = 0; + foreach ( $a->ownParticipant as $p ) { + if ( $p->role === 'manager' ) { + $managers++; + } + if ( $p->role === 'developer' ) { + $developers++; + } + } + $p = reset( $a->ownParticipant ); + asrt( $p->person->getMeta( 'type' ), 'person' ); + asrt( ( $p->person->id > 0 ), TRUE ); + asrt( $managers, 1 ); + asrt( $developers, 1 ); + asrt( (int) R::count( 'participant' ), 5 ); + asrt( (int) R::count( 'person' ), 3 ); + } + + /** + * test emulation of sharedList through intermediate beans + * + * @return void + */ + public function testSharedListIntermediate() + { + list( $v1, $v2, $v3 ) = R::dispense( 'village', 3 ); + list( $a1, $a2, $a3 ) = R::dispense( 'army', 3 ); + $a1->name = 'one'; + $a2->name = 'two'; + $a3->name = 'three'; + $v1->name = 'Ville 1'; + $v2->name = 'Ville 2'; + $v3->name = 'Ville 3'; + $v1->link( 'armyVillage' )->army = $a3; + $v2->link( 'army_village' )->army = $a2; + $v3->link( 'armyVillage' )->army = $a1; + $a2->link( 'army_village' )->village = $v1; + $id1 = R::store( $v1 ); + $id2 = R::store( $v2 ); + $id3 = R::store( $v3 ); + $village1 = R::load( 'village', $id1 ); + $village2 = R::load( 'village', $id2 ); + $village3 = R::load( 'village', $id3 ); + asrt( count( $village1->sharedArmy ), 2 ); + asrt( count( $village2->sharedArmy ), 1 ); + asrt( count( $village3->sharedArmy ), 1 ); + } + + /** + * test emulation via association renaming + * + * @return void + */ + public function testAssociationRenaming() + { + list( $p1, $p2, $p3 ) = R::dispense( 'painting', 3 ); + list( $m1, $m2, $m3 ) = R::dispense( 'museum', 3 ); + $p1->name = 'painting1'; + $p2->name = 'painting2'; + $p3->name = 'painting3'; + $m1->thename = 'a'; + $m2->thename = 'b'; + $m3->thename = 'c'; + R::renameAssociation( 'museum_painting', 'exhibited' ); + // Also test array syntax + R::renameAssociation( array( 'museum_museum' => 'center' ) ); + $m1->link( 'center', array( 'name' => 'History Center' ) )->museum2 = $m2; + $m1->link( 'exhibited', '{"from":"2014-02-01","til":"2014-07-02"}' )->painting = $p3; + $m2->link( 'exhibited', '{"from":"2014-07-03","til":"2014-10-02"}' )->painting = $p3; + $m3->link( 'exhibited', '{"from":"2014-02-01","til":"2014-07-02"}' )->painting = $p1; + $m2->link( 'exhibited', '{"from":"2014-02-01","til":"2014-07-02"}' )->painting = $p2; + R::storeAll( array( $m1, $m2, $m3 ) ); + list( $m1, $m2, $m3 ) = array_values( R::findAll( 'museum', ' ORDER BY thename ASC' ) ); + asrt( count( $m1->sharedMuseum ), 1 ); + asrt( count( $m1->sharedPainting ), 1 ); + asrt( count( $m2->sharedPainting ), 2 ); + asrt( count( $m3->sharedPainting ), 1 ); + $p3 = reset( $m1->sharedPainting ); + asrt( count( $p3->ownExhibited ), 2 ); + asrt( count( $m2->ownExhibited ), 2 ); + R::storeAll( array( $m1, $m2, $m3 ) ); + list( $m1, $m2, $m3 ) = array_values( R::findAll( 'museum', ' ORDER BY thename ASC' ) ); + asrt( count( $m1->sharedPainting ), 1 ); + asrt( count( $m2->sharedPainting ), 2 ); + asrt( count( $m3->sharedPainting ), 1 ); + $p3 = reset( $m1->sharedPainting ); + asrt( count( $p3->ownExhibited ), 2 ); + $paintings = $m2->sharedPainting; + foreach ( $paintings as $painting ) { + if ( $painting->name === 'painting2' ) { + pass(); + $paintingX = $painting; + } + } + unset( $m2->sharedPainting[$paintingX->id] ); + R::store( $m2 ); + $m2 = R::load( 'museum', $m2->id ); + asrt( count( $m2->sharedPainting ), 1 ); + $left = reset( $m2->sharedPainting ); + asrt( $left->name, 'painting3' ); + asrt( count( $m2->ownExhibited ), 1 ); + $exhibition = reset( $m2->ownExhibited ); + asrt( $exhibition->from, '2014-07-03' ); + asrt( $exhibition->til, '2014-10-02' ); + } + + /** + * Test don't try to store other things in shared list. + * + * @return void + */ + public function testDontTryToStoreOtherThingsInSharedList() { + + $book = R::dispense( 'book' ); + $book->sharedPage[] = 'nonsense'; + try { + R::store( $book ); + fail(); + } catch( RedException $exception) { + pass(); + } + $book->sharedPageList = R::dispense( 'page', 2 ); + R::store( $book ); + $book->sharedPageList; + R::trash( $book ); + asrt( R::count('page'), 2 ); + } + + /** + * Test whether magic array interface functions like isset() and + * unset work correctly with the x-own-list and the List-suffix. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @return void + */ + public function testWhetherIssetWorksWithXList() + { + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + $book->xownPageList[] = $page; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPage ), TRUE ); + //Test array access + asrt( isset( $book['xownPageList'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + R::store( $book ); + $book = $book->fresh(); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['ownPage'] ), FALSE ); + $book->xownPageList; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + $book = $book->fresh(); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['ownPage'] ), FALSE ); + $book->noLoad()->xownPageList; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + //but empty + asrt( count( $book->ownPageList ), 0 ); + asrt( count( $book->xownPageList ), 0 ); + asrt( count( $book->ownPage ), 0 ); + asrt( count( $book->xownPage ), 0 ); + $book->xownPageList[] = $page; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + unset( $book->xownPageList ); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['ownPage'] ), FALSE ); + $book->xownPageList[] = $page; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + unset( $book->xownPage ); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['ownPage'] ), FALSE ); + $book = $book->fresh(); + asrt( isset( $book->xownPageList ), FALSE ); + asrt( isset( $book->ownPageList ), FALSE ); + asrt( isset( $book->xownPage ), FALSE ); + asrt( isset( $book->ownPage ), FALSE ); + asrt( isset( $book['xownPageList'] ), FALSE ); + asrt( isset( $book['ownPageList'] ), FALSE ); + asrt( isset( $book['xownPage'] ), FALSE ); + asrt( isset( $book['ownPage'] ), FALSE ); + $book->ownPageList; + asrt( isset( $book->xownPageList ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book['xownPageList'] ), TRUE ); + asrt( isset( $book['ownPageList'] ), TRUE ); + asrt( isset( $book['xownPage'] ), TRUE ); + asrt( isset( $book['ownPage'] ), TRUE ); + } + + /** + * Test whether you can still set items starting with 'xown' or + * 'own' not followed by an uppercase character. + * + * @return void + */ + public function testConfusionWithXOwnList() + { + $book = R::dispense( 'book' ); + $book->xownitem = 1; + asrt( isset( $book->xownitem ), TRUE ); + asrt( (int) $book->xownitem, 1 ); + asrt( isset( $book->xownItem ), FALSE ); + asrt( isset( $book->xownItemList ), FALSE ); + $book->ownitem = 1; + asrt( isset( $book->ownitem ), TRUE ); + asrt( (int) $book->ownitem, 1 ); + asrt( isset( $book->ownItemList ), FALSE ); + R::store( $book ); + $book = $book->fresh(); + asrt( isset( $book->xownitem ), TRUE ); + asrt( (int) $book->xownitem, 1 ); + asrt( isset( $book->xownItem ), FALSE ); + asrt( isset( $book->xownItemList ), FALSE ); + asrt( isset( $book->ownitem ), TRUE ); + asrt( (int) $book->ownitem, 1 ); + asrt( isset( $book->ownItemList ), FALSE ); + } + + /** + * Test whether we can determine the mode of a list. + * + * @return void + */ + public function testModeCheckerOfLists() + { + foreach( array( 'ownPage', 'xownPage', 'ownPageList', 'xownPageList' ) as $listName ) { + $book = R::dispense( 'book' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book->ownPageList[] = R::dispense( 'page' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book = R::dispense( 'book' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book->xownPageList[] = R::dispense( 'page' ); + asrt( $book->isListInExclusiveMode( $listName ), TRUE ); + $book = R::dispense( 'book' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book = R::dispense( 'book' ); + asrt( $book->isListInExclusiveMode( $listName ), FALSE ); + $book->xownPage[] = R::dispense( 'page' ); + asrt( $book->isListInExclusiveMode( $listName ), TRUE ); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Tags.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Tags.php new file mode 100644 index 0000000..b1eede7 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Tags.php @@ -0,0 +1,221 @@ +setUseCache( TRUE ); + list( $beer1, $beer2, $beer3 ) = R::dispense( 'beer', 3 ); + $beer1->title = 'b1'; + $beer2->title = 'b2'; + $beer3->title = 'b3'; + R::tag( $beer1, 'stout' ); + R::tag( $beer2, 'porter' ); + R::tag( $beer3, 'lager,popular' ); + $beers = R::tagged( 'beer', 'lager,popular' ); + asrt(count($beers),1); + R::exec( 'DELETE FROM beer_tag WHERE beer_id = ? -- keep-cache', array( $beer3->id ) ); + $beers = R::tagged( 'beer', 'lager,popular' ); + asrt(count($beers),1); + R::getWriter()->setUseCache( FALSE ); + $beers = R::tagged( 'beer', 'lager,popular' ); + asrt(count($beers),0); + } + + /** + * Tests tags with SQL. + * + * @return void + */ + public function testTagsWithSQL() + { + R::nuke(); + list( $m1, $m2, $m3 ) = R::dispense( 'movie', 3 ); + $m1->title = 'Frankenstein'; + $m2->title = 'Fall of the House Usher'; + $m3->title = 'Sleepy Hollow'; + R::tag($m1, 'horror,gothic'); + R::tag($m2, 'horror,gothic,short'); + R::tag($m3, 'horror,legend'); + asrt( count( R::tagged( 'movie', 'horror' ) ), 3); + asrt( count( R::tagged( 'movie', 'horror', ' LIMIT 2' ) ), 2); + asrt( count( R::tagged( 'movie', 'horror', ' LIMIT ?', array( 2 ) ) ), 2); + asrt( count( R::tagged( 'movie', 'horror', ' ORDER BY movie.title DESC LIMIT ?', array( 2 ) ) ), 2); + asrt( count( R::tagged( 'movie', 'horror,gothic', ' ORDER BY movie.title DESC LIMIT ?', array( 1 ) ) ), 1); + asrt( count( R::tagged( 'movie', 'horror,gothic') ), 3 ); + asrt( count( R::taggedAll( 'movie', 'horror,gothic') ), 2 ); + asrt( R::countTaggedAll( 'movie', 'horror,gothic'), 2 ); + asrt( count( R::tagged( 'movie', 'horror,gothic', ' LIMIT ? ', array( 2 ) ) ), 2 ); + asrt( ( R::countTagged( 'movie', 'horror,gothic', ' LIMIT ? ', array( 2 ) ) ), 2 ); + asrt( count( R::taggedAll( 'movie', 'horror,gothic', ' LIMIT ? ', array( 2 ) ) ), 2 ); + asrt( R::countTaggedAll( 'movie', 'horror,gothic', ' LIMIT ? ', array( 2 ) ), 2 ); + asrt( count( R::tagged( 'movie', 'horror,gothic', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( ( R::countTagged( 'movie', 'horror,gothic', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( count( R::taggedAll( 'movie', 'horror,gothic', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'horror,gothic', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( count( R::tagged( 'movie', 'horror,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( ( R::countTagged( 'movie', 'horror,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( count( R::taggedAll( 'movie', 'horror,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'horror,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( count( R::tagged( 'movie', 'gothic,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( ( R::countTagged( 'movie', 'gothic,legend', ' LIMIT ? ', array( 1 ) ) ), 1 ); + asrt( count( R::taggedAll( 'movie', 'gothic,legend', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', 'gothic,legend', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( count( R::tagged( 'movie', 'romance', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( ( R::countTagged( 'movie', 'romance', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( count( R::taggedAll( 'movie', 'romance', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', 'romance', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( count( R::tagged( 'movie', 'romance,xmas', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( ( R::countTagged( 'movie', 'romance,xmas', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( count( R::taggedAll( 'movie', 'romance,xmas', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', 'romance,xmas', ' LIMIT ? ', array( 1 ) ) ), 0 ); + asrt( count( R::tagged( 'movie', 'gothic,short', ' LIMIT ? ', array( 4 ) ) ), 2 ); + asrt( ( R::countTagged( 'movie', 'gothic,short', ' LIMIT ? ', array( 4 ) ) ), 2 ); + asrt( count( R::taggedAll( 'movie', 'gothic,short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'gothic,short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( count( R::tagged( 'movie', 'gothic,short', ' LIMIT 4 ' ) ), 2 ); + asrt( ( R::countTagged( 'movie', 'gothic,short', ' LIMIT 4 ' ) ), 2 ); + asrt( count( R::taggedAll( 'movie', 'gothic,short', ' LIMIT 4 ' ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'gothic,short', ' LIMIT 4 ' ) ), 1 ); + asrt( count( R::tagged( 'movie', 'gothic,short', ' ORDER BY movie.id DESC LIMIT 4 ' ) ), 2 ); + asrt( ( R::countTagged( 'movie', 'gothic,short', ' ORDER BY movie.id DESC LIMIT 4 ' ) ), 2 ); + asrt( count( R::taggedAll( 'movie', 'gothic,short', ' ORDER BY movie.id DESC LIMIT 4 ' ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'gothic,short', ' ORDER BY movie.id DESC LIMIT 4 ' ) ), 1 ); + asrt( count( R::tagged( 'movie', 'short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( ( R::countTagged( 'movie', 'short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( count( R::taggedAll( 'movie', 'short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( ( R::countTaggedAll( 'movie', 'short', ' LIMIT ? ', array( 4 ) ) ), 1 ); + asrt( count( R::tagged( 'movie', '', ' LIMIT ? ', array( 4 ) ) ), 0 ); + asrt( ( R::countTagged( 'movie', '', ' LIMIT ? ', array( 4 ) ) ), 0 ); + asrt( count( R::taggedAll( 'movie', '', ' LIMIT ? ', array( 4 ) ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', '', ' LIMIT ? ', array( 4 ) ) ), 0 ); + asrt( count( R::tagged( 'movie', '', ' LIMIT 4 ' ) ), 0 ); + asrt( ( R::countTagged( 'movie', '', ' LIMIT 4 ' ) ), 0 ); + asrt( count( R::taggedAll( 'movie', '', ' LIMIT 4 ' ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', '', ' LIMIT 4 ' ) ), 0 ); + asrt( count( R::tagged( 'movie', '', '' ) ), 0 ); + asrt( ( R::countTagged( 'movie', '', '' ) ), 0 ); + asrt( count( R::taggedAll( 'movie', '', '' ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', '', '' ) ), 0 ); + asrt( count( R::tagged( 'movie', '' ) ), 0 ); + asrt( ( R::countTagged( 'movie', '' ) ), 0 ); + asrt( count( R::taggedAll( 'movie', '' ) ), 0 ); + asrt( ( R::countTaggedAll( 'movie', '' ) ), 0 ); + } + + /** + * Some basic tests. + * + * @return void + */ + public function testTags() + { + list( $c, $d, $e, $f ) = R::dispense( 'coffee', 4 ); + R::tag( $c, 'strong,black' ); + R::tag( $d, 'black' ); + R::tag( $e, 'strong,sweet' ); + R::tag( $f, 'black,strong' ); + asrt( count( R::taggedAll( 'coffee', 'strong,sweet' ) ), 1 ); + asrt( count( R::taggedAll( 'coffee', 'strong' ) ), 3 ); + asrt( count( R::taggedAll( 'coffee', '' ) ), 0 ); + asrt( count( R::taggedAll( 'coffee', 'sweet' ) ), 1 ); + asrt( count( R::taggedAll( 'coffee', 'sweet,strong' ) ), 1 ); + asrt( count( R::taggedAll( 'coffee', 'black,strong' ) ), 2 ); + asrt( count( R::taggedAll( 'coffee', array( 'black', 'strong' ) ) ), 2 ); + asrt( count( R::taggedAll( 'coffee', 'salty' ) ), 0 ); + $blog = R::dispense( 'blog' ); + $blog->title = 'testing'; + $blog->blog = 'tesing'; + R::store( $blog ); + $blogpost = ( R::load( "blog", 1 ) ); + $post = R::dispense( "post" ); + $post->message = "hello"; + R::tag( $post, "lousy,smart" ); + asrt( implode( ',', R::tag( $post ) ), "lousy,smart" ); + R::tag( $post, "clever,smart" ); + $tagz = implode( ',', R::tag( $post ) ); + asrt( ( $tagz == "smart,clever" || $tagz == "clever,smart" ), TRUE ); + R::tag( $blog, array( "smart", "interesting" ) ); + asrt( implode( ',', R::tag( $blog ) ), "smart,interesting" ); + try { + R::tag( $blog, array( "smart", "interesting", "lousy!" ) ); + pass(); + } catch ( RedException $e ) { + fail(); + } + asrt( implode( ',', R::tag( $blog ) ), "smart,interesting,lousy!" ); + R::untag( $blog, array( "smart", "interesting" ) ); + asrt( implode( ",", R::tag( $blog ) ), "lousy!" ); + asrt( R::hasTag( $blog, array( "lousy!" ) ), TRUE ); + asrt( R::hasTag( $blog, array( "lousy!", "smart" ) ), TRUE ); + asrt( R::hasTag( $blog, array( "lousy!", "smart" ), TRUE ), FALSE ); + R::tag( $blog, FALSE ); + asrt( count( R::tag( $blog ) ), 0 ); + R::tag( $blog, array( "funny", "comic" ) ); + asrt( count( R::tag( $blog ) ), 2 ); + R::addTags( $blog, array( "halloween" ) ); + asrt( count( R::tag( $blog ) ), 3 ); + asrt( R::hasTag( $blog, array( "funny", "commic", "halloween" ), TRUE ), FALSE ); + R::unTag( $blog, "funny" ); + R::addTags( $blog, "horror" ); + asrt( count( R::tag( $blog ) ), 3 ); + asrt( R::hasTag( $blog, array( "horror", "commic", "halloween" ), TRUE ), FALSE ); + //no double tags + R::addTags( $blog, "horror" ); + asrt( R::hasTag( $blog, array( "horror", "commic", "halloween" ), TRUE ), FALSE ); + asrt( R::hasTag( $blog, "horror,commic,halloween", TRUE ), FALSE ); + asrt( count( R::tag( $blog ) ), 3 ); + testpack( "fetch tagged items" ); + } + + /** + * Fetching tagged items. + * + * @return void + */ + public function fetchTaggedItems() + { + $b = R::dispense( "book" ); + $b->title = 'horror'; + R::store( $b ); + $c = R::dispense( "book" ); + $c->title = 'creepy'; + R::store( $c ); + $d = R::dispense( "book" ); + $d->title = "chicklit"; + R::store( $d ); + R::tag( $b, "horror,classic" ); + R::tag( $d, "women,classic" ); + R::tag( $c, "horror" ); + $x = R::tagged( "book", "classic" ); + asrt( count( $x ), 2 ); + $x = R::tagged( "book", "classic,horror" ); + asrt( count( $x ), 3 ); + $x = R::tagged( "book", array( "classic", "horror" ) ); + asrt( count( $x ), 3 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Threeway.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Threeway.php new file mode 100644 index 0000000..d2fc1d4 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Threeway.php @@ -0,0 +1,129 @@ +sharedRole[] = $role; + R::store( $person ); + $person->link( 'person_role', array( + 'unit' => R::dispense('unit') + ))->role = $role; + //Can we add a duplicate role now? - No because we started with a simple N-M table + //and unique constraint has been applied accordingly, manually change database. + asrt( R::count( 'person_role' ), 1 ); + R::nuke(); + $person = R::dispense( 'person' ); + $role = R::dispense( 'role' ); + $person->via('participant')->sharedRole[] = $role; + R::store( $person ); + $person->link( 'participant', array( + 'unit' => R::dispense('unit') + ))->role = $role; + //Can we add a duplicate role now? - No because we started with a simple N-M table + //and unique constraint has been applied accordingly, manually change database. + asrt( R::count( 'participant' ), 1 ); + R::nuke(); + $participant = R::dispense( 'participant' ); + $person = R::dispense( 'person' ); + $role = R::dispense( 'role' ); + $unit = R::dispense( 'unit' ); + $participant->person = $person; + $participant->role = $role; + $participant->unit = $unit; + R::store( $participant ); + $person->link( 'participant', array( + 'unit' => R::dispense('unit') + ))->role = $role; + R::store( $person ); + //Can we add a duplicate role now? + asrt( R::count( 'participant' ), 2 ); + AQueryWriter::clearRenames(); + } + + /** + * Test whether a duplicate bean in the list isnt saved. + * This was an issue with Postgres while testing the threeway tables. + * Postgres returned the ID as a string while other drivers returned + * a numeric value causing different outcome in array_diff when + * calculating the shared additions. + * + * @return void + */ + public function testIssueWithDriverReturnID() + { + AQueryWriter::clearRenames(); + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->sharedPageList[] = $page; + R::store( $book ); + asrt( R::count( 'page' ), 1 ); + $book = $book->fresh(); + $book->sharedPageList[] = $page; + R::store( $book ); + //don't save the duplicate bean! + asrt( R::count( 'page' ), 1 ); + $book = $book->fresh(); + $page->item = 2; //even if we change a property ? + $book->sharedPageList[] = $page; + R::store( $book ); + foreach( $book->sharedPageList as $listItem) { + asrt( is_string( $listItem->id ), TRUE ); + } + //same test but for own-list + R::nuke(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->ownPageList[] = $page; + R::store( $book ); + asrt( R::count( 'page' ), 1 ); + $book = $book->fresh(); + $book->ownPageList[] = $page; + R::store( $book ); + //don't save the duplicate bean! + asrt( R::count( 'page' ), 1 ); + $book = $book->fresh(); + $book->ownPageList[] = $page; + $page->item = 3; + R::store( $book ); + //don't save the duplicate bean! + asrt( R::count( 'page' ), 1 ); + foreach( $book->ownPageList as $listItem) { + asrt( is_string( $listItem->id ), TRUE ); + } + AQueryWriter::clearRenames(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Trash.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Trash.php new file mode 100644 index 0000000..8362d3a --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Trash.php @@ -0,0 +1,49 @@ +'book', + 'pages'=>3 + ) + )); + asrt( R::count('book'), 1 ); + $n = R::trash(R::findOne('book')); + asrt( $n, 1 ); + asrt( R::count('book'), 0 ); + list($books) = R::dispenseAll('book*10'); + R::storeAll( $books ); + asrt( R::count('book'), 10 ); + $n = R::trashAll( $books ); + asrt( R::count('book'), 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Traverse.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Traverse.php new file mode 100644 index 0000000..157877c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Traverse.php @@ -0,0 +1,249 @@ +title = 'Book ' . ( $i++ ); + } + $books[5]->marked = TRUE; + $shelf = R::dispense( 'shelf' ); + $shelf->ownBook = $books; + $found = NULL; + $shelf->traverse('ownBookList', function( $book ) use ( &$found ) { + if ( $book->marked ) $found = $book; + }); + asrt( ( $found->marked == TRUE ), TRUE ); + asrt( $found->title, 'Book 6' ); + } + + /** + * Tests basic traversal. + * + * @return void + */ + public function testBasicTraversal() + { + R::nuke(); + $pageA = R::dispense( 'page' )->setAttr( 'title', 'a' ); + $pageB = R::dispense( 'page' )->setAttr( 'title', 'b' ); + $pageC = R::dispense( 'page' )->setAttr( 'title', 'c' ); + $pageD = R::dispense( 'page' )->setAttr( 'title', 'd' ); + $pageE = R::dispense( 'page' )->setAttr( 'title', 'e' ); + $pageF = R::dispense( 'page' )->setAttr( 'title', 'f' ); + $pageG = R::dispense( 'page' )->setAttr( 'title', 'g' ); + $pageH = R::dispense( 'page' )->setAttr( 'title', 'h' ); + $pageA->ownPage = array( $pageB, $pageC ); + $pageB->ownPage = array( $pageD ); + $pageC->ownPage = array( $pageE, $pageF ); + $pageD->ownPage = array( $pageG ); + $pageF->ownPage = array( $pageH ); + R::store( $pageA ); + $pageA = $pageA->fresh(); + //also tests non-existant column handling by count(). + asrt( R::count( 'page', ' price = ? ', array( '5' ) ), 0); + asrt( R::count( 'tag', ' title = ? ', array( 'new' ) ), 0); + $pageA->traverse( 'ownPageList', function( $bean ) { + $bean->price = 5; + }); + R::store( $pageA ); + asrt( R::count( 'page', ' price = ? ', array( '5' ) ), 7); + } + + /** + * Test traversing paths, ancestry. + * + * @return void + */ + public function testTraversePaths() + { + R::nuke(); + $pageA = R::dispense( 'page' )->setAttr( 'title', 'a' ); + $pageB = R::dispense( 'page' )->setAttr( 'title', 'b' ); + $pageC = R::dispense( 'page' )->setAttr( 'title', 'c' ); + $pageD = R::dispense( 'page' )->setAttr( 'title', 'd' ); + $pageE = R::dispense( 'page' )->setAttr( 'title', 'e' ); + $pageF = R::dispense( 'page' )->setAttr( 'title', 'f' ); + $pageG = R::dispense( 'page' )->setAttr( 'title', 'g' ); + $pageH = R::dispense( 'page' )->setAttr( 'title', 'h' ); + $pageA->ownPage = array( $pageB, $pageC ); + $pageB->ownPage = array( $pageD ); + $pageC->ownPage = array( $pageE, $pageF ); + $pageD->ownPage = array( $pageG ); + $pageF->ownPage = array( $pageH ); + R::store( $pageA ); + $parents = array(); + $pageF->traverse( 'page', function( $page ) use ( &$parents ) { + $parents[] = $page->title; + } ); + asrt( implode( ',', $parents ), 'c,a' ); + $parents = array(); + $pageH->traverse( 'page', function( $page ) use ( &$parents ) { + $parents[] = $page->title; + } ); + asrt( implode( ',', $parents ), 'f,c,a' ); + $parents = array(); + $pageG->traverse( 'page', function( $page ) use ( &$parents ) { + $parents[] = $page->title; + } ); + asrt( implode( ',', $parents ), 'd,b,a' ); + $path = array(); + $pageA->traverse( 'ownPageList', function( $page ) use ( &$path ) { + $path[] = $page->title; + } ); + asrt( implode( ',', $path ), 'b,d,g,c,e,f,h' ); + $path = array(); + $pageC->traverse( 'ownPageList', function( $page ) use ( &$path ) { + $path[] = $page->title; + } ); + asrt( implode( ',', $path ), 'e,f,h' ); + $path = array(); + $pageA->traverse( 'ownPageList', function( $page ) use ( &$path ) { + $path[] = $page->title; + }, 2 ); + asrt( implode( ',', $path ), 'b,d,c,e,f' ); + } + + /** + * Test traversal with embedded SQL snippets. + * + * @return void + */ + public function testTraversalWithSQL() + { + $tasks = R::dispense('task', 10); + foreach( $tasks as $key => $task ) { + $task->descr = 't'.$key; + } + $tasks[0]->ownTask = array( $tasks[1], $tasks[9], $tasks[7] ); + $tasks[1]->ownTask = array( $tasks[5] ); + $tasks[9]->ownTask = array( $tasks[3], $tasks[8] ); + $tasks[2]->ownTask = array( $tasks[4] ); + $tasks[7]->ownTask = array( $tasks[6] ); + R::storeAll( $tasks ); + $task = R::load('task', $tasks[0]->id); + $todo = array(); + $task->with(' ORDER BY descr ASC ')->traverse('ownTaskList', function( $t ) use ( &$todo ) { + $todo[] = $t->descr; + } ); + asrt( implode( ',', $todo ), 't1,t5,t7,t6,t9,t3,t8' ); + $task = R::load( 'task', $tasks[0]->id ); + $todo = array(); + $task->withCondition( ' ( descr = ? OR descr = ? ) ', array( 't7','t6' ) ) + ->traverse( 'ownTaskList', function( $task ) use( &$todo ){ + $todo[] = $task->descr; + } ); + asrt( implode( ',', $todo ), 't7,t6' ); + } + + /** + * Test traversal with aliases. + * + * @return void + */ + public function testTraversalWithAlias() + { + R::nuke(); + $book = R::dispense( 'book' ); + $cats = R::dispense( 'category', 3 ); + $cats[0]->gname = 'SF'; + $cats[1]->gname = 'Fantasy'; + $cats[2]->gname = 'Horror'; + $book->genre = $cats[0]; + $book->name = 'Space Story'; + $cats[0]->genre = $cats[1]; + $cats[2]->genre = $cats[1]; + R::store( $book ); + $book2 = R::dispense( 'book' ); + $book2->genre = $cats[2]; + $book2->name = 'Ghost Story'; + R::store( $book2 ); + $fantasy = R::load( 'category', $cats[1]->id ); + $cats = array(); + $book = $book->fresh(); + $book->fetchAs( 'category' )->traverse( 'genre', function( $cat ) use ( &$cats ) { + $cats[] = $cat->gname; + } ); + asrt( implode( ',', $cats ), 'SF,Fantasy' ); + $catList = array(); + $fantasy->alias( 'genre' ) + ->with( ' ORDER BY gname ASC ' ) + ->traverse( 'ownCategory', function( $cat ) use ( &$catList ) { + $catList[] = $cat->gname; + } ); + asrt( implode( ',', $catList ), 'Horror,SF' ); + } + + /** + * Traverse can only work with own-lists, otherwise infinite loops. + * + * @return void + */ + public function testSharedTraversal() + { + $friend = R::dispense( 'friend' ); + try { + $friend->traverse( 'sharedFriend', function( $friend ){ } ); + fail(); + } catch( RedException $e ) { + pass(); + } + } + + /** + * Test whether traverse() passes the depth of the + * current item in the tree along with the bean. + */ + public function testDepthCount() + { + R::nuke(); + $page = R::dispense('page'); + $page->num = 1; + $root = $page; + for($i = 2; $i < 10; $i++) { + $child = R::dispense('page'); + $page->ownPageList[] = $child; + $child->num = $i; + $page = $child; + } + R::store($root); + $total = 0; + $page = $root->fresh(); + $page->traverse('ownPageList', function( $child, $count ) use(&$total) { + asrt( $count+1, intval($child->num) ); + $total += $count; + }); + asrt( $total, 36 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Typechecking.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Typechecking.php new file mode 100644 index 0000000..04b25c0 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Typechecking.php @@ -0,0 +1,151 @@ + STRING + * NULL -> NULL + * + * @note Why not simply return bean->id in store()? Because not every driver returns the same type: + * databases without insert_id support require a separate query or a suffix returning STRINGS, not INTEGERS. + * + * @note Why not preserve types? I.e. I store integer, why do I get back a string? + * Answer: types are handled different across database platforms, would cause overhead to inspect every value for type, + * also PHP is a dynamically typed language so types should not matter that much. Another reason: due to the nature + * of RB columns in the database might change (INT -> STRING) this would cause return types to change as well which would + * cause 'cascading errors', i.e. a column gets widened and suddenly your code would break. + * + * @note Unfortunately the 32/64-bit issue cannot be tested fully. Return-strategy store() is probably the safest + * solution. + * + * @return void + */ + public function testTypes() + { + testpack( 'Beans can only contain STRING and NULL after reload' ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->number = 123; + $bean->float = 12.3; + $bean->bool = FALSE; + $bean->bool2 = TRUE; + $bean->text = 'abc'; + $bean->null = null; + $bean->datetime = new\DateTime( 'NOW', new\DateTimeZone( 'Europe/Amsterdam' ) ); + $id = R::store( $bean ); + asrt( is_int( $id ), TRUE ); + asrt( is_float( $bean->float ), TRUE ); + asrt( is_integer( $bean->number ), TRUE ); + asrt( is_string( $bean->bool ), TRUE ); + asrt( is_string( $bean->bool2 ), TRUE ); + asrt( is_string( $bean->datetime ), TRUE ); + asrt( is_string( $bean->text ), TRUE ); + asrt( is_null( $bean->null ), TRUE ); + $bean = R::load('bean', $id ); + asrt( is_string( $bean->id ), TRUE ); + asrt( is_string( $bean->float ), TRUE ); + asrt( is_string( $bean->number ), TRUE ); + asrt( is_string( $bean->bool ), TRUE ); + asrt( is_string( $bean->bool2 ), TRUE ); + asrt( is_string( $bean->datetime ), TRUE ); + asrt( is_string( $bean->text ), TRUE ); + asrt( is_null( $bean->null ), TRUE ); + asrt( $bean->bool, '0' ); + asrt( $bean->bool2, '1' ); + } + + /** + * Test bean type checking. + * + * @return void + */ + public function testBeanTypeChecking() + { + $redbean = R::getRedBean(); + $bean = $redbean->dispense( "page" ); + // Set some illegal values in the bean; this should trigger Security exceptions. + // Arrays are not allowed. + $bean->name = array( "1" ); + try { + $redbean->store( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + try { + $redbean->check( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + $bean->name = new OODBBean; + try { + $redbean->check( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + // Property names should be alphanumeric + $prop = "."; + $bean->$prop = 1; + try { + $redbean->store( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + try { + $redbean->check( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + // Really... + $prop = "-"; + $bean->$prop = 1; + try { + $redbean->store( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + try { + $redbean->check( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Update.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Update.php new file mode 100644 index 0000000..1d6f7a4 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Update.php @@ -0,0 +1,448 @@ +getRedBean()->getCurrentRepository(); + $database = $toolbox->getDatabaseAdapter()->getDatabase(); + $logger = new Logger; + $database->setLogger( $logger ); + $bean = R::dispense('bean'); + $bean->property1 = 'test'; + $bean->property2 = 'test'; //should not cause 2nd DESCRIBE. + R::startLogging(); + R::store( $bean ); + $logger = R::getLogger(); + asrt( + count( $logger->grep('DESCRIBE') ) + + count( $logger->grep('SELECT column_name') ) + + count( $logger->grep('SHOW COLUMNS') ) + //CUBRID + count( $logger->grep('PRAGMA table_info') ) + , 1); + R::stopLogging(); + //new round, same results, no cache between beans + R::nuke(); + $bean = R::dispense('bean'); + $bean->property1 = 'test'; + $bean->property2 = 'test'; //should not cause 2nd DESCRIBE. + R::startLogging(); + R::store( $bean ); + $logger = R::getLogger(); + asrt( + count( $logger->grep('DESCRIBE') ) + + count( $logger->grep('SELECT column_name') ) + + count( $logger->grep('SHOW COLUMNS') ) + //CUBRID + count( $logger->grep('PRAGMA table_info') ) + , 1); + R::stopLogging(); + } + + /** + * Test whether we can use SQL filters and + * whether they are being applied properly for + * different types of SELECT queries in the QueryWriter. + */ + public function testSQLFilters() + { + R::nuke(); + AQueryWriter::setSQLFilters(array( + QueryWriter::C_SQLFILTER_READ => array( + 'book' => array( 'title' => ' LOWER(book.title) '), + ), + QueryWriter::C_SQLFILTER_WRITE => array( + 'book' => array( 'title' => ' UPPER(?) '), + ), + )); + + $book = R::dispense( 'book' ); + $book->title = 'story'; + R::store( $book ); + asrt( R::getCell( 'SELECT title FROM book WHERE id = ?', array( $book->id ) ), 'STORY' ); + $book = $book->fresh(); + asrt( $book->title, 'story' ); + $library = R::dispense( 'library' ); + $library->sharedBookList[] = $book; + R::store( $library ); + $library = $library->fresh(); + $books = $library->sharedBookList; + $book = reset( $books ); + asrt( $book->title, 'story' ); + $otherBook = R::dispense('book'); + $otherBook->sharedBook[] = $book; + R::store( $otherBook ); + $otherBook = $otherBook->fresh(); + $books = $otherBook->sharedBookList; + $book = reset( $books ); + asrt( $book->title, 'story' ); + $links = $book->ownBookBookList; + $link = reset( $links ); + $link->shelf = 'x13'; + AQueryWriter::setSQLFilters(array( + QueryWriter::C_SQLFILTER_READ => array( + 'book' => array( 'title' => ' LOWER(book.title) '), + 'book_book' => array( 'shelf' => ' LOWER(book_book.shelf) '), + ), + QueryWriter::C_SQLFILTER_WRITE => array( + 'book' => array( 'title' => ' UPPER(?) '), + 'book_book' => array( 'shelf' => ' UPPER(?) ') + ), + )); + R::store( $link ); + asrt( R::getCell( 'SELECT shelf FROM book_book WHERE id = ?', array( $link->id ) ), 'X13' ); + $otherBook = $otherBook->fresh(); + unset($book->sharedBookList[$otherBook->id]); + R::store( $book ); + AQueryWriter::setSQLFilters(array()); + } + + /** + * Test unsetting properties. + * + * @return void + */ + public function testUnsetUpdate() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->name = 'x'; + $book->price = 40; + R::store( $book ); + $book = $book->fresh(); + $book->name = 'y'; + unset( $book->name ); + R::store( $book ); + $book = $book->fresh(); + asrt( $book->name, 'x' ); + asrt( (int) $book->price, 40 ); + $book->price = 30; + R::store( $book ); + $book = $book->fresh(); + asrt( $book->name, 'x' ); + asrt( (int) $book->price, 30 ); + $book->price = 20; + unset( $book->price ); + $book->name = 'y'; + R::store( $book ); + $book = $book->fresh(); + asrt( $book->name, 'y' ); + asrt( (int) $book->price, 30 ); + } + + /** + * Tests whether we can update or unset a parent bean + * with an alias without having to use fetchAs and + * without loading the aliased bean causing table-not-found + * errors. + */ + public function testUpdatingParentBeansWithAliases() + { + testpack( 'Test updating parent beans with aliases' ); + R::nuke(); + $trans = R::dispense( 'transaction' ); + $seller = R::dispense( 'user' ); + $trans->seller = $seller; + $id = R::store( $trans ); + R::freeze( TRUE ); + $trans = R::load( 'transaction', $id ); + //should not try to load seller, should not require fetchAs(). + try { + $trans->seller = R::dispense( 'user' ); + pass(); + } catch( Exception $e ) { + fail(); + } + $trans = R::load( 'transaction', $id ); + //same for unset... + try { + unset( $trans->seller ); + pass(); + } catch ( Exception $e ) { + fail(); + } + R::freeze( FALSE ); + $account = R::dispense( 'user' ); + asrt( count( $account->alias( 'seller' )->ownTransaction ), 0 ); + $account->alias( 'seller' )->ownTransaction = R::dispense( 'transaction', 10 ); + $account->alias( 'boo' ); //try to trick me... + $id = R::store( $account ); + R::freeze( TRUE ); + $account = R::load( 'user', $id ); + asrt( count( $account->alias( 'seller' )->ownTransaction ), 10 ); + //you cannot unset a list + unset( $account->alias( 'seller' )->ownTransaction ); + $id = R::store( $account ); + $account = R::load( 'user', $id ); + asrt( count( $account->alias( 'seller' )->ownTransaction ), 10 ); + $account->alias( 'seller' )->ownTransaction = array(); + $id = R::store( $account ); + $account = R::load( 'user', $id ); + asrt(count($account->alias( 'seller' )->ownTransaction), 0 ); + asrt(count($account->ownTransaction), 0 ); + R::freeze( FALSE ); + //but also make sure we don't cause extra column issue #335 + R::nuke(); + $building = R::dispense('building'); + $village = R::dispense('village'); + $building->village = $village; + R::store($building); + $building = $building->fresh(); + $building->village = NULL; + R::store($building); + $building = $building->fresh(); + $columns = R::inspect('building'); + asrt( isset( $columns['village'] ), FALSE ); + asrt( isset( $building->village ), FALSE ); + R::nuke(); + $building = R::dispense('building'); + $village = R::dispense('village'); + $building->village = $village; + R::store($building); + $building = $building->fresh(); + unset($building->village); + R::store($building); + $building = $building->fresh(); + $columns = R::inspect('building'); + asrt( isset( $columns['village'] ), FALSE ); + asrt( isset( $building->village ), FALSE ); + $building = R::dispense('building'); + $village = R::dispense('village'); + $building->village = $village; + R::store($building); + $building = $building->fresh(); + $building->village = FALSE; + R::store($building); + $building = $building->fresh(); + $columns = R::inspect('building'); + asrt( isset( $columns['village'] ), FALSE ); + asrt( isset( $building->village ), FALSE ); + } + + /** + * All kinds of tests for basic CRUD. + * + * Does the data survive? + * + * @return void + */ + public function testUpdatingBeans() + { + testpack( 'Test basic support UUID/override ID default value' ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + if ($this->currentlyActiveDriverID === 'mysql') { + //otherwise UTF8 causes index overflow in mysql: SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes + R::exec('alter table bean modify column id char(3);'); + } else { + R::getWriter()->widenColumn( 'bean', 'id', R::getWriter()->scanType( 'abc' ) ); + } + $bean->id = 'abc'; + R::store( $bean ); + asrt( $bean->id, 'abc' ); + testpack( 'Test Update' ); + try { + R::store( array() ); + fail(); + } catch ( RedException $e ) { + pass(); + } + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $page = $redbean->dispense( "page" ); + $page->name = "old name"; + $id = $redbean->store( $page ); + asrt( $page->getMeta( 'tainted' ), FALSE ); + $page->setAttr( 'name', "new name" ); + asrt( $page->getMeta( 'tainted' ), TRUE ); + $id = $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + // Null should == NULL after saving + $page->rating = NULL; + $newid = $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( ( $page->rating === NULL ), TRUE ); + asrt( !$page->rating, TRUE ); + $page->rating = FALSE; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( (bool) $page->rating, FALSE ); + asrt( ( $page->rating == FALSE ), TRUE ); + asrt( !$page->rating, TRUE ); + $page->rating = TRUE; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( (bool) $page->rating, TRUE ); + asrt( ( $page->rating == TRUE ), TRUE ); + asrt( ( $page->rating == TRUE ), TRUE ); + $page->rating = NULL; + R::store( $page ); + $page = R::load( 'page', $page->id ); + asrt( $page->rating, NULL ); + $page->rating = '1'; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( $page->rating, "1" ); + $page->rating = "0"; + $newid = $redbean->store( $page ); + asrt( $page->rating, "0" ); + $page->rating = 0; + $newid = $redbean->store( $page ); + asrt( $page->rating, 0 ); + $page->rating = "0"; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( !$page->rating, TRUE ); + asrt( ( $page->rating == 0 ), TRUE ); + asrt( ( $page->rating == FALSE ), TRUE ); + $page->rating = 5; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( strval( $page->rating ), "5" ); + $page->rating = 300; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( strval( $page->rating ), "300" ); + $page->rating = -2; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( strval( $page->rating ), "-2" ); + $page->rating = 2.5; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( ( $page->rating == 2.5 ), TRUE ); + $page->rating = -3.3; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( ( $page->rating == -3.3 ), TRUE ); + $page->rating = "good"; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( $page->rating, "good" ); + $longtext = str_repeat( 'great! because..', 100 ); + $page->rating = $longtext; + $newid = $redbean->store( $page ); + asrt( $newid, $id ); + $page = $redbean->load( "page", $id ); + asrt( $page->name, "new name" ); + asrt( $page->rating, $longtext ); + // Test leading zeros + $numAsString = "0001"; + $page->numasstring = $numAsString; + $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + asrt( $page->numasstring, "0001" ); + $page->numnotstring = "0.123"; + $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + asrt( $page->numnotstring == 0.123, TRUE ); + $page->numasstring2 = "00.123"; + $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + asrt( $page->numasstring2, "00.123" ); + try { + $redbean->trash( array() ); + fail(); + } catch ( RedException $e ) { + pass(); + } + $redbean->trash( $page ); + asrt( (int) $pdo->GetCell( "SELECT count(*) FROM page" ), 0 ); + } + + /** + * Tests whether empty strings are preserved as such. + * + * @return void + */ + public function testEmptyStringShouldNotBeStoredAsInteger() + { + R::nuke(); + $bean = R::dispense('bean'); + $bean->str = ''; + R::store($bean); + $bean = $bean->fresh(); + asrt( ( $bean->str === '' ), TRUE); + } + + /** + * Test handling of infinity values. + * + * @return void + */ + public function testStoringInf() + { + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->inf = INF; + R::store( $bean ); + $bean = $bean->fresh(); + asrt( ( $bean->inf === 'INF' ), TRUE ); + asrt( ( $bean->inf == 'INF' ), TRUE ); + $bean->modifyme = 'yes'; + R::store( $bean ); + $bean = $bean->fresh(); + asrt( ( $bean->inf === 'INF' ), TRUE ); + asrt( ( $bean->inf == 'INF' ), TRUE ); + $bean->modifyme = 'yes'; + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Utf8.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Utf8.php new file mode 100644 index 0000000..6d11f08 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Utf8.php @@ -0,0 +1,58 @@ +byte = $byte; + OODBBean::setEnforceUTF8encoding( TRUE ); + $str = strval( $bean ); + OODBBean::setEnforceUTF8encoding( FALSE ); + pass(); + } + + /** + * Test UTF8 handling. + * + * @return void + */ + public function testUTF8() + { + //skip if < 5.3 + if (version_compare(PHP_VERSION, '5.4', '<')) return pass(); + $str = '𠜎ὃ𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜'; + $bean = R::dispense( 'bean' ); + $bean->bla = $str; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->bla, $str ); + pass(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Via.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Via.php new file mode 100644 index 0000000..73fbc68 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Via.php @@ -0,0 +1,330 @@ +sharedPersonList[] = $fooPerson; + R::store($mediaBean); + asrt( count( $mediaBean->sharedPersonList ), 1 ); + $person = R::findOne('person'); + $person->via('relation')->sharedMarriageList[] = R::dispense('marriage'); + //this second one caused the via property to not get cleared + $person->via('relation')->sharedMarriageList; + asrt( count( $person->sharedMediaList ), 1 ); + //also found this scenario, non-existing property + $book = R::dispense('book'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->nothing; + asrt( count( $book->sharedPageList ), 1 ); + //yet another + $book = R::dispense('magazine'); + $book->ownAdList[] = R::dispense('ad'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->ownAdList; + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('folder'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->sharedItemList[] = R::dispense('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('folder2'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->sharedItemList[] = R::dispense('item'); + $book->via('garbage')->sharedItemList[] = R::dispense('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('folder3'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->item = R::dispense('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('folder3'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->item = 'test'; + asrt( count( $book->sharedPageList ), 1 ); + //yet another + $book = R::dispense('leaflet'); + $book->title = 'leaflet'; + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->title; + asrt( count( $book->sharedPageList ), 1 ); + //yet another + $book = R::dispense('paper'); + $book->author = R::dispense('author'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->author; + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('paper2'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('garbage')->author; + asrt( count( $book->sharedPageList ), 1 ); + //yet another one + $book = R::dispense('archive'); + $book->sharedItem[] = R::dispense('item'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + unset( $book->via('garbage')->sharedItem ); + asrt( count( $book->sharedPageList ), 1 ); + //theoretic cases + $book = R::dispense('guide'); + $book->sharedItem[] = R::dispense('item'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('relation')->countShared('item'); + $book->via('relation')->countShared('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('catalogue'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('relation')->countShared('item'); + $book->via('relation')->countShared('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('tabloid'); + $book->ownItemList[] = R::dispense('item'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('relation')->countOwn('item'); + $book->via('relation')->countOwn('item'); + asrt( count( $book->sharedPageList ), 1 ); + $book = R::dispense('booklet'); + $book->ownItemList[] = R::dispense('item'); + $book->sharedPage[] = R::dispense('page'); + R::store($book); + $book = $book->fresh(); + $book->via('relation')->countOwn('item'); + $book->via('relation')->countOwn('item'); + asrt( count( $book->sharedPageList ), 1 ); + AQueryWriter::clearRenames(); + } + + /** + * Via specific tests. + * + * @return void + */ + public function testViaAndSQL() + { + R::nuke(); + list($p1, $p2) = R::dispense('participant', 2); + list($e1, $e2) = R::dispense('employee', 2); + list($x1, $x2) = R::dispense('project', 2); + $e1->name = 'Anna'; + $e2->name = 'John'; + $p1->project = $x1; + $p1->employee = $e1; + $p1->arole = 'designer'; + $p2->project = $x1; + $p2->employee = $e2; + $p2->arole = 'coder'; + R::storeAll(array( $p1, $p2 )); + $project = R::load('project', $x1->id); + $designers = $project + ->withCondition(' participant.arole = ? ', array( 'designer' ) ) + ->via( 'participant' ) + ->sharedEmployeeList; + $anna = reset( $designers ); + asrt(count($designers), 1); + asrt($anna->name, 'Anna'); + $coders = $project + ->withCondition(' participant.arole = ? ', array( 'coder' ) ) + ->via( 'participant' ) + ->sharedEmployeeList; + $john = reset( $coders ); + asrt(count($coders), 1); + asrt($john->name, 'John'); + } + + /** + * Test Via and Link together. + * + * @return void + */ + public function testViaAndLink() + { + R::nuke(); + list( $John, $Anna, $Celine ) = R::dispense( 'employee', 3 ); + $John->badge = 'John'; + $Anna->badge = 'Anna'; + $Celine->badge = 'Celine'; + $project = R::dispense( 'project' ); + $project->name = 'x'; + $project2 = R::dispense( 'project' ); + $project2->name = 'y'; + $John->link( 'participant', array( + 'arole' => 'designer' + ) )->project = $project; + $Anna->link( 'participant', array( + 'arole' => 'developer' + ) )->project = $project; + $Celine->link( 'participant', array( + 'arole' => 'sales' + ) )->project = $project2; + $Anna->link('participant', array( + 'arole' => 'lead' + ) )->project = $project2; + R::storeAll( array( $project, $project2, $John, $Anna, $Celine ) ); + $employees = $project + ->with(' ORDER BY badge ASC ') + ->via( 'participant' ) + ->sharedEmployee; + asrt( is_array( $employees ), TRUE ); + asrt( count( $employees ), 2 ); + $badges = array(); + foreach( $employees as $employee ) { + $badges[] = $employee->badge; + } + asrt( implode( ',', $badges ), 'Anna,John' ); + $employees = $project2 + ->with(' ORDER BY badge ASC ') + ->via( 'participant' ) + ->sharedEmployee; + asrt( is_array( $employees ), TRUE ); + asrt( count( $employees ), 2 ); + $badges = array(); + foreach( $employees as $employee ) { + $badges[] = $employee->badge; + } + asrt( implode( ',', $badges ), 'Anna,Celine' ); + $projects = $John->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 1 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'x' ); + $projects = $Anna->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 2 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'x,y' ); + $projects = $Anna->via( 'participant' )->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 2 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'x,y' ); + $projects = $Celine->via( 'participant' )->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 1 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'y' ); + $roles = $Anna->ownParticipant; + asrt( is_array( $roles ), TRUE ); + asrt( count( $roles ), 2 ); + $roleList = array(); + foreach( $roles as $role ) { + $roleList[] = $role->arole; + } + sort( $roleList ); + asrt( implode( ',', $roleList ), 'developer,lead' ); + $project2->sharedEmployee[] = $John; + R::store( $project2 ); + $projects = $John->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 2 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'x,y' ); + $projects = $John->via( 'participant' )->sharedProject; + asrt( is_array( $projects ), TRUE ); + asrt( count( $projects ), 2 ); + $projectList = array(); + foreach( $projects as $project ) { + $projectList[] = $project->name; + } + sort( $projectList ); + asrt( implode( ',', $projectList ), 'x,y' ); + } + + /** + * Test effect of via on shared list removal of beans. + * + * @return void + */ + public function testViaAndRemove() + { + R::nuke(); + $project = R::dispense( 'project' ); + $employees = R::dispense( 'employee', 2); + $project->via( 'partcipant' )->sharedEmployeeList = $employees; + R::store( $project ); + asrt( R::count('employee'), 2 ); + asrt( R::count('participant'), 2 ); + $project = $project->fresh(); + $project->sharedEmployee = array(); + R::store( $project ); + asrt( R::count( 'employee' ), 2 ); + asrt( R::count( 'participant' ), 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/With.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/With.php new file mode 100644 index 0000000..01bf47f --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/With.php @@ -0,0 +1,581 @@ + 'book', + 'title' => 'Book of Lorem Ipsum', + 'ownPage' => array( + array( + '_type' => 'page', + 'content' => 'Lorem Ipsum', + ) + ), + 'sharedTag' => array( + array( + '_type' => 'tag', + 'label' => 'testing' + ) + ) + ) ); + R::store( $book ); + $book = $book->fresh(); + asrt( R::count( 'book' ), 1 ); + asrt( count( $book->ownPage ), 1 ); + //now try with no-load + $book = $book->fresh(); + asrt( count( $book->noLoad()->ownPage ), 0 ); + asrt( count( $book->noLoad()->sharedTag ), 0 ); + //now try to add with no-load + $book = $book->fresh(); + $book->noLoad()->xownPageList[] = R::dispense( 'page' ); + $book->noLoad()->sharedTagList[] = R::dispense( 'tag' ); + R::store( $book ); + $book = $book->fresh(); + asrt( count( $book->ownPage ), 2 ); + asrt( count( $book->sharedTagList ), 2 ); + //no-load overrides with and withCondition + $book = $book->fresh(); + asrt( count( $book->with(' invalid sql ')->noLoad()->ownPage ), 0 ); + asrt( count( $book->withCondition(' invalid sql ')->noLoad()->sharedTag ), 0 ); + //no-load overrides all and alias + $book = $book->fresh(); + asrt( count( $book->all()->noLoad()->ownPage ), 0 ); + asrt( count( $book->alias('nothing')->noLoad()->sharedTag ), 0 ); + //no-load gets cleared + $book = $book->fresh(); + asrt( count( $book->ownPage ), 2 ); + asrt( count( $book->sharedTagList ), 2 ); + //We cant clear with no-load accidentally? + $book = $book->fresh(); + $book->noLoad()->ownPage = array(); + $book->noLoad()->sharedTagList = array(); + R::store( $book ); + asrt( count( $book->ownPage ), 2 ); + asrt( count( $book->sharedTagList ), 2 ); + //No-load does not have effect if list is already cached + $book = $book->fresh(); + $book->ownPage; + $book->sharedTag; + asrt( count( $book->ownPage ), 2 ); + asrt( count( $book->sharedTagList ), 2 ); + } + + /** + * Test all(). + * + * @return void + */ + public function testAll() + { + $book = R::dispense( 'book' ); + $book->ownPage = R::dispense( 'page', 10 ); + R::store( $book ); + asrt( count( $book->with( ' LIMIT 3 ' )->ownPage ), 3 ); + asrt( count( $book->ownPage ), 3 ); + asrt( count( $book->all()->ownPage ), 10 ); + asrt( count( $book->ownPage ), 10 ); + R::nuke(); + asrt( count( $book->ownPage ), 10 ); + asrt( count( $book->all()->ownPage ), 0 ); + } + + /** + * Test embedded SQL snippets using with and withCondition. + * + * @return void + */ + public function testEmbeddedSQL() + { + list( $page1, $page2, $page3 ) = R::dispense( 'page', 3 ); + list( $ad1, $ad2, $ad3 ) = R::dispense( 'ad', 3 ); + $ad2->name = 'shampoo'; + $page3->name = 'homepage'; + $page1->sharedAd = array( $ad1, $ad3 ); + $page2->sharedAd = array( $ad2, $ad3 ); + $page3->sharedAd = array( $ad3, $ad2, $ad1 ); + R::storeAll( array( $page1, $page2, $page3 ) ); + $page1 = R::load( 'page', $page1->id ); + asrt( 1, count( $page1->with( ' LIMIT 1 ' )->sharedAd ) ); + $page2 = R::load( 'page', $page2->id ); + $adsOnPage2 = $page2->withCondition( ' `name` = ? ', array( 'shampoo' ) )->sharedAd; + asrt( 1, count( $adsOnPage2 ) ); + $ad = reset( $adsOnPage2 ); + asrt( $ad->name, 'shampoo' ); + $ad = R::load( 'ad', $ad->id ); + asrt( count( $ad->sharedPage ), 2 ); + $ad = R::load( 'ad', $ad->id ); + $homepage = reset( $ad->withCondition( ' `name` LIKE ? AND page.id > 0 ORDER BY id DESC ', array( '%ome%' ) )->sharedPage ); + asrt( $homepage->name, 'homepage' ); + } + + /** + * More variations... + * + * @return void + */ + public function testEmbeddedSQLPart2() + { + list( $book1, $book2, $book3 ) = R::dispense( 'book', 3 ); + $book1->position = 1; + $book2->position = 2; + $book3->position = 3; + $shelf = R::dispense( 'shelf' ); + $shelf->ownBook = array( $book1, $book2, $book3 ); + $id = R::store( $shelf ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' ORDER BY position ASC ' )->ownBook; + $book1 = array_shift( $books ); + asrt( (int) $book1->position, 1 ); + $book2 = array_shift( $books ); + asrt( (int) $book2->position, 2 ); + $book3 = array_shift( $books ); + asrt( (int) $book3->position, 3 ); + $books = $shelf->with( ' ORDER BY position DESC ' )->ownBook; + $book1 = array_shift( $books ); + asrt( (int) $book1->position, 3 ); + $book2 = array_shift( $books ); + asrt( (int) $book2->position, 2 ); + $book3 = array_shift( $books ); + asrt( (int) $book3->position, 1 ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position > 2 ' )->ownBook; + asrt( count( $books ), 1 ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position < ? ', array( 3 ) )->ownBook; + asrt( count( $books ), 2 ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position = 1 ' )->ownBook; + asrt( count( $books ), 1 ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->withCondition( ' position > -1 ' )->ownBook; + asrt( count( $books ), 3 ); + // With-condition should not affect storing + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position = 1 ' )->ownBook; + asrt( count( $books ), 1 ); + asrt( count( $shelf->ownBook ), 1 ); + $book = reset( $shelf->ownBook ); + $book->title = 'Trees and other Poems'; + R::store( $shelf ); + $books = $shelf->withCondition( ' position > -1 ' )->ownBook; + asrt( count( $books ), 3 ); + asrt( count( $shelf->ownBook ), 3 ); + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position = 1 ' )->ownBook; + // Also with trashing -- just trash one! + $shelf->ownBook = array(); + R::store( $shelf ); + $books = $shelf->withCondition( ' position > -1 ' )->ownBook; + asrt( count( $books ), 2 ); + // With should cause a reload of a list + $shelf = R::load( 'shelf', $id ); + $books = $shelf->with( ' AND position = 2 ' )->ownBook; + asrt( count( $books ), 1 ); + $books = $shelf->withCondition( ' position > -1 ' )->ownBook; + asrt( count( $books ), 2 ); + $book = reset( $books ); + $book->title = 'Venetian Music'; + // Should not affect storage (fact that we used with twice, unsetting prop) + R::store( $shelf ); + $shelf = R::load( 'shelf', $id ); + asrt( count( $shelf->ownBook ), 2 ); + // Alias + list( $game1, $game2, $game3 ) = R::dispense( 'game', 3 ); + list( $t1, $t2, $t3 ) = R::dispense( 'team', 3 ); + $t1->name = 'Bats'; + $t2->name = 'Tigers'; + $t3->name = 'Eagles'; + $game1->name = 'a'; + $game1->team1 = $t1; + $game1->team2 = $t2; + $game2->name = 'b'; + $game2->team1 = $t1; + $game2->team2 = $t3; + $game3->name = 'c'; + $game3->team1 = $t2; + $game3->team2 = $t3; + R::storeAll( array( $game1, $game2, $game3 ) ); + $team1 = R::load( 'team', $t1->id ); + $team2 = R::load( 'team', $t2->id ); + $team3 = R::load( 'team', $t3->id ); + asrt( count( $team1->alias( 'team1' )->ownGame ), 2 ); + asrt( count( $team2->alias( 'team1' )->ownGame ), 1 ); + $team1 = R::load( 'team', $t1->id ); + $team2 = R::load( 'team', $t2->id ); + asrt( count( $team1->alias( 'team2' )->ownGame ), 0 ); + asrt( count( $team2->alias( 'team2' )->ownGame ), 1 ); + asrt( count( $team3->alias( 'team1' )->ownGame ), 0 ); + $team3 = R::load( 'team', $t3->id ); + asrt( count( $team3->alias( 'team2' )->ownGame ), 2 ); + $team1 = R::load( 'team', $t1->id ); + $games = $team1->alias( 'team1' )->ownGame; + $game4 = R::dispense( 'game' ); + $game4->name = 'd'; + $game4->team2 = $t3; + $team1->alias( 'team1' )->ownGame[] = $game4; + R::store( $team1 ); + $team1 = R::load( 'team', $t1->id ); + asrt( count( $team1->alias( 'team1' )->ownGame ), 3 ); + foreach ( $team1->ownGame as $g ) { + if ( $g->name == 'a' ) $game = $g; + } + $game->name = 'match'; + R::store( $team1 ); + $team1 = R::load( 'team', $t1->id ); + asrt( count( $team1->alias( 'team1' )->ownGame ), 3 ); + $found = 0; + foreach ( $team1->ownGame as $g ) { + if ( $g->name == 'match' ) $found = 1; + } + if ( $found ) pass(); + $team1->ownGame = array(); + R::store( $team1 ); + $team1 = R::load( 'team', $t1->id ); + asrt( count( $team1->alias( 'team1' )->ownGame ), 0 ); + $team1->ownBook[] = $book1; + R::store( $team1 ); + $team1 = R::load( 'team', $t1->id ); + asrt( count( $team1->alias( 'team1' )->ownGame ), 0 ); + asrt( count( $team1->ownBook ), 1 ); + } + + /** + * Test when to reload and when to NOT reload beans. + * Use UNSET to reload a parent bean. Use UNSET or + * a modifier (with, withCondition, all) to reload a list. + * Use noLoad() to obtain an empty list - does not reload + * but sets an empty array. + * + * @return void + */ + public function testWhenToReload() + { + $book = R::dispense( 'book' ); + $book->ownPage = R::dispense( 'page', 3 ); + $book->author = R::dispense( 'author' ); + $book->coauthor = R::dispense( 'author' ); + R::store( $book ); + $book = $book->fresh(); + $firstPage = reset( $book->ownPage ); + $id = $firstPage->id; + $book->ownPage[ $id ]->title = 'a'; + //Do not reload an own list after manipulations + asrt( $book->ownPage[ $id ]->title, 'a' ); //dont reload! + $book->ownPage[] = R::dispense( 'page' ); //dont reload! + asrt( $book->ownPage[ $id ]->title, 'a' ); //dont reload! + asrt( $book->ownPageList[ $id ]->title, 'a' ); //dont reload! + asrt( $book->xownPageList[ $id ]->title, 'a' ); //dont reload! + asrt( $book->xownPage[ $id ]->title, 'a' ); //dont reload! + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + unset( $book->ownPageList ); + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + unset( $book->xownPageList ); + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + unset( $book->xownPage ); + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + unset( $book->ownPage ); + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + $book->all()->ownPage; + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + $book->all()->xownPage; + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + $book->all()->ownPageList; + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //now trigger reload + $book->all()->xownPageList; + asrt( count( $book->ownPageList ), 3 ); + $book->ownPage[] = R::dispense( 'page' ); + asrt( count( $book->ownPageList ), 4 ); + //Do not reload an own list if told to not reload using noLoad() + $book->noLoad()->with(' LIMIT 1 ')->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->noLoad()->all()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->noLoad()->alias('magazine')->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->noLoad()->withCondition('')->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + //even if modifiers proceed noLoad() + $book->with(' LIMIT 1 ')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->all()->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->alias('magazine')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->withCondition('')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + //even in combinations + $book->all()->with(' LIMIT 1 ')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->alias('magazine')->all()->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->alias('magazine')->with('LIMIT 1')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + $book->alias('magazine')->withCondition('')->noLoad()->ownPage; //dont reload! + asrt( count( $book->xownPage ), 0); //dont reload! + //now test shared list + $book->sharedTag = R::dispense( 'tag', 16 ); + asrt( count( $book->sharedTag ), 16 ); + $book->sharedTag[] = R::dispense( 'tag' ); + asrt( count( $book->sharedTag ), 17 ); //dont reload after adding + $last = end( $book->sharedTagList ); + $id = $last->id; + $book->sharedTag[ $id ]->title = 'b'; + asrt( count( $book->sharedTag ), 17 ); //dont reload after manipulation + unset( $book->sharedTagList[ $id ] ); + asrt( count( $book->sharedTag ), 16 ); //dont reload after manipulation + //now trigger reload + unset( $book->sharedTagList ); + asrt( count( $book->sharedTag ), 0 ); + $book->sharedTag = R::dispense( 'tag', 16 ); + asrt( count( $book->sharedTag ), 16 ); + //now trigger reload + unset( $book->sharedTag ); + asrt( count( $book->sharedTag ), 0 ); + $book->sharedTag = R::dispense( 'tag', 16 ); + asrt( count( $book->sharedTag ), 16 ); + //now trigger reload + $book->all()->sharedTag; + asrt( count( $book->sharedTag ), 0 ); + $book->sharedTag = R::dispense( 'tag', 16 ); + asrt( count( $book->sharedTag ), 16 ); + //now trigger reload + $book->all()->sharedTagList; + asrt( count( $book->sharedTag ), 0 ); + $book->sharedTag = R::dispense( 'tag', 16 ); + asrt( count( $book->sharedTag ), 16 ); + //Do not reload a sharedTag list if told to not reload using noLoad() + $book->noLoad()->with(' LIMIT 1 ')->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->noLoad()->all()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->noLoad()->alias('magazine')->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->noLoad()->withCondition('')->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + //even if modifiers proceed noLoad() + $book->with(' LIMIT 1 ')->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->all()->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->alias('magazine')->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->withCondition('')->noLoad()->ownPage; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + //even in combinations + $book->all()->with(' LIMIT 1 ')->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->alias('magazine')->all()->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->alias('magazine')->with('LIMIT 1')->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + $book->alias('magazine')->withCondition('')->noLoad()->sharedTag; //dont reload! + asrt( count( $book->sharedTag ), 0); //dont reload! + //test do not reload parent bean + $book->author->name = 'me'; + asrt( $book->author->name, 'me' ); + $book->fetchAs('author')->coauthor; + asrt( $book->author->name, 'me' ); + $book->fetchAs('author')->author; + asrt( $book->author->name, 'me' ); + $book->with(' LIMIT 1 ')->author; + asrt( $book->author->name, 'me' ); + $book->withCondition('')->author; + asrt( $book->author->name, 'me' ); + $book->all()->author; + asrt( $book->author->name, 'me' ); + $book->noLoad()->author; + asrt( $book->author->name, 'me' ); + $book->noLoad()->all()->author; + asrt( $book->author->name, 'me' ); + $book->with('LIMIT 1')->noLoad()->all()->author; + asrt( $book->author->name, 'me' ); + //now trigger reload + unset( $book->author ); + asrt( $book->author->name, NULL ); + $book->author->name = 'me'; + asrt( $book->author->name, 'me' ); + } + + /** + * Tests whether modifiers are cleared after reading or + * writing a bean property. + * + * @return void + */ + public function testClearanceOfModFlags() + { + //test base condition, retrieving list or parent should not set flags + $book = R::dispense( 'book' ); + asrt( $book->getModFlags(), '' ); + $book->ownPage = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->xownPage = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->ownPageList = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->xownPageList = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->ownPage[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->xownPage[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->ownPageList[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->xownPageList[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->sharedPage = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->sharedPageList = R::dispense( 'page', 2 ); + asrt( $book->getModFlags(), '' ); + $book->sharedPage[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->sharedPageList[] = R::dispense( 'page', 1 ); + asrt( $book->getModFlags(), '' ); + $book->author = R::dispense( 'author' ); + asrt( $book->getModFlags(), '' ); + $book->title = 'title'; + //Test whether appropriate flags are set and whether they are cleared after + //accessing a property. + $modifiers = array('with'=>'w', 'withCondition'=>'w', 'alias'=>'a', 'fetchAs'=>'f', 'all'=>'r', 'noLoad'=>'n'); + $properties = array('ownPage', 'ownPageList', 'xownPage', 'xownPageList', 'sharedPage', 'sharedPageList', 'author', 'title'); + foreach( $modifiers as $modifier => $flag ) { + foreach( $properties as $property ) { + $book = R::dispense( 'book' ); + $book->$modifier('something'); + $flags = $book->getModFlags(); + $expect = $flag; + asrt( $flags, $expect ); + $book->$property; + $flags = $book->getModFlags(); + asrt( $flags, '' ); + } + } + //now test combinations and also test whether we can + //clear modifiers manually using the clearModifiers() method. + foreach( $modifiers as $modifier => $flag ) { + foreach( $modifiers as $modifier2 => $flag2 ) { + foreach( $properties as $property ) { + $book = R::dispense( 'book' ); + $book->$modifier( 'something' )->$modifier2( 'something' ); + $flags = $book->getModFlags(); + $expect = array($flag, $flag2); + $expect = array_unique( $expect ); + sort( $expect ); + $expect = implode( '', $expect ); + asrt( $flags, $expect ); + $book->$modifier( 'something' )->$modifier2( 'something' )->clearModifiers(); + $flags = $book->getModFlags(); + asrt( $flags, '' ); + $book->$modifier( 'something' )->$modifier2( 'something' )->clearModifiers(); + $book->$property; + $flags = $book->getModFlags(); + asrt( $flags, '' ); + } + } + } + $book = R::dispense( 'book' ); + $book->ownPage = R::dispense( 'page', 2 ); + $book->sharedPage = R::dispense( 'page', 2 ); + R::store( $book ); + $book = R::dispense( 'book' ); + $book->alias('magazine')->ownPage = R::dispense( 'page', 2 ); + R::store( $book ); + //test modifier with countOwn and countShared methods + foreach( $modifiers as $modifier => $flag ) { + $book = R::dispense( 'book' ); + if ($modifier === 'withCondition') $book->$modifier( ' 1 ' ); + elseif ($modifier === 'with') $book->$modifier( ' LIMIT 1 ' ); + elseif ($modifier === 'alias') $book->$modifier('magazine'); + else $book->$modifier('something'); + $flags = $book->getModFlags(); + $expect = $flag; + asrt( $flags, $expect ); + $book->countOwn('page'); + $flags = $book->getModFlags(); + asrt( $flags, '' ); + if ($modifier === 'withCondition') $book->$modifier( ' 1 ' ); + elseif ($modifier === 'with') $book->$modifier( ' LIMIT 1 ' ); + elseif ($modifier === 'alias') $book->$modifier('magazine'); + else $book->$modifier('something'); + $flags = $book->getModFlags(); + $expect = $flag; + asrt( $flags, $expect ); + $book->countShared('page'); + $flags = $book->getModFlags(); + asrt( $flags, '' ); + if ($modifier === 'withCondition') $book->$modifier( ' 1 ' ); + elseif ($modifier === 'with') $book->$modifier( ' LIMIT 1 ' ); + elseif ($modifier === 'alias') $book->$modifier('magazine'); + else $book->$modifier('something'); + $flags = $book->getModFlags(); + $expect = $flag; + asrt( $flags, $expect ); + unset( $book->author ); + $flags = $book->getModFlags(); + asrt( $flags, '' ); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Writecache.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Writecache.php new file mode 100644 index 0000000..0abc426 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Writecache.php @@ -0,0 +1,427 @@ +name = "John"; + $id = R::store($author); + $author = R::load('author', 1); // This gets cached + $author = $author->fresh(); // This gets cached + $author = R::findOne('author', ' id = ? ', array( 1 )); // This gets cached + asrt($author->name, 'John'); + R::exec('UPDATE author SET name = "Bob" WHERE id = 1'); + // It's broken because there's no cache check between those two calls + //more realistic query: 'SELECT MAX(id) FROM author -- keep-cache'); + R::getCell('SELECT 123 -- keep-cache'); + $author = R::load('author', $id); + asrt($author->name, 'John'); + $author = $author->fresh(); + asrt($author->name, 'John'); + $author = R::findOne('author', ' id = ? ', array( 1 )); + asrt($author->name, 'John'); + } + + /** + * Test whether cache size remains constant (per type). + * Avoiding potential memory leaks. (Issue #424). + * + * @return void + */ + public function testCacheSize() + { + R::nuke(); + R::useWriterCache( TRUE ); + $writer = R::getWriter(); + $bean = R::dispense( 'bean' ); + $bean->prop = 1; + R::store( $bean ); + $writer->flushCache( 20 ); + $count = $writer->flushCache(); + asrt( $count, 0 ); + R::find( 'bean', ' prop < ? ', array( 1 ) ); + $count = $writer->flushCache(); + asrt( $count, 2 ); + R::find( 'bean', ' prop < ? ', array( 2 ) ); + $count = $writer->flushCache(); + asrt( $count, 5 ); + R::find( 'bean', ' prop < ? ', array( 2 ) ); + $count = $writer->flushCache(); + asrt( $count, 5 ); + for( $i = 0; $i < 40; $i ++ ) { + R::find( 'bean', ' prop < ? ', array( $i ) ); + } + $count = $writer->flushCache(); + asrt( $count, 85 ); + for( $i = 0; $i < 120; $i ++ ) { + R::find( 'bean', ' prop < ? ', array( $i ) ); + } + $count = $writer->flushCache( 1 ); + asrt( $count, 85 ); + for( $i = 0; $i < 20; $i ++ ) { + R::find( 'bean', ' prop < ? ', array( $i ) ); + } + $count = $writer->flushCache( 20 ); + asrt( $count, 9 ); + } + + /** + * When using fetchAs(), Query Cache does not recognize objects + * that have been previously fetched, see issue #400. + */ + public function testCachingAndFetchAs() + { + testpack( 'Testing whether you can cache multiple records of the same type' ); + R::debug( TRUE, 1 ); + $logger = R::getDatabaseAdapter()->getDatabase()->getLogger(); + R::nuke(); + $coauthor1 = R::dispense( 'author' ); + $coauthor1->name = 'John'; + $book = R::dispense( 'book' ); + $book->title = 'a Funny Tale'; + $book->coauthor = $coauthor1; + $id = R::store( $book ); + $coauthor = R::dispense( 'author' ); + $coauthor->name = 'Pete'; + $book = R::dispense( 'book' ); + $book->title = 'a Funny Tale 2'; + $book->coauthor = $coauthor; + $id = R::store( $book ); + $book = R::dispense( 'book' ); + $book->title = 'a Funny Tale 3'; + $book->coauthor = $coauthor1; + $id = R::store( $book ); + $books = R::find( 'book' ); + $logger->clear(); + $authors = array(); + $authorsByName = array(); + foreach($books as $book) { + $coAuthor = $book->with( ' ORDER BY title ASC ' ) + ->fetchAs( 'author' )->coauthor; + $authors[] = $coAuthor->name; + $authorsByName[ $coAuthor->name ] = $coAuthor; + } + asrt( count( $logger->grep( 'SELECT' ) ), 2 ); //must be 2! 3 if cache does not work! + asrt( count( $authors ), 3 ); + asrt( isset( $authorsByName[ 'John' ] ), TRUE ); + asrt( isset( $authorsByName[ 'Pete' ] ), TRUE ); + $logger->clear(); + $authors = array(); + $authorsByName = array(); + foreach($books as $book) { + $coAuthor = $book->with( ' ORDER BY title DESC ' ) + ->fetchAs( 'author' )->coauthor; + $authors[] = $coAuthor->name; + $authorsByName[ $coAuthor->name ] = $coAuthor; + } + asrt( count( $logger->grep( 'SELECT' ) ), 0 ); //must be 0! + asrt( count( $authors ), 3 ); + asrt( isset( $authorsByName[ 'John' ] ), TRUE ); + asrt( isset( $authorsByName[ 'Pete' ] ), TRUE ); + } + + /** + * Test effects of cache. + * + * @return void + */ + public function testCachingEffects() + { + testpack( 'Testing WriteCache Query Writer Cache' ); + R::setNarrowFieldMode( FALSE ); + R::useWriterCache( FALSE ); + R::debug( TRUE, 1 ); + $logger = R::getDatabaseAdapter()->getDatabase()->getLogger(); + $book = R::dispense( 'book' )->setAttr( 'title', 'ABC' ); + $book->ownPage[] = R::dispense( 'page' ); + $id = R::store( $book ); + // Test load cache -- without + $logger->clear(); + $book = R::load( 'book', $id ); + $book = R::load( 'book', $id ); + asrt( count( $logger->grep( 'SELECT' ) ), 2 ); + // With cache + R::useWriterCache( TRUE ); + $logger->clear(); + $book = R::load( 'book', $id ); + $book = R::load( 'book', $id ); + asrt( count( $logger->grep( 'SELECT' ) ), 1 ); + R::useWriterCache( FALSE ); + // Test find cache + $logger->clear(); + $book = R::find( 'book' ); + $book = R::find( 'book' ); + asrt( count( $logger->grep( 'SELECT' ) ), 2 ); + // With cache + R::getWriter()->setUseCache( TRUE ); + $logger->clear(); + $book = R::find( 'book' ); + $book = R::find( 'book' ); + asrt( count( $logger->grep( 'SELECT' ) ), 1 ); + R::getWriter()->setUseCache( FALSE ); + // Test combinations + $logger->clear(); + $book = R::findOne( 'book', ' id = ? ', array( $id ) ); + $book->ownPage; + R::batch( 'book', array( $id ) ); + $book = R::findOne( 'book', ' id = ? ', array( $id ) ); + $book->ownPage; + R::batch( 'book', array( $id ) ); + asrt( count( $logger->grep( 'SELECT' ) ), 6 ); + // With cache + R::getWriter()->setUseCache( TRUE ); + $logger->clear(); + R::batch( 'book', array( $id ) ); + $book = R::findOne( 'book', ' id = ? ', array( $id ) ); + $book->ownPage; + $book = R::findOne( 'book', ' id = ? ', array( $id ) ); + $book->ownPage; + asrt( count( $logger->grep( 'SELECT' ) ), 3 ); + R::getWriter()->setUseCache( FALSE ); + // Test auto flush + $logger->clear(); + $book = R::findOne( 'book' ); + $book->name = 'X'; + R::store( $book ); + $book = R::findOne( 'book' ); + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + // With cache + R::getWriter()->setUseCache( TRUE ); + $logger->clear(); + $book = R::findOne( 'book' ); + $book->name = 'Y'; + // Will flush + R::store( $book ); + $book = R::findOne( 'book' ); + // Now the same, auto flushed + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + R::getWriter()->setUseCache( FALSE ); + // Test whether delete flushes as well (because uses selectRecord - might be a gotcha!) + R::store( R::dispense( 'garbage' ) ); + $garbage = R::findOne( 'garbage' ); + $logger->clear(); + $book = R::findOne( 'book' ); + R::trash( $garbage ); + $book = R::findOne( 'book' ); + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + R::store( R::dispense( 'garbage' ) ); + $garbage = R::findOne( 'garbage' ); + // With cache + R::getWriter()->setUseCache( TRUE ); + $logger->clear(); + $book = R::findOne( 'book' ); + R::trash( $garbage ); + $book = R::findOne( 'book' ); + // Now the same, auto flushed + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + R::getWriter()->setUseCache( FALSE ); + R::store( R::dispense( 'garbage' ) ); + $garbage = R::findOne( 'garbage' ); + // With cache + R::getWriter()->setUseCache( TRUE ); + $logger->clear(); + $book = R::findOne( 'book' ); + R::getWriter()->queryRecord( 'garbage', array( 'id' => array( $garbage->id ) ) ); + $book = R::findOne( 'book' ); + // Now the same, auto flushed + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + $page = R::dispense('page'); + $book->sharedPage[] = $page; + R::store( $book ); + $logger->clear(); + $link = R::getWriter()->queryRecordLink( 'book', 'page', $book->id, $page->id ); + asrt( count( $logger->grep( 'SELECT' ) ), 1 ); + $link = R::getWriter()->queryRecordLink( 'book', 'page', $book->id, $page->id ); + asrt( count( $logger->grep( 'SELECT' ) ), 1 ); + R::getWriter()->setUseCache( FALSE ); + $link = R::getWriter()->queryRecordLink( 'book', 'page', $book->id, $page->id ); + asrt( count( $logger->grep( 'SELECT' ) ), 2 ); + R::getWriter()->setUseCache( TRUE ); + R::setNarrowFieldMode( TRUE ); + } + + /** + * Try to fool the cache :) + * + * @return void + */ + public function testRegressions() + { + testpack( 'Testing possible regressions: Try to fool the cache' ); + $str = 'SELECT * FROM ' . R::getWriter()->esc( 'bean', TRUE ) . ' WHERE ( ' . R::getWriter()->esc( 'id', TRUE ) . ' IN ( 1) ) '; + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + $bean->title = 'xxx'; + R::store( $bean ); + // Fire exact same query so cache may think no other query has been fired + R::exec( $str ); + $bean = R::load( 'bean', $id ); + asrt( $bean->title, 'xxx' ); + } + + /** + * Test keep-cache comment. + * + * @return void + */ + public function testKeepCacheCommentInSQL() + { + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + $bean->title = 'xxx'; + R::store( $bean ); + // Causes flush even though it contains -- keep-cache (not at the end, not intended) + R::findOne( 'bean', ' title = ? ', array( '-- keep-cache' ) ); + $bean = R::load( 'bean', $id ); + asrt( $bean->title, 'xxx' ); + } + + /** + * + * Same as above.. test keep cache. + * + * @return void + */ + public function testInstructNoDrop() + { + $str = 'SELECT * FROM ' . R::getWriter()->esc( 'bean', TRUE ) . ' -- keep-cache'; + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + $bean->title = 'xxx'; + R::store( $bean ); + R::exec( $str ); + $bean = R::load( 'bean', $id ); + asrt( $bean->title, 'abc' ); + R::nuke(); + // Now INSTRUCT the cache to not drop the cache CASE 2 + $str = 'SELECT * FROM ' . R::getWriter()->esc( 'bean', TRUE ) . ' -- keep-cache'; + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + $bean->title = 'xxx'; + R::store( $bean ); + R::findOne( 'bean', ' title = ? ', array( 'cache' ) ); + $bean = R::load( 'bean', $id ); + asrt( $bean->title, 'xxx' ); + } + + /** + * Can we confuse the cache? + * + * @return void + */ + public function testConfusionRegression() + { + testpack( 'Testing possible confusion regression' ); + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id1 = R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->title = 'abc2'; + $id2 = R::store( $bean ); + $bean = R::load( 'bean', $id1 ); + asrt( $bean->title, 'abc' ); + $bean = R::load( 'bean', $id2 ); + asrt( $bean->title, 'abc2' ); + } + + /** + * Test Ghost beans.... + * + * @return void + */ + public function testGhostBeans() + { + testpack( 'Testing ghost beans' ); + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id1 = R::store( $bean ); + R::trash( $bean ); + $bean = R::load( 'bean', $id1 ); + asrt( (int) $bean->id, 0 ); + } + + /** + * Test explicit flush. + * + * @return void + */ + public function testExplicitCacheFlush() + { + testpack( 'Test cache flush (explicit)' ); + R::setNarrowFieldMode( FALSE ); + R::debug( TRUE, 1 ); + $logger = R::getDatabaseAdapter()->getDatabase()->getLogger(); + $bean = R::dispense( 'bean' ); + $bean->title = 'abc'; + $id1 = R::store( $bean ); + $logger->clear(); + $bean = R::load( 'bean', $id1 ); + asrt( $bean->title, 'abc' ); + asrt( count( $logger->grep( 'SELECT *' ) ), 1 ); + $bean = R::load( 'bean', $id1 ); + asrt( count( $logger->grep( 'SELECT *' ) ), 1 ); + R::getWriter()->flushCache(); + $bean = R::load( 'bean', $id1 ); + asrt( count( $logger->grep( 'SELECT *' ) ), 2 ); + R::getWriter()->flushCache(); + R::getWriter()->setUseCache( FALSE ); + R::setNarrowFieldMode( TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Xnull.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Xnull.php new file mode 100644 index 0000000..5f13e2a --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Base/Xnull.php @@ -0,0 +1,280 @@ +title = 'Much ado about Null'; + R::store( $book ); + $book = R::dispense('book'); + $book->title = NULL; + R::store( $book ); + $books = R::findLike('book', array( 'title' => NULL ) ); + asrt(count($books), 2); + $wasFalse = R::useISNULLConditions( TRUE ); + asrt( $wasFalse, FALSE ); + $books = R::findLike('book', array( 'title' => NULL ) ); + asrt(count($books), 1); + $books = R::find('book', ' title = :title ', array( 'title' => NULL ) ); + asrt(count($books), 0); + } + + /** + * Test Null bindings. + */ + public function testBindings() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->content = NULL; + //can we store a NULL? + asrt( is_null( $book->content ), TRUE ); + R::store( $book ); + //did we really store the NULL value ? + $book = R::findOne( 'book', ' content IS NULL ' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + //still NULL, not empty STRING ? + asrt( is_null( $book->content ), TRUE ); + $book->pages = 100; + R::store( $book ); + //did we save it once again as NULL? + $book = R::findOne( 'book', ' content IS NULL ' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( is_null( $book->content ), TRUE ); + asrt( gettype( $book->pages ), 'string' ); + $otherBook = R::dispense( 'book' ); + $otherBook->pages = 99; + //also if the column is VARCHAR-like? + $otherBook->content = 'blah blah'; + R::store( $otherBook ); + $book = R::findOne( 'book', ' content IS NULL ' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( is_null( $book->content ), TRUE ); + asrt( intval( $book->pages ), 100 ); + //can we query not NULL as well? + $book = R::findOne( 'book', ' content IS NOT NULL ' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( is_null( $book->content ), FALSE ); + asrt( intval( $book->pages ), 99 ); + asrt( $book->content, 'blah blah' ); + //Can we bind NULL directly? + $book->isGood = FALSE; + //Is NULL the default? And... no confusion with boolean FALSE? + R::store( $book ); + $book = R::findOne( 'book', ' is_good IS NULL' ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( is_null( $book->content ), TRUE ); + asrt( intval( $book->pages ), 100 ); + $book = R::findOne( 'book', ' is_good = ?', array( 0 ) ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( is_null( $book->content ), FALSE ); + asrt( intval( $book->pages ), 99 ); + } + + /** + * Tests whether we can NULLify a parent bean + * page->book if the parent (book) is already + * NULL. (isset vs array_key_exists bug). + * + * @return void + */ + public function testUnsetParent() + { + R::nuke(); + $book = R::dispense( 'book' ); + $book->title = 'My Book'; + $page = R::dispense( 'page' ); + $page->text = 'Lorem Ipsum'; + $book->ownPage[] = $page; + R::store( $book ); + $page = $page->fresh(); + R::freeze( TRUE ); + asrt( (int) $page->book->id, (int) $book->id ); + unset( $page->book ); + R::store( $page ); + $page = $page->fresh(); + asrt( (int) $page->book->id, (int) $book->id ); + $page->book = NULL; + R::store( $page ); + $page = $page->fresh(); + asrt( $page->book, NULL ); + asrt( $page->book_id, NULL ); + asrt( $page->bookID, NULL ); + asrt( $page->bookId, NULL ); + $page = R::dispense( 'page' ); + $page->text = 'Another Page'; + $page->book = NULL; + try { + R::store( $page ); + fail(); + } catch( \Exception $exception ) { + pass(); + } + unset($page->book); + R::store($page); + $page = $page->fresh(); + $page->book = NULL; //this must set field id to NULL not ADD column! + try { + R::store($page); + pass(); + } catch( \Exception $exception ) { + fail(); + } + $page = $page->fresh(); + $page->book = NULL; + R::store( $page ); + $page = $page->fresh(); + asrt( is_null( $page->book_id ), TRUE ); + $page->book = $book; + R::store( $page ); + $page = $page->fresh(); + asrt( (int) $page->book->id, (int) $book->id ); + $page->book = NULL; + R::store( $page ); + asrt( is_null( $page->book_id ), TRUE ); + asrt( is_null( $page->book ), TRUE ); + R::freeze( FALSE ); + } + + /** + * Test nullifying aliased parent. + * + * @return void + */ + public function testUnsetAliasedParent() + { + R::nuke(); + $book = R::dispense( 'book' ); + $author = R::dispense( 'author' ); + $book->coauthor = $author; + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), FALSE ); + unset( $book->coauthor ); + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), FALSE ); + $book->coauthor = NULL; + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), TRUE ); + R::trash( $book ); + R::trash( $author ); + R::freeze( TRUE ); + $book = R::dispense( 'book' ); + $author = R::dispense( 'author' ); + $book->coauthor = $author; + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), FALSE ); + unset( $book->coauthor ); + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), FALSE ); + $book->coauthor = NULL; + R::store( $book ); + $book = $book->fresh(); + asrt( is_null( $book->fetchAs('author')->coauthor ), TRUE ); + R::trash( $book ); + R::trash( $author ); + R::freeze( FALSE ); + } + + /** + * Test NULL handling, setting a property to NULL must + * cause a change. + * + * @return void + */ + public function testBasicNullHandling() + { + // NULL can change bean + $bean = R::dispense( 'bean' ); + $bean->bla = 'a'; + R::store( $bean ); + $bean = $bean->fresh(); + asrt( $bean->hasChanged( 'bla' ), FALSE ); + $bean->bla = NULL; + asrt( $bean->hasChanged( 'bla' ), TRUE ); + // NULL test + $page = R::dispense( 'page' ); + $book = R::dispense( 'book' ); + $page->title = 'a NULL page'; + $page->book = $book; + $book->title = 'Why NUll is painful..'; + R::store( $page ); + $bookid = $page->book->id; + unset( $page->book ); + $id = R::store( $page ); + $page = R::load( 'page', $id ); + $page->title = 'another title'; + R::store( $page ); + pass(); + $page = R::load( 'page', $id ); + $page->title = 'another title'; + $page->book_id = NULL; + R::store( $page ); + pass(); + } + + /** + * Here we test whether the column type is set correctly. + * Normally if you store NULL, the smallest type (bool/set) will + * be selected. However in case of a foreign key type INT should + * be selected because fks columns require matching types. + * + * @return void + */ + public function ColumnType() + { + + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->ownPage[] = $page; + R::store( $book ); + pass(); + asrt( $page->getMeta( 'cast.book_id' ), 'id' ); + } + + /** + * Test meta column type. + * + * @return void + */ + public function TypeColumn() + { + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $page->book = $book; + R::store( $page ); + pass(); + asrt( $page->getMeta( 'cast.book_id' ), 'id' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole.php new file mode 100644 index 0000000..f947fa6 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole.php @@ -0,0 +1,36 @@ +setMode( $mode ); + $debugger->setParamStringLength( 20 ); + ob_start(); + if (!is_null($bindings)) { + $debugger->log($query, $bindings); + } else { + $debugger->log($query); + } + $out = ob_get_contents(); + ob_clean(); + ob_end_flush(); + $logs = $debugger->getLogs(); + $log = reset($logs); + $log = str_replace( "\e[32m", '', $log ); + $log = str_replace( "\e[39m", '', $log ); + asrt($log, $expected); + if (!$mode) { + asrt($out, $expected2); + } + $debugger->clear(); + } + + /** + * Tests the bean dump function used to inspect + * the contents of a bean. + * + * @return void + */ + public function testDump() + { + $beans = R::dispense( 'bean', 2 ); + $beans[0]->name = 'hello'; + $beans[1]->name = 'world'; + $array = R::dump($beans); + asrt( is_array( $array ), TRUE ); + foreach( $array as $item ) { + asrt( is_string( $item ), TRUE ); + } + $beans[1]->name = 'world, and a very long string that should be shortened'; + $array = R::dump($beans); + asrt( is_array( $array ), TRUE ); + asrt( strpos( $array[1], '...' ), 35 ); + dmp( $beans ); + pass(); + //test wrong input + asrt( is_array( R::dump( NULL ) ), TRUE ); + asrt( count( R::dump( NULL ) ), 0 ); + asrt( is_array( R::dump( '' ) ), TRUE ); + asrt( count( R::dump( '' ) ), 0 ); + asrt( is_array( R::dump( 1 ) ), TRUE ); + asrt( count( R::dump( 1 ) ), 0 ); + asrt( is_array( R::dump( TRUE ) ), TRUE ); + asrt( count( R::dump( FALSE ) ), 0 ); + } + + /** + * Tests debugging with parameters. + * + * @return void + */ + public function testDebugger2() + { + testpack( 'Test debugger with params.' ); + $this->testDebug('SELECT * FROM table', NULL, 'SELECT * FROM table'); + $this->testDebug('SELECT * FROM book WHERE title = ?', array('my book'), 'SELECT * FROM book WHERE title = \'my book\''); + $this->testDebug('title = ? OR title = ?', array('book1', 'book2'), 'title = \'book1\' OR title = \'book2\''); + $this->testDebug('title = ? OR price = ?', array('book1', 20), 'title = \'book1\' OR price = 20'); + $this->testDebug('number IN (?,?)', array(8,900), 'number IN (8,900)'); + $this->testDebug('?', array(20), '20'); + $this->testDebug('?,?', array('test',20), '\'test\',20'); + $this->testDebug('?', array( NULL ), 'NULL'); + $this->testDebug('title = ?', array( NULL ), 'title = NULL'); + $this->testDebug('?,?', array( NULL,NULL ), 'NULL,NULL'); + $this->testDebug('title = ?', array('a very long title that should be shortened'), 'title = \'a very long title th... \''); + $this->testDebug('title = ? OR title = ?', array('a very long title that should be shortened', 'another long title that should be shortened'), 'title = \'a very long title th... \' OR title = \'another long title t... \''); + $this->testDebug('title = ? OR ?', array('a very long title that should be shortened', NULL), 'title = \'a very long title th... \' OR NULL'); + $this->testDebug('?,?', array('hello'), '\'hello\',:slot1'); + $this->testDebug('title = :first OR title = :second', array(':first'=>'book1', ':second'=>'book2'), 'title = \'book1\' OR title = \'book2\''); + $this->testDebug('title = :first OR price = :second', array(':first'=>'book1', ':second'=>20), 'title = \'book1\' OR price = 20'); + $this->testDebug('number IN (:one,:two)', array(':one'=>8, ':two'=>900), 'number IN (8,900)'); + $this->testDebug('number IN (:one,:two)', array(':one'=>8, ':two'=>900, ':three'=>999), 'number IN (8,900)'); + $this->testDebug('number IN (:one,:two)', array(':three'=>999, ':one'=>8, ':two'=>900), 'number IN (8,900)'); + $this->testDebug('number IN (:one,:two)', array(':one'=>8, ':three'=>999, ':two'=>900), 'number IN (8,900)'); + $this->testDebug(':a', array(':a'=>20), '20'); + $this->testDebug(':a,?', array(':a'=>20, 30), '20,30'); + $this->testDebug(':a,?', array(30, ':a'=>20), '20,30'); + $this->testDebug('?,?', array('test',20), '\'test\',20'); + $this->testDebug('?', array( NULL ), 'NULL'); + $this->testDebug('title = ?', array( NULL ), 'title = NULL'); + $this->testDebug('?,?', array( NULL,NULL ), 'NULL,NULL'); + $this->testDebug('title = ?', array('a very long title that should be shortened'), 'title = \'a very long title th... \''); + $this->testDebug('title = ? OR title = ?', array('a very long title that should be shortened', 'another long title that should be shortened'), 'title = \'a very long title th... \' OR title = \'another long title t... \''); + $this->testDebug('title = ? OR ?', array('a very long title that should be shortened', NULL), 'title = \'a very long title th... \' OR NULL'); + $this->testDebug('?,?', array('hello'), '\'hello\',:slot1'); + $this->testDebug('hello ?', 'world', 'hello ?'); + $this->testDebug(':slot0 :slot1 :slot2 :slot3 :slot4 :slot5 :slot6 :slot7 :slot8 :slot9 :slot10', array( + 'a','b','c','d','e','f','g','h','i','j','k' + ),"'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k'"); + $this->testDebug('? ? ? ? ? ? ? ? ? ? ?', array( + 'a','b','c','d','e','f','g','h','i','j','k' + ),"'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k'"); + $this->testDebug(':a :aaa :ab', array(':a'=>1,':aaa'=>2,':ab'=>3),'1 2 3'); + Debugger::setOverrideCLIOutput( TRUE ); + $this->testDebug('SELECT * FROM table', NULL, 'SELECT * FROM table', 0, 'SELECT * FROM table
'); + $this->testDebug('DROP TABLE myths', NULL, 'DROP TABLE myths', 0, 'DROP TABLE myths
'); + $this->testDebug('SELECT * FROM book WHERE title = ?', array('my book'), 'SELECT * FROM book WHERE title = \'my book\''); + Debugger::setOverrideCLIOutput( FALSE ); + } + + /** + * Test facade fancyDebug function. + * + * @return void + */ + public function testDebug2InFacade() + { + R::fancyDebug( TRUE ); + pass(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Export.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Export.php new file mode 100644 index 0000000..35ef368 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Export.php @@ -0,0 +1,130 @@ +import( array( 'a' => 1, 'b' => 2 ) ); + $bean->setMeta( 'justametaproperty', 'hellothere' ); + $arr = $bean->export(); + asrt( is_array( $arr ), TRUE ); + asrt( isset( $arr["a"] ), TRUE ); + asrt( isset( $arr["b"] ), TRUE ); + asrt( $arr["a"], 1 ); + asrt( $arr["b"], 2 ); + asrt( isset( $arr["__info"] ), FALSE ); + $arr = $bean->export( TRUE ); + asrt( isset( $arr["__info"] ), TRUE ); + asrt( $arr["a"], 1 ); + asrt( $arr["b"], 2 ); + $exportBean = $redbean->dispense( 'abean' ); + $exportBean->setMeta( 'metaitem.bla', 1 ); + $exportedBean = $exportBean->export( TRUE ); + asrt( $exportedBean["__info"]["metaitem.bla"], 1 ); + asrt( $exportedBean["__info"]["type"], "abean" ); + // Can we determine whether a bean is empty? + testpack( 'test $bean->isEmpty() function' ); + $bean = R::dispense( 'bean' ); + asrt( $bean->isEmpty(), TRUE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + $bean->property = 1; + asrt( $bean->isEmpty(), FALSE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + $bean->property = 0; + asrt( $bean->isEmpty(), TRUE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + $bean->property = FALSE; + asrt( $bean->isEmpty(), TRUE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + $bean->property = NULL; + asrt( $bean->isEmpty(), TRUE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + unset( $bean->property ); + asrt( $bean->isEmpty(), TRUE ); + asrt( ( count( $bean ) > 0 ), TRUE ); + // Export bug I found + $bandmember = R::dispense( 'bandmember' ); + $bandmember->name = 'Duke'; + $instrument = R::dispense( 'instrument' ); + $instrument->name = 'Piano'; + $bandmember->ownInstrument[] = $instrument; + $a = R::exportAll( $bandmember ); + pass(); + asrt( isset( $a[0] ), TRUE ); + asrt( (int) $a[0]['id'], 0 ); + asrt( $a[0]['name'], 'Duke' ); + asrt( $a[0]['ownInstrument'][0]['name'], 'Piano' ); + R::nuke(); + $v = R::dispense( 'village' ); + $b = R::dispense( 'building' ); + $v->name = 'a'; + $b->name = 'b'; + $v->ownBuilding[] = $b; + $id = R::store( $v ); + $a = R::exportAll( $v ); + asrt( $a[0]['name'], 'a' ); + asrt( $a[0]['ownBuilding'][0]['name'], 'b' ); + $v = R::load( 'village', $id ); + $b2 = R::dispense( 'building' ); + $b2->name = 'c'; + $v->ownBuilding[] = $b2; + $a = R::exportAll( $v ); + asrt( $a[0]['name'], 'a' ); + asrt( $a[0]['ownBuilding'][0]['name'], 'b' ); + asrt( count( $a[0]['ownBuilding'] ), 2 ); + list( $r1, $r2 ) = R::dispense( 'army', 2 ); + $r1->name = '1'; + $r2->name = '2'; + $v->sharedArmy[] = $r2; + $a = R::exportAll( $v ); + asrt( count( $a[0]['sharedArmy'] ), 1 ); + R::store( $v ); + $v = R::load( 'village', $id ); + $a = R::exportAll( $v ); + asrt( count( $a[0]['sharedArmy'] ), 1 ); + asrt( $a[0]['name'], 'a' ); + asrt( $a[0]['ownBuilding'][0]['name'], 'b' ); + asrt( count( $a[0]['ownBuilding'] ), 2 ); + $v->sharedArmy[] = $r1; + $a = R::exportAll( $v ); + asrt( count( $a[0]['sharedArmy'] ), 2 ); + $v = R::load( 'village', $id ); + $a = R::exportAll( $v ); + asrt( count( $a[0]['sharedArmy'] ), 1 ); + $v->sharedArmy[] = $r1; + R::store( $v ); + $v = R::load( 'village', $id ); + $a = R::exportAll( $v ); + asrt( count( $a[0]['sharedArmy'] ), 2 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Fusebox.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Fusebox.php new file mode 100644 index 0000000..076df5d --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Fusebox.php @@ -0,0 +1,66 @@ +taste() ); + asrt( 'tomato', $soup->flavour ); + } + + /** + * Test unboxing + * + * @param OODBBean $bean + */ + private function giveMeBean( OODBBean $bean ) + { + asrt( ( $bean instanceof OODBBean ), TRUE ); + asrt( 'A bit too salty', $bean->taste() ); + asrt( 'tomato', $bean->flavour ); + } + + /** + * Test boxing. + * + * @return void + */ + public function testBasicBox() + { + $soup = R::dispense( 'soup' ); + $soup->flavour = 'tomato'; + $this->giveMeSoup( $soup->box() ); + $this->giveMeBean( $soup->box()->unbox() ); + $this->giveMeBean( $soup ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Glue.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Glue.php new file mode 100644 index 0000000..d0845e0 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Glue.php @@ -0,0 +1,63 @@ +glueSQLCondition( ' name = ? '), ' WHERE name = ? ' ); + asrt( $writer->glueSQLCondition( ' value1 > ? OR value < ? '), ' WHERE value1 > ? OR value < ? ' ); + //Does it recognize NON-WHERE conditions? - usual suspects + asrt( $writer->glueSQLCondition( ' ORDER BY name '), ' ORDER BY name ' ); + asrt( $writer->glueSQLCondition( ' LIMIT 10 '), ' LIMIT 10 ' ); + asrt( $writer->glueSQLCondition( ' OFFSET 20 '), ' OFFSET 20 ' ); + //highly doubtful but who knows... - I think nobody will ever use this in a query snippet. + asrt( $writer->glueSQLCondition( ' GROUP BY grp '), ' GROUP BY grp ' ); + asrt( $writer->glueSQLCondition( ' HAVING x = ? '), ' HAVING x = ? ' ); + //can we replace WHERE with AND ? + asrt( $writer->glueSQLCondition( ' AND name = ? ', QueryWriter::C_GLUE_WHERE ), ' WHERE name = ? ' ); + //can we glue with AND instead of WHERE ? + asrt( $writer->glueSQLCondition( ' value1 > ? OR value < ? ', QueryWriter::C_GLUE_AND ), ' AND value1 > ? OR value < ? ' ); + //non-cases + asrt( $writer->glueSQLCondition( ' GROUP BY grp ', QueryWriter::C_GLUE_WHERE ), ' GROUP BY grp ' ); + asrt( $writer->glueSQLCondition( ' GROUP BY grp ', QueryWriter::C_GLUE_AND ), ' GROUP BY grp ' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Import.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Import.php new file mode 100644 index 0000000..77ee333 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Import.php @@ -0,0 +1,283 @@ +import( array( ' title ' => 'my book' ), array( ' title ' ), TRUE ); + asrt( $book[' title '], 'my book' ); + } + + /** + * Test multi array dispense import. + * + * @return void + */ + public function testMultiRecurImport() + { + $books = R::dispense( array( + array( '_type' => 'book', 'title' => 'book one' ), + array( '_type' => 'book', 'title' => 'book two' ), + ) ); + asrt( is_array( $books ), TRUE ); + asrt( count( $books ), 2 ); + $book = reset( $books ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( $book->title, 'book one' ); + $book = next( $books ); + asrt( ( $book instanceof OODBBean ), TRUE ); + asrt( $book->title, 'book two' ); + } + + /** + * Test recursive imports (formely known as R::graph). + * + * @return void + */ + public function testRecursiveImport() + { + $book = R::dispense( + array( + '_type'=>'book', + 'title'=>'The magic book', + 'ownPageList' => array( + array( + '_type' => 'page', + 'content' => 'magic potions', + ), + array( + '_type' => 'page', + 'content' => 'magic spells', + ) + ) + ) + ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'The magic book' ); + $pages = $book->with(' ORDER BY content ASC ')->ownPageList; + asrt( count($pages), 2 ); + $page1 = array_shift( $pages ); + asrt( $page1->content, 'magic potions' ); + $page2 = array_shift( $pages ); + asrt( $page2->content, 'magic spells' ); + R::nuke(); + $book = R::dispense( + array( + '_type'=>'book', + 'title'=>'The magic book', + 'author' => array( + '_type' => 'author', + 'name' => 'Dr. Evil' + ), + 'coAuthor' => array( + '_type' => 'author', + 'name' => 'Dr. Creepy' + ), + 'ownPageList' => array( + array( + '_type' => 'page', + 'content' => 'magic potions', + 'ownRecipe' => array( + 'a' => array('_type'=>'recipe', 'name'=>'Invisibility Salad'), + 'b' => array('_type'=>'recipe', 'name'=>'Soup of Madness'), + 'c' => array('_type'=>'recipe', 'name'=>'Love cake'), + ) + ), + array( + '_type' => 'page', + 'content' => 'magic spells', + ) + ), + 'sharedCategory' => array( + array( + '_type' => 'category', + 'label' => 'wizardry' + ), + ) + ) + ); + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( $book->title, 'The magic book' ); + $pages = $book->with(' ORDER BY content ASC ')->ownPageList; + asrt( count($pages), 2 ); + $page1 = array_shift( $pages ); + asrt( $page1->content, 'magic potions' ); + $page2 = array_shift( $pages ); + asrt( $page2->content, 'magic spells' ); + $recipes = $page1->with(' ORDER BY name ASC ')->ownRecipeList; + asrt( count( $recipes ), 3 ); + $recipe1 = array_shift( $recipes ); + asrt( $recipe1->name, 'Invisibility Salad' ); + $recipe2 = array_shift( $recipes ); + asrt( $recipe2->name, 'Love cake' ); + $recipe3 = array_shift( $recipes ); + asrt( $recipe3->name, 'Soup of Madness' ); + $categories = $book->sharedCategoryList; + asrt( count($categories), 1 ); + $category = reset( $categories ); + asrt( $category->label, 'wizardry' ); + asrt( $book->author->name, 'Dr. Evil' ); + asrt( $book->fetchAs('author')->coAuthor->name, 'Dr. Creepy' ); + try { + $list = R::dispense( array() ); + pass(); + asrt( is_array( $list ), TRUE ); + asrt( count( $list ), 0 ); + } catch ( RedException $ex ) { + pass(); + } + try { + R::dispense( array( array() ) ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + try { + R::dispense( array( 'a' ) ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + try { + R::dispense( array( 'property' => 'value' ) ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + } + + /** + * Test import from and tainted. + * + * @return void + */ + public function testImportFromAndTainted() + { + testpack( 'Test importFrom() and Tainted' ); + $bean = R::dispense( 'bean' ); + R::store( $bean ); + $bean->name = 'abc'; + asrt( $bean->getMeta( 'tainted' ), TRUE ); + R::store( $bean ); + asrt( $bean->getMeta( 'tainted' ), FALSE ); + $copy = R::dispense( 'bean' ); + R::store( $copy ); + $copy = R::load( 'bean', $copy->id ); + asrt( $copy->getMeta( 'tainted' ), FALSE ); + $copy->import( array( 'name' => 'xyz' ) ); + asrt( $copy->getMeta( 'tainted' ), TRUE ); + $copy->setMeta( 'tainted', FALSE ); + asrt( $copy->getMeta( 'tainted' ), FALSE ); + $copy->importFrom( $bean ); + asrt( $copy->getMeta( 'tainted' ), TRUE ); + testpack( 'Test basic import() feature.' ); + $bean = R::dispense('bean'); + $bean->import( array( "a" => 1, "b" => 2 ) ); + asrt( $bean->a, 1 ); + asrt( $bean->b, 2 ); + $bean->import( array( "a" => 3, "b" => 4 ), "a,b" ); + asrt( $bean->a, 3 ); + asrt( $bean->b, 4 ); + $bean->import( array( "a" => 5, "b" => 6 ), " a , b " ); + asrt( $bean->a, 5 ); + asrt( $bean->b, 6 ); + $bean->import( array( "a" => 1, "b" => 2 ) ); + testpack( 'Test inject() feature.' ); + $coffee = R::dispense( 'coffee' ); + $coffee->id = 2; + $coffee->liquid = 'black'; + $cup = R::dispense( 'cup' ); + $cup->color = 'green'; + // Pour coffee in cup + $cup->inject( $coffee ); + // Do we still have our own property? + asrt( $cup->color, 'green' ); + // Did we pour the liquid in the cup? + asrt( $cup->liquid, 'black' ); + // Id should not be transferred + asrt( $cup->id, 0 ); + } + + /** + * Test import using array access functions + * + * @return void + */ + public function testArrayAccess() + { + $book = R::dispense( 'book' ); + $book->isAwesome = TRUE; + asrt( isset( $book->isAwesome ), TRUE ); + $book = R::dispense( 'book' ); + $book['isAwesome'] = TRUE; + asrt( isset( $book->isAwesome ), TRUE ); + $book = R::dispense( 'book' ); + $book['xownPageList'] = R::dispense( 'page', 2 ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPageList ), TRUE ); + $book = R::dispense( 'book' ); + $book['ownPageList'] = R::dispense( 'page', 2 ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPageList ), TRUE ); + $book = R::dispense( 'book' ); + $book['xownPage'] = R::dispense( 'page', 2 ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPageList ), TRUE ); + $book = R::dispense( 'book' ); + $book['ownPage'] = R::dispense( 'page', 2 ); + asrt( isset( $book->ownPage ), TRUE ); + asrt( isset( $book->xownPage ), TRUE ); + asrt( isset( $book->ownPageList ), TRUE ); + asrt( isset( $book->xownPageList ), TRUE ); + $book = R::dispense( 'book' ); + $book['sharedTag'] = R::dispense( 'tag', 2 ); + asrt( isset( $book->sharedTag ), TRUE ); + asrt( isset( $book->sharedTagList ), TRUE ); + $book = R::dispense( 'book' ); + $book['sharedTagList'] = R::dispense( 'tag', 2 ); + asrt( isset( $book->sharedTag ), TRUE ); + asrt( isset( $book->sharedTagList ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Labels.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Labels.php new file mode 100644 index 0000000..7259dc9 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Labels.php @@ -0,0 +1,47 @@ +setMeta( "this.is.a.custom.metaproperty", "yes" ); + asrt( $bean->getMeta( "this.is.a.custom.metaproperty" ), "yes" ); + asrt( $bean->getMeta( "nonexistant" ), NULL ); + asrt( $bean->getMeta( "nonexistant", "abc" ), "abc" ); + asrt( $bean->getMeta( "nonexistant.nested" ), NULL ); + asrt( $bean->getMeta( "nonexistant,nested", "abc" ), "abc" ); + $bean->setMeta( "test.two", "second" ); + asrt( $bean->getMeta( "test.two" ), "second" ); + $bean->setMeta( "another.little.property", "yes" ); + asrt( $bean->getMeta( "another.little.property" ), "yes" ); + asrt( $bean->getMeta( "test.two" ), "second" ); + // Copy Metadata + $bean = new OODBBean; + $bean->setMeta( "meta.meta", "123" ); + $bean2 = new OODBBean; + asrt( $bean2->getMeta( "meta.meta" ), NULL ); + $bean2->copyMetaFrom( $bean ); + asrt( $bean2->getMeta( "meta.meta" ), "123" ); + } + + /** + * Meta properties should not be saved. + * + * @return void + */ + public function testMetaPersist() + { + $bean = R::dispense( 'bean' ); + $bean->property = 'test'; + $bean->setMeta( 'meta', 'hello' ); + R::store( $bean ); + asrt( $bean->getMeta( 'meta' ), 'hello' ); + $bean = $bean->fresh(); + asrt( $bean->getMeta( 'meta' ), NULL ); + } + + /** + * You cant access meta data using the array accessors. + * + * @return void + */ + public function testNoArrayMetaAccess() + { + $bean = R::dispense( 'bean' ); + $bean->setMeta( 'greet', 'hello' ); + asrt( isset( $bean['greet'] ), FALSE ); + asrt( isset( $bean['__info']['greet'] ), FALSE ); + asrt( isset( $bean['__info'] ), FALSE ); + asrt( isset( $bean['meta'] ), FALSE ); + asrt( count( $bean ), 1 ); + } + + /** + * Test meta masks. + * + * @return void + */ + public function testMetaMask() + { + $rows = array( + array('id'=>1, 'name'=>'a', '__meta_rows'=>2, '__meta_columns'=>4), + array('id'=>2, 'name'=>'b', '__meta_rows'=>2, '__meta_columns'=>4) + ); + $books = R::convertToBeans( 'book', $rows, '__meta' ); + $book = reset($books); + $data = $book->getMeta('data.bundle'); + asrt( $data['__meta_rows'], 2 ); + asrt( $data['__meta_columns'], 4 ); + $books = R::convertToBeans( 'book', $rows, array( '__meta_rows', '__meta_columns' ) ); + $book = reset($books); + $data = $book->getMeta('data.bundle'); + asrt( $data['__meta_rows'], 2 ); + asrt( $data['__meta_columns'], 4 ); + $books = R::convertToBeans( 'book', $rows, array( '__meta_rows' ) ); + $book = reset($books); + $data = $book->getMeta('data.bundle'); + asrt( $data['__meta_rows'], 2 ); + asrt( isset($data['__meta_columns']), FALSE ); + $books = R::convertToBeans( 'book', $rows, array( '__meta_rows', TRUE ) ); + $book = reset($books); + $data = $book->getMeta('data.bundle'); + asrt( isset($data['__meta_rows']), FALSE ); + asrt( isset($data['__meta_columns']), FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Misc.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Misc.php new file mode 100644 index 0000000..1550c7c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Misc.php @@ -0,0 +1,624 @@ +getMessage(), 'Unsupported database (blackhole).' ); + $rpdo = new \TestRPO( new \MockPDO ); + asrt( @$rpdo->testCap( 'utf8mb4' ), FALSE ); + } + + + + /** + * Misc tests. + * 'Tests' almost impossible lines to test. + * Not sure if very useful. + * + * @return void + */ + public function testMisc() + { + $null = R::getDatabaseAdapter()->getDatabase()->stringifyFetches( TRUE ); + asrt( NULL, $null ); + R::getDatabaseAdapter()->getDatabase()->stringifyFetches( FALSE ); + } + + /** + * Test whether we can toggle enforcement of the RedBeanPHP + * naming policy. + * + * @return void + */ + public function testEnforceNamingPolicy() + { + \RedBeanPHP\Util\DispenseHelper::setEnforceNamingPolicy( FALSE ); + R::dispense('a_b'); + pass(); + \RedBeanPHP\Util\DispenseHelper::setEnforceNamingPolicy( TRUE ); + try { + R::dispense('a_b'); + fail(); + } catch( \Exception $e ) { + pass(); + } + } + + /** + * Test R::csv() + * + * @return void + */ + public function testCSV() + { + \RedBeanPHP\Util\QuickExport::operation( 'test', TRUE, TRUE ); + R::nuke(); + $city = R::dispense('city'); + $city->name = 'city1'; + $city->region = 'region1'; + $city->population = '200k'; + R::store($city); + $qe = new \RedBeanPHP\Util\QuickExport( R::getToolBox() ); + $out = $qe->csv( 'SELECT `name`, population FROM city WHERE region = :region ', + array( ':region' => 'region1' ), + array( 'city', 'population' ), + '/tmp/cities.csv' + ); + $out = preg_replace( '/\W/', '', $out ); + asrt( 'PragmapublicExpires0CacheControlmustrevalidatepostcheck0precheck0CacheControlprivateContentTypetextcsvContentDispositionattachmentfilenamecitiescsvContentTransferEncodingbinarycitypopulationcity1200k', $out ); + } + + /** + * Test whether sqlStateIn can detect lock timeouts. + * + * @return void + */ + public function testLockTimeoutDetection() + { + $queryWriter = new MySQLQueryWriter( R::getDatabaseAdapter() ); + asrt($queryWriter->sqlStateIn('HY000', array(QueryWriter::C_SQLSTATE_LOCK_TIMEOUT), array(0,'1205')), TRUE); + $queryWriter = new PostgresQueryWriter( R::getDatabaseAdapter() ); + asrt($queryWriter->sqlStateIn('55P03', array(QueryWriter::C_SQLSTATE_LOCK_TIMEOUT), array(0,'')), TRUE); + } + + /** + * Tests setOption + * + * @return void + */ + public function testSetOptionFalse() + { + $false = R::getDatabaseAdapter()->setOption( 'unknown', 1 ); + asrt( $false, FALSE ); + } + + /** + * Test whether we can use the JSONSerializable interface and + * whether old-style JSON is still the same (backwards compatibility). + * + * @return void + */ + public function testJSONSerialize() + { + $hotel = R::dispense( 'hotel' ); + $hotel->name = 'Overlook'; + $room = R::dispense( 'room' ); + $room->number = 237; + $hotel->ownRoomList[] = $room; + $shine = (string) $hotel; + asrt( $shine, '{"id":0,"name":"Overlook"}' ); //basic JSON + $shine = json_encode( $hotel->jsonSerialize() ); //As of PHP 5.4 json_encode() will call jsonSerializable + asrt( $shine, '{"id":0,"name":"Overlook","ownRoom":[{"id":0,"number":237}]}' ); //should get full JSON + } + + /** + * Tests max parameter binding. + * + * @return void + */ + public function testIntegerBindingMax() + { + if ( defined( 'HHVM_VERSION' ) ) return; //not for hhvm... + $driver = new RPDO( 'test-sqlite-53', 'user', 'pass' ); + $max = $driver->getIntegerBindingMax(); + asrt( $max, 2147483647 ); + $driver = new RPDO( 'cubrid', 'user', 'pass' ); + $max = $driver->getIntegerBindingMax(); + asrt( $max, 2147483647 ); + $driver = new RPDO( 'other', 'user', 'pass' ); + $max = $driver->getIntegerBindingMax(); + asrt( $max, PHP_INT_MAX ); + } + + /** + * Should not be able to pass invalid mode (must be 0 or 1). + * + */ + public function testInvalidDebugModeException() + { + try { + R::debug( TRUE, 6 ); + fail(); + } catch ( RedException $e ) { + pass(); + } + R::debug( FALSE ); + } + + /** + * Adding a database twice no longer allowed, causes confusion + * and possible damage. + */ + public function testAddingTwice() + { + testpack( 'Test adding DB twice.' ); + + try { + R::addDatabase( 'sqlite', '' ); + fail(); + } catch ( RedException $ex ) { + pass(); + } + } + + /** + * Tests whether getID never produces a notice. + * + * @return void + */ + public function testGetIDShouldNeverPrintNotice() + { + set_error_handler(function($err, $errStr){ + die('>>>>FAIL :'.$err.' '.$errStr); + }); + $bean = new OODBBean; + $bean->getID(); + restore_error_handler(); + pass(); + } + + /** + * Tests setProperty. + * + * @return void + */ + public function testSetProperty() + { + $bean = R::dispense( 'bean' ); + $bean->item = 2; + $bean->ownBean = R::dispense( 'bean', 2 ); + R::store( $bean ); + $bean = $bean->fresh(); + $bean->ownBean; + $bean->setProperty( 'ownBean', array(), FALSE, FALSE ); + asrt( count( $bean->ownBean ), 0 ); + asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 2 ); + asrt( $bean->isTainted(), TRUE ); + $bean->setProperty( 'ownBean', array(), TRUE, FALSE ); + asrt( count( $bean->ownBean ), 0 ); + asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 ); + asrt( $bean->isTainted(), TRUE ); + $bean = $bean->fresh(); + $bean->setProperty( 'ownBean', array(), TRUE, FALSE ); + asrt( count( $bean->ownBean ), 0 ); + asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 ); + asrt( $bean->isTainted(), FALSE ); + $bean = $bean->fresh(); + $bean->setProperty( 'ownBean', array(), TRUE, TRUE ); + asrt( count( $bean->ownBean ), 0 ); + asrt( count( $bean->getMeta( 'sys.shadow.ownBean' ) ), 0 ); + asrt( $bean->isTainted(), TRUE ); + } + + /** + * Tests beansToArray(). + * + * @return void + */ + public function testBeansToArray() + { + testpack('Test R::beansToArray method'); + $bean1 = R::dispense( 'bean' ); + $bean1->name = 'hello'; + $bean2 = R::dispense( 'bean' ); + $bean2->name = 'world'; + $beans = array( $bean1, $bean2 ); + $array = R::beansToArray( $beans ); + asrt( $array[0]['name'], 'hello' ); + asrt( $array[1]['name'], 'world' ); + } + + /** + * Test debugging with custom logger. + * + * @return void + */ + public function testDebugCustomLogger() + { + testpack( 'Test debug mode with custom logger' ); + $pdoDriver = new RPDO( R::getDatabaseAdapter()->getDatabase()->getPDO() ); + $customLogger = new \CustomLogger; + $pdoDriver->setDebugMode( TRUE, $customLogger ); + $pdoDriver->Execute( 'SELECT 123' ); + asrt( count( $customLogger->getLogMessage() ), 1 ); + $pdoDriver->setDebugMode( TRUE, NULL ); + asrt( ( $pdoDriver->getLogger() instanceof RDefault ), TRUE ); + testpack( 'Test bean->getProperties method' ); + $bean = R::dispense( 'bean' ); + $bean->property = 'hello'; + $props = $bean->getProperties(); + asrt( isset( $props['property'] ), TRUE ); + asrt( $props['property'], 'hello' ); + + } + + /** + * Test Facade transactions. + * + * @return void + * + * @throws\Exception + */ + public function testTransactionInFacade() + { + testpack( 'Test transaction in facade' ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + R::store( $bean ); + R::trash( $bean ); + R::freeze( TRUE ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + R::store( $bean ); + asrt( R::count( 'bean' ), 1 ); + R::trash( $bean ); + asrt( R::count( 'bean' ), 0 ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + $id = R::transaction( function() use( &$bean ) { + return R::transaction( function() use( &$bean ) { + return R::store( $bean ); + } ); + } ); + asrt( (int) $id, (int) $bean->id ); + R::trash( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + $id = R::transaction( function() use( &$bean ) { + return R::store( $bean ); + } ); + asrt( (int) $id, (int) $bean->id ); + R::trash( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + try { + R::transaction( function () use ( $bean ) { + R::store( $bean ); + R::transaction( function () { + throw new\Exception(); + } ); + } ); + } catch (\Exception $e ) { + pass(); + } + asrt( R::count( 'bean' ), 0 ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + try { + R::transaction( function () use ( $bean ) { + R::transaction( function () use ( $bean ) { + R::store( $bean ); + throw new\Exception(); + } ); + } ); + } catch (\Exception $e ) { + pass(); + } + asrt( R::count( 'bean' ), 0 ); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + try { + R::transaction( function () use ( $bean ) { + R::transaction( function () use ( $bean ) { + R::store( $bean ); + } ); + } ); + } catch (\Exception $e ) { + pass(); + } + asrt( R::count( 'bean' ), 1 ); + R::freeze( FALSE ); + try { + R::transaction( 'nope' ); + fail(); + } catch (\Exception $e ) { + pass(); + } + testpack( 'Test Camelcase 2 underscore' ); + $names = array( + 'oneACLRoute' => 'one_acl_route', + 'ALLUPPERCASE' => 'alluppercase', + 'clientServerArchitecture' => 'client_server_architecture', + 'camelCase' => 'camel_case', + 'peer2peer' => 'peer2peer', + 'fromUs4You' => 'from_us4_you', + 'lowercase' => 'lowercase', + 'a1A2b' => 'a1a2b', + ); + $bean = R::dispense( 'bean' ); + foreach ( $names as $name => $becomes ) { + $bean->$name = 1; + asrt( isset( $bean->$becomes ), TRUE ); + } + testpack( 'Misc Tests' ); + R::debug( 1 ); + flush(); + ob_start(); + R::exec( 'SELECT 123' ); + $out = ob_get_contents(); + ob_end_clean(); + flush(); + pass(); + asrt( ( strpos( $out, 'SELECT 123' ) !== FALSE ), TRUE ); + R::debug( 0 ); + flush(); + ob_start(); + R::exec( 'SELECT 123' ); + $out = ob_get_contents(); + ob_end_clean(); + flush(); + pass(); + asrt( $out, '' ); + R::debug( 0 ); + pass(); + testpack( 'test to string override' ); + $band = R::dispense( 'band' ); + $str = strval( $band ); + asrt( $str, 'bigband' ); + testpack( 'test whether we can use isset/set in model' ); + $band->setProperty( 'property1', 123 ); + asrt( $band->property1, 123 ); + asrt( $band->checkProperty( 'property1' ), TRUE ); + asrt( $band->checkProperty( 'property2' ), FALSE ); + $band = new \Model_Band; + $bean = R::dispense( 'band' ); + $bean->property3 = 123; + $band->loadBean( $bean ); + $bean->property4 = 345; + $band->setProperty( 'property1', 123 ); + asrt( $band->property1, 123 ); + asrt( $band->checkProperty( 'property1' ), TRUE ); + asrt( $band->checkProperty( 'property2' ), FALSE ); + asrt( $band->property3, 123 ); + asrt( $band->property4, 345 ); + testpack( 'Can we pass a\PDO object to Setup?' ); + $pdo = new \PDO( 'sqlite:test.db' ); + R::addDatabase( 'pdo', $pdo ); + R::selectDatabase( 'pdo' ); + R::getCell('SELECT 123;'); + testpack( 'Test array interface of beans' ); + $bean = R::dispense( 'bean' ); + $bean->hello = 'hi'; + $bean->world = 'planet'; + asrt( $bean['hello'], 'hi' ); + asrt( isset( $bean['hello'] ), TRUE ); + asrt( isset( $bean['bye'] ), FALSE ); + $bean['world'] = 'sphere'; + asrt( $bean->world, 'sphere' ); + foreach ( $bean as $key => $el ) { + if ( $el == 'sphere' || $el == 'hi' || $el == 0 ) { + pass(); + } else { + fail(); + } + if ( $key == 'hello' || $key == 'world' || $key == 'id' ) { + pass(); + } else { + fail(); + } + } + asrt( count( $bean ), 3 ); + unset( $bean['hello'] ); + asrt( count( $bean ), 2 ); + asrt( count( R::dispense( 'countable' ) ), 1 ); + // Otherwise untestable... + $bean->setBeanHelper( new SimpleFacadeBeanHelper() ); + R::getRedBean()->setBeanHelper( new SimpleFacadeBeanHelper() ); + pass(); + // Test whether properties like owner and shareditem are still possible + testpack( 'Test Bean Interface for Lists' ); + $bean = R::dispense( 'bean' ); + // Must not be list, because first char after own is lowercase + asrt( is_array( $bean->owner ), FALSE ); + // Must not be list, because first char after shared is lowercase + asrt( is_array( $bean->shareditem ), FALSE ); + asrt( is_array( $bean->own ), FALSE ); + asrt( is_array( $bean->shared ), FALSE ); + asrt( is_array( $bean->own_item ), FALSE ); + asrt( is_array( $bean->shared_item ), FALSE ); + asrt( is_array( $bean->{'own item'} ), FALSE ); + asrt( is_array( $bean->{'shared Item'} ), FALSE ); + } + + public function testConv2Beans() + { + $row1 = array('id' => 1, 'title'=>'test'); + $row2 = array('id' => 2, 'title'=>'test2'); + $beans = R::convertToBeans('page', array($row1, $row2)); + asrt(count($beans), 2); + asrt($beans[2]->title, 'test2'); + } + + /** + * Test the most important invalid bean combinations. + * + * @return void + */ + public function testInvalidType() + { + $invalid = array( + 'book_page', //no link beans + 'a_b_c', //no prefix + 'a b', //no space + 'bean@', //no invalid symbols + 'bean#', //no invalid symbols + 'bean$', //sometimes used in DB, not allowed + '__bean',//no prefixes + '.bean', //no object notation + 'bean-item', //no dash + 'beanOther'); //no camelcase (uppercase because of file system issues) + + foreach( $invalid as $j ) { + try { + R::dispense( $j ); + fail(); + } catch( RedException $e ) { + pass(); + } + } + } + + /** + * Test whether batch still works if no IDs have been passed. + * + * @return void + */ + public function testBatch0() + { + $zero = R::batch( 'page', array() ); + asrt( is_array( $zero ), TRUE ); + asrt( count( $zero ), 0 ); + $zero = R::batch( 'page', FALSE ); + asrt( is_array( $zero ), TRUE ); + asrt( count( $zero ), 0 ); + $zero = R::batch( 'page', NULL); + asrt( is_array( $zero ), TRUE ); + asrt( count( $zero ), 0 ); + } + + /** + * Test whether connection failure does not reveal + * credentials. + * + * @return void + */ + public function testConnect() + { + $driver = new RPDO( 'dsn:invalid', 'usr', 'psst' ); + try { + $driver->connect(); + fail(); + } + catch( \PDOException $e ) { + asrt( strpos( $e->getMessage(), 'invalid' ), FALSE ); + asrt( strpos( $e->getMessage(), 'usr' ), FALSE ); + asrt( strpos( $e->getMessage(), 'psst' ), FALSE ); + } + } + + /** + * Test whether we can create an instant database using + * R::setup(). + * + * Probably only works on *NIX systems. + * + * @return void + */ + public function testSetup() + { + $tmpDir = sys_get_temp_dir(); + R::setup(); + } + + /** + * Test camelCase to snake_case conversions. + * + * @return void + */ + public function testCamel2Snake() + { + asrt( AQueryWriter::camelsSnake('bookPage'), 'book_page' ); + asrt( AQueryWriter::camelsSnake('FTP'), 'ftp' ); + asrt( AQueryWriter::camelsSnake('ACLRules'), 'acl_rules' ); + asrt( AQueryWriter::camelsSnake('SSHConnectionProxy'), 'ssh_connection_proxy' ); + asrt( AQueryWriter::camelsSnake('proxyServerFacade'), 'proxy_server_facade' ); + asrt( AQueryWriter::camelsSnake('proxySSHClient'), 'proxy_ssh_client' ); + asrt( AQueryWriter::camelsSnake('objectACL2Factory'), 'object_acl2_factory' ); + asrt( AQueryWriter::camelsSnake('bookItems4Page'), 'book_items4_page' ); + asrt( AQueryWriter::camelsSnake('book☀Items4Page'), 'book☀_items4_page' ); + } + + /** + * Test that init SQL is being executed upon setting PDO. + * + * @return void + */ + public function testRunInitCodeOnSetPDO() + { + $pdo = R::getToolBox()->getDatabaseAdapter()->getDatabase()->getPDO(); + $rpdo = new \RedBeanPHP\Driver\RPDO( $pdo ); + $rpdo->setEnableLogging(true); + $logger = new \RedBeanPHP\Logger\RDefault\Debug; + $logger->setMode( \RedBeanPHP\Logger\RDefault::C_LOGGER_ARRAY ); + $rpdo->setLogger( $logger ); + $rpdo->setInitQuery('SELECT 123'); + $rpdo->setPDO( $pdo ); + $found = $logger->grep('SELECT 123'); + asrt(count($found), 1); + asrt($found[0], 'SELECT 123'); + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Plugins.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Plugins.php new file mode 100644 index 0000000..75dfde0 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Plugins.php @@ -0,0 +1,71 @@ +getMessage(), 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + try { + R::__callStatic( '---', function() {} ); + fail(); + } catch ( RedException $e ) { + asrt( $e->getMessage(), 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + try { + R::invalidMethod(); + fail(); + } catch ( RedException $e ) { + asrt( $e->getMessage(), 'Plugin \'invalidMethod\' does not exist, add this plugin using: R::ext(\'invalidMethod\')' ); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Stub.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Stub.php new file mode 100644 index 0000000..e385d8e --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Stub.php @@ -0,0 +1,151 @@ +answerGetCol = array(); + $writer->callMethod( 'buildFK', $type, $targetType, $property, $targetProperty, $isDep = FALSE ); + pass(); + $mockdapter->errorExec = new \RedBeanPHP\RedException\SQL('Test Exception'); + $writer->callMethod( 'buildFK', $type, $targetType, $property, $targetProperty, $isDep = FALSE ); + pass(); + $mockdapter->errorExec = NULL; + $mockdapter->answerGetSQL = array( + array( + 'CREATE TABLE' => 'CONSTRAINT [key] FOREIGN KEY ([bean]) REFERENCES [bean] ON DELETE CASCADE ON UPDATE RESTRICT' + ) + ); + $writer->addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ); + pass(); + $writer->callMethod( 'getKeyMapForType', 'bean' ); + pass(); + $writer->getTypeForID(); + pass(); + $writer->getTables(); + pass(); + $writer->createTable( $table ); + pass(); + $mockdapter->answerGetSQL = array(array('Field'=>'title','Type'=>'STRING')); + $writer->getColumns( $table ); + pass(); + asrt( $writer->scanType( 123, $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 12.3, $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_DOUBLE ); + asrt( $writer->scanType( '0001', $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_STRING ); + asrt( $writer->scanType( '1001', $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( NULL, $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( '2019-01-01', $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_STRING ); + asrt( $writer->scanType( '2019-01-01 10:00:00', $flagSpecial = FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_STRING ); + asrt( $writer->scanType( '2019-01-01', $flagSpecial = TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->scanType( '2019-01-01 10:00:00', $flagSpecial = TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIAL_DATETIME ); + pass(); + $writer->code( $typedescription, $includeSpecials = FALSE ); + $writer->code( $typedescription, $includeSpecials = TRUE ); + asrt( $writer->code( 'INTEGER', FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_INTEGER ); + asrt( $writer->code( 'DOUBLE', FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_DOUBLE ); + asrt( $writer->code( 'STRING', FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_STRING ); + asrt( $writer->code( 'DATE', FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIFIED ); + asrt( $writer->code( 'DATETIME', FALSE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIFIED ); + asrt( $writer->code( 'INTEGER', TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_INTEGER ); + asrt( $writer->code( 'DOUBLE', TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_DOUBLE ); + asrt( $writer->code( 'STRING', TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_STRING ); + asrt( $writer->code( 'DATE', TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->code( 'DATETIME', TRUE ), \RedBeanPHP\QueryWriter\CUBRID::C_DATATYPE_SPECIAL_DATETIME ); + pass(); + $writer->addColumn( $type, $column, $field ); + pass(); + $writer->addUniqueConstraint( $type, $properties ); + $mockdapter->errorExec = new \RedBeanPHP\RedException\SQL('Test Exception'); + $writer->addUniqueConstraint( $type, $properties ); + pass(); + asrt( $writer->sqlStateIn( 'HY000', array() ), FALSE ); + asrt( $writer->sqlStateIn( 'HY000', array(\RedBeanPHP\QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION) ), TRUE ); + pass(); + $writer->addIndex( $type, $name, $column ); + pass(); + $mockdapter->errorExec = NULL; + $writer->addIndex( $type, $name, $column ); + pass(); + $writer->wipeAll(); + pass(); + $mockdapter->answerGetCol = array( 'table1' ); + $mockdapter->answerGetSQL = array( + array( + 'CREATE TABLE' => 'CONSTRAINT [key] FOREIGN KEY ([bean]) REFERENCES [bean] ON DELETE CASCADE ON UPDATE RESTRICT' + ) + ); + $writer->wipeAll(); + pass(); + $writer->esc( $dbStructure, $noQuotes = FALSE ); + pass(); + } + + /** + * Test base implementation of getKeyMapForType(). + * + * @return void + */ + public function testKeyMap() + { + $proxyWriter = new \ProxyWriter; + $empty = $proxyWriter->callMethod( $proxyWriter, 'getKeyMapForType', 'bean' ); + asrt( is_array( $empty ), TRUE ); + asrt( count( $empty ), 0 ); + } + + /** + * Test whether autoresolve() function for BC exists. + * + * @return void + */ + public function testSetAutoResolve() + { + R::setAutoResolve( TRUE ); + pass(); + } +} + + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Tainted.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Tainted.php new file mode 100644 index 0000000..3069492 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Tainted.php @@ -0,0 +1,131 @@ +hasListChanged( 'ownPage' ), FALSE ); + $book->ownPage[] = $page; + asrt( $book->hasListChanged( 'ownPage' ), TRUE ); + R::store( $book ); + $book = $book->fresh(); + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + $page = R::dispense( 'page' ); + $book->ownPageList[] = $page; + asrt( $book->hasListChanged( 'ownPage' ), TRUE ); + R::store( $book ); + $book = $book->fresh(); + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + asrt( count( $book->ownPageList ), 2 ); + array_pop( $book->ownPageList ); + asrt( count( $book->ownPageList ), 1 ); + asrt( $book->hasListChanged( 'ownPage' ), TRUE ); + array_pop( $book->ownPageList ); + asrt( count( $book->ownPageList ), 0 ); + asrt( $book->hasListChanged( 'ownPage' ), TRUE ); + $book = $book->fresh(); + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + asrt( count( $book->ownPageList ), 2 ); + $otherPage = R::dispense( 'page' ); + array_pop( $book->ownPageList ); + $book->ownPageList[] = $otherPage; + asrt( count( $book->ownPageList ), 2 ); + asrt( $book->hasListChanged( 'ownPage' ), TRUE ); + $book = $book->fresh(); + $firstPage = reset( $book->ownPageList ); + $firstPage->content = 'abc'; + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + $book = $book->fresh(); + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + $lastPage = end( $book->ownPageList ); + $lastPage->ownText[] = R::dispense( 'text' ); + asrt( $book->hasListChanged( 'ownPage' ), FALSE ); + } + + /** + * Tests whether we can clear the history of a bean. + * + * @return void + */ + public function testClearHist() + { + R::nuke(); + $book = R::dispense( 'book' ); + asrt( $book->hasChanged( 'title' ), FALSE ); + $book->title = 'book'; + asrt( $book->hasChanged( 'title' ), TRUE ); + R::store( $book ); + asrt( $book->hasChanged( 'title' ), TRUE ); + $book->clearHistory(); + asrt( $book->hasChanged( 'title' ), FALSE ); + } + + /** + * Test tainted. + * + * @return void + */ + public function testTainted() + { + testpack( 'Original Tainted Tests' ); + $redbean = R::getRedBean(); + $spoon = $redbean->dispense( "spoon" ); + asrt( $spoon->getMeta( "tainted" ), TRUE ); + $spoon->dirty = "yes"; + asrt( $spoon->getMeta( "tainted" ), TRUE ); + testpack( 'Tainted List test' ); + $note = R::dispense( 'note' ); + $note->text = 'abc'; + $note->ownNote[] = R::dispense( 'note' )->setAttr( 'text', 'def' ); + $id = R::store( $note ); + $note = R::load( 'note', $id ); + asrt( $note->isTainted(), FALSE ); + // Shouldn't affect tainted + $note->text; + asrt( $note->isTainted(), FALSE ); + $note->ownNote; + asrt( $note->isTainted(), TRUE ); + testpack( 'Tainted Test Old Value' ); + $text = $note->old( 'text' ); + asrt( $text, 'abc' ); + asrt( $note->hasChanged( 'text' ), FALSE ); + $note->text = 'xxx'; + asrt( $note->hasChanged( 'text' ), TRUE ); + $text = $note->old( 'text' ); + asrt( $text, 'abc' ); + testpack( 'Tainted Non-exist' ); + asrt( $note->hasChanged( 'text2' ), FALSE ); + testpack( 'Misc Tainted Tests' ); + $bean = R::dispense( 'bean' ); + $bean->hasChanged( 'prop' ); + $bean->old( 'prop' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Toolbox.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Toolbox.php new file mode 100644 index 0000000..552c3b1 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Toolbox.php @@ -0,0 +1,164 @@ +getToolbox(); + asrt( ( $toolbox2 instanceof TB), TRUE ); + asrt( $toolbox, $toolbox2 ); + $extractedToolbox = $beanHelper->getExtractedToolbox(); + asrt( is_array( $extractedToolbox ), TRUE ); + asrt( count( $extractedToolbox ), 4 ); + asrt( ( $extractedToolbox[0] instanceof OODB ), TRUE ); + asrt( ( $extractedToolbox[1] instanceof Adapter ), TRUE ); + asrt( ( $extractedToolbox[2] instanceof QueryWriter ), TRUE ); + asrt( ( $extractedToolbox[3] instanceof TB ), TRUE ); + } + + /** + * Does the toolbox contain the necessary tools ? + * + * @return void + */ + public function testDoesToolboxContainTheTools() + { + $toolbox = R::getToolBox(); + asrt( ( $toolbox->getDatabaseAdapter() instanceof Adapter ), TRUE ); + asrt( ( $toolbox->getRedBean() instanceof OODB ), TRUE ); + asrt( ( $toolbox->getWriter() instanceof QueryWriter ), TRUE ); + } + + /** + * Tests whether freeze() switches the repository object + * as it is supposed to do. + * + * @return void + */ + public function testRepoSwitching() + { + asrt( class_exists( 'RedBeanPHP\Repository' ), TRUE ); + asrt( class_exists( 'RedBeanPHP\Repository\Fluid' ), TRUE ); + asrt( class_exists( 'RedBeanPHP\Repository\Frozen' ), TRUE ); + R::freeze( FALSE ); + $redbean = R::getRedBean(); + $repo = $redbean->getCurrentRepository(); + asrt( is_object( $repo ), TRUE ); + asrt( ( $repo instanceof Repository ), TRUE ); + asrt( ( $repo instanceof FluidRepo ), TRUE ); + R::freeze( TRUE ); + $fluid = $repo; + $repo = $redbean->getCurrentRepository(); + asrt( is_object( $repo ), TRUE ); + asrt( ( $repo instanceof Repository ), TRUE ); + asrt( ( $repo instanceof FrozenRepo ), TRUE ); + $frozen = $repo; + R::freeze( FALSE ); + $redbean = R::getRedBean(); + $repo = $redbean->getCurrentRepository(); + asrt( is_object( $repo ), TRUE ); + asrt( ( $repo instanceof Repository ), TRUE ); + asrt( ( $repo instanceof FluidRepo ), TRUE ); + asrt( $repo, $fluid ); + R::freeze( TRUE ); + $fluid = $repo; + $repo = $redbean->getCurrentRepository(); + asrt( is_object( $repo ), TRUE ); + asrt( ( $repo instanceof Repository ), TRUE ); + asrt( ( $repo instanceof FrozenRepo ), TRUE ); + asrt( $repo, $frozen ); + R::freeze( FALSE ); + } + + /** + * Can we add and remove toolboxes using + * neat accessors? + * + * @return void + */ + public function testAddRemoveToolBox() + { + $t1 = R::getToolBox(); + R::addToolBoxWithKey( 't1', $t1 ); + asrt( ( R::getToolBoxByKey('t2') instanceof Toolbox), FALSE ); + asrt( ( R::getToolBoxByKey('t1') instanceof Toolbox), FALSE ); + asrt( R::removeToolBoxByKey('t1'), TRUE ); + asrt( R::removeToolBoxByKey('t2'), FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Version.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Version.php new file mode 100644 index 0000000..dfe703e --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Blackhole/Version.php @@ -0,0 +1,121 @@ +getFeatureFlags(), '1,0,0,0,0' ); + R::useFeatureSet('5.3'); + asrt( $this->getFeatureFlags(), '1,0,0,0,0' ); + R::useFeatureSet('novice/5.3'); + asrt( $this->getFeatureFlags(), '1,1,0,0,0' ); + R::useFeatureSet('5.4'); + asrt( $this->getFeatureFlags(), '0,0,0,1,1' ); + R::useFeatureSet('latest'); + asrt( $this->getFeatureFlags(), '0,0,0,1,1' ); + R::useFeatureSet('novice/5.4'); + asrt( $this->getFeatureFlags(), '0,1,0,0,1' ); + R::useFeatureSet('5.5'); + asrt( $this->getFeatureFlags(), '0,0,0,1,1' ); + R::useFeatureSet('novice/5.5'); + asrt( $this->getFeatureFlags(), '0,1,0,0,1' ); + R::useFeatureSet('novice/latest'); + asrt( $this->getFeatureFlags(), '0,1,0,0,1' ); + R::useFeatureSet('original'); + asrt( $this->getFeatureFlags(), '1,0,0,0,0' ); + } + + /** + * Test whether an invalid feature set label will + * cause an exception. + * + * @return void + */ + public function testInvalidFeatureLabel() + { + try { + R::useFeatureSet('Invalid'); + fail(); + } catch( \Exception $e ) { + pass(); + } + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID.php b/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID.php new file mode 100644 index 0000000..11a9501 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID.php @@ -0,0 +1,33 @@ +setTimeZone( new \DateTimeZone( 'Europe/Amsterdam' ) ); + $dt->setDate( 1981, 5, 1 ); + $dt->setTime( 3, 13, 13 ); + asrt( setget( $dt ), '1981-05-01 03:13:13.000' ); + $bean = R::dispense( 'bean' ); + $bean->dt = $dt; + } + + /** + * Test numbers. + * + * @return void + */ + public function testNumbers() + { + asrt( setget( "-1" ), "-1" ); + asrt( setget( -1 ), "-1" ); + asrt( setget( "1.0" ), "1" ); + asrt( setget( 1.0 ), "1" ); + asrt( setget( "-0.25" ), "-0.2500000000000000" ); + asrt( setget( -0.25 ), "-0.2500000000000000" ); + asrt( setget( "0.12345678" ), "0.1234567800000000" ); + asrt( setget( 0.12345678 ), "0.1234567800000000" ); + asrt( setget( "-0.12345678" ), "-0.1234567800000000" ); + asrt( setget( -0.12345678 ), "-0.1234567800000000" ); + asrt( setget( "2147483647" ), "2147483647" ); + asrt( setget( 2147483647 ), "2147483647" ); + asrt( setget( -2147483647 ), "-2147483647" ); + asrt( setget( "-2147483647" ), "-2147483647" ); + asrt( setget( "2147483648" ), "2147483648.0000000000000000" ); + asrt( setget( "-2147483648" ), "-2147483648.0000000000000000" ); + asrt( setget( "199936710040730" ), "199936710040730.0000000000000000" ); + asrt( setget( "-199936710040730" ), "-199936710040730.0000000000000000" ); + } + + /** + * Test dates. + * + * @return void + */ + public function testDates() + { + asrt( setget( "2010-10-11" ), "2010-10-11" ); + asrt( setget( "2010-10-11 12:10" ), "2010-10-11 12:10" ); + asrt( setget( "2010-10-11 12:10:11" ), "2010-10-11 12:10:11.000" ); + asrt( setget( "x2010-10-11 12:10:11" ), "x2010-10-11 12:10:11" ); + } + + /** + * Test strings. + * + * @return void + */ + public function testStrings() + { + asrt( setget( "a" ), "a" ); + asrt( setget( "." ), "." ); + asrt( setget( "\"" ), "\"" ); + asrt( setget( "just some text" ), "just some text" ); + } + + /** + * Test booleans. + * + * @return void + */ + public function testBool() + { + asrt( setget( TRUE ), "1" ); + asrt( setget( FALSE ), "0" ); + asrt( setget( "TRUE" ), "TRUE" ); + asrt( setget( "FALSE" ), "FALSE" ); + } + + /** + * Test NULL. + * + * @return void + */ + public function testNull() + { + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "0123" ), "0123" ); + asrt( setget( "0000123" ), "0000123" ); + asrt( setget( NULL ), NULL ); + asrt( ( setget( 0 ) == 0 ), TRUE ); + asrt( ( setget( 1 ) == 1 ), TRUE ); + asrt( ( setget( TRUE ) == TRUE ), TRUE ); + asrt( ( setget( FALSE ) == FALSE ), TRUE ); + // minor test sqltest + $a = R::getWriter()->sqlStateIn( '000', array() ); + // Unknown state must return FALSE. + asrt( $a, FALSE ); + try { + R::getWriter()->esc( '`aaa`' ); + fail(); + } catch (\Exception $e ) { + pass(); + } + asrt( ( $e instanceof RedException ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID/Writer.php b/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID/Writer.php new file mode 100644 index 0000000..5b10e62 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/CUBRID/Writer.php @@ -0,0 +1,47 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $writer->createTable( "testtable" ); + $writer->addColumn( "testtable", "special", CUBRID::C_DATATYPE_SPECIAL_DATE ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special'], TRUE ), CUBRID::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->code( $cols['special'], FALSE ), CUBRID::C_DATATYPE_SPECIFIED ); + $writer->addColumn( "testtable", "special2", CUBRID::C_DATATYPE_SPECIAL_DATETIME ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special2'], TRUE ), CUBRID::C_DATATYPE_SPECIAL_DATETIME ); + asrt( $writer->code( $cols['special'], FALSE ), CUBRID::C_DATATYPE_SPECIFIED ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql.php new file mode 100644 index 0000000..e8ca95b --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql.php @@ -0,0 +1,35 @@ +id, $book1ID ); + asrt( $book1->title, 'book 1' ); + $book2 = R::load( 'book', $book2ID ); + asrt( $book2->id, $book2ID ); + asrt( $book2->title, 'book 2' ); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->fresh()->with( 'LIMIT 1' )->ownPage ), 1 ); + asrt( count( $book1->fresh()->withCondition( ' title = ? ', array('page 2 of book 1'))->ownPage ), 1 ); + asrt( count($book2->ownPage), 1 ); + asrt( $book2->fresh()->countOwn( 'page' ), 1 ); + $page1 = R::load( 'page', $page1ID ); + asrt( count( $page1->sharedPage ), 0 ); + asrt( $page1->fetchAs( 'book' )->magazine->id, $book2ID ); + $page2 = R::load( 'page', $page2ID ); + asrt( count($page2->sharedPage), 1 ); + asrt( $page2->fresh()->countShared( 'page' ), 1 ); + $page3 = R::findOne( 'page', ' title = ? ', array( 'page 1 of book 2' ) ); + asrt( $page3->id, $page3ID ); + asrt( $page3->book->id, $book2ID ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Double.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Double.php new file mode 100644 index 0000000..4242287 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Double.php @@ -0,0 +1,50 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $largeDouble = 999999888889999922211111; //8.88889999922211e+17; + $page = $redbean->dispense( "page" ); + $page->weight = $largeDouble; + $id = $redbean->store( $page ); + $cols = $writer->getColumns( 'page' ); + asrt( $cols['weight'], 'double' ); + $page = $redbean->load( 'page', $id ); + $page->name = 'dont change the numbers!'; + $redbean->store( $page ); + $page = $redbean->load( 'page', $id ); + $cols = $writer->getColumns( 'page' ); + asrt( $cols['weight'], 'double' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Foreignkeys.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Foreignkeys.php new file mode 100644 index 0000000..b75d3a0 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Foreignkeys.php @@ -0,0 +1,342 @@ +xownMetrics[] = $metrics; + R::store( $constraint ); + asrt( 1, R::count( 'metrics' ) ); + R::trash($constraint); + asrt( 0, R::count( 'metrics') ); + } + + /** + * Basic FK tests. + * + * @return void + */ + public function testFKS() + { + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $cover = R::dispense( 'cover' ); + list( $g1, $g2 ) = R::dispense( 'genre', 2 ); + $g1->name = '1'; + $g2->name = '2'; + $book->ownPage = array( $page ); + $book->cover = $cover; + $book->sharedGenre = array( $g1, $g2 ); + R::store( $book ); + $fkbook = R::getAll( 'describe book' ); + $fkgenre = R::getAll( 'describe book_genre' ); + $fkpage = R::getAll( 'describe cover' ); + $j = json_encode( R::getAll( 'SELECT + ke.referenced_table_name parent, + ke.table_name child, + ke.constraint_name + FROM + information_schema.KEY_COLUMN_USAGE ke + WHERE + ke.referenced_table_name IS NOT NULL + AND ke.CONSTRAINT_SCHEMA="oodb" + ORDER BY + constraint_name;' ) ); + $json = '[ + { + "parent": "genre", + "child": "book_genre", + "constraint_name": "c_fk_book_genre_genre_id" + }, + { + "parent": "book", + "child": "book_genre", + "constraint_name": "c_fk_book_genre_book_id" + }, + { + "parent": "cover", + "child": "book", + "constraint_name": "c_fk_book_cover_id" + }, + { + "parent": "book", + "child": "page", + "constraint_name": "c_fk_page_book_id" + } + ]'; + $j1 = json_decode( $j, TRUE ); + $j2 = json_decode( $json, TRUE ); + foreach ( $j1 as $jrow ) { + $s = json_encode( $jrow ); + $found = 0; + foreach ( $j2 as $k => $j2row ) { + if ( json_encode( $j2row ) === $s ) { + pass(); + unset( $j2[$k] ); + $found = 1; + break; + } + } + if ( !$found ) fail(); + } + } + + /** + * Test widen for constraint. + * + * @return void + */ + public function testWideningColumnForConstraint() + { + testpack( 'widening column for constraint' ); + $bean1 = R::dispense( 'project' ); + $bean2 = R::dispense( 'invoice' ); + $bean3 = R::getRedBean()->dispense( 'invoice_project' ); + $bean3->project_id = FALSE; + $bean3->invoice_id = TRUE; + R::store( $bean3 ); + $cols = R::getColumns( 'invoice_project' ); + asrt( $cols['project_id'], "int(11) unsigned" ); + asrt( $cols['invoice_id'], "int(11) unsigned" ); + } + + /** + * Test adding of constraints directly by invoking + * the writer method. + * + * @return void + */ + public function testContrain() + { + R::nuke(); + $sql = ' + CREATE TABLE book ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $sql = ' + CREATE TABLE page ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $sql = ' + CREATE TABLE book_page ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + book_id INT( 11 ) UNSIGNED NOT NULL, + page_id INT( 11 ) UNSIGNED NOT NULL, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "book_page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 0 ); + $writer = R::getWriter(); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "book_page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 2 ); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "book_page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 2 ); + } + + /** + * Test adding foreign keys. + * + * @return void + */ + public function testAddingForeignKey() + { + R::nuke(); + $sql = ' + CREATE TABLE book ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $sql = ' + CREATE TABLE page ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + book_id INT( 11 ) UNSIGNED NOT NULL, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 0 ); + $writer = R::getWriter(); + //Can we add a foreign key with cascade? + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 1 ); + //dont add it twice + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 1 ); + //even if different + $writer->addFK('page', 'book', 'book_id', 'id', FALSE); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" '); + asrt( (int) $numOfFKS, 1 ); + //Now add non-dep key + R::nuke(); + $sql = ' + CREATE TABLE book ( + id INT( 11 ) UNSIGNED AUTO_INCREMENT, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $sql = ' + CREATE TABLE page ( + id INT( 11 ) UNSIGNED AUTO_INCREMENT, + book_id INT( 11 ) UNSIGNED NULL, + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 0 ); + //even if different + $writer->addFK('page', 'book', 'book_id', 'id', FALSE); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "CASCADE"'); + asrt( (int) $numOfFKS, 0 ); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" AND DELETE_RULE = "SET NULL"'); + asrt( (int) $numOfFKS, 1 ); + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + $numOfFKS = R::getCell(' + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + WHERE TABLE_NAME = "page" '); + } + + /** + * Test whether we can manually create indexes. + * + * @return void + */ + public function testAddingIndex() + { + R::nuke(); + $sql = ' + CREATE TABLE song ( + id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, + album_id INT( 11 ) UNSIGNED NOT NULL, + category VARCHAR( 255 ), + PRIMARY KEY ( id ) + ) + ENGINE = InnoDB + '; + R::exec( $sql ); + $sql = 'SHOW INDEX FROM song'; + $indexes = R::getAll( $sql ); + asrt( count( $indexes ), 1 ); + asrt( $indexes[0]['Table'], 'song' ); + asrt( $indexes[0]['Key_name'], 'PRIMARY' ); + $writer = R::getWriter(); + $writer->addIndex('song', 'index1', 'album_id'); + $indexes = R::getAll( 'SHOW INDEX FROM song' ); + asrt( count( $indexes ), 2 ); + asrt( $indexes[0]['Table'], 'song' ); + asrt( $indexes[0]['Key_name'], 'PRIMARY' ); + asrt( $indexes[1]['Table'], 'song' ); + asrt( $indexes[1]['Key_name'], 'index1' ); + //Cant add the same index twice + $writer->addIndex('song', 'index2', 'category'); + $indexes = R::getAll( 'SHOW INDEX FROM song' ); + asrt( count( $indexes ), 3 ); + //Dont fail, just dont + try { + $writer->addIndex('song', 'index3', 'nonexistant'); + pass(); + } catch( \Exception $e ) { + fail(); + } + asrt( count( $indexes ), 3 ); + try { + $writer->addIndex('nonexistant', 'index4', 'nonexistant'); + pass(); + } catch( \Exception $e ) { + fail(); + } + asrt( count( $indexes ), 3 ); + try { + $writer->addIndex('nonexistant', '', 'nonexistant'); + pass(); + } catch( \Exception $e ) { + fail(); + } + asrt( count( $indexes ), 3 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Freeze.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Freeze.php new file mode 100644 index 0000000..f5a7605 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Freeze.php @@ -0,0 +1,105 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + $post = $redbean->dispense( 'post' ); + $post->title = 'title'; + $redbean->store( $post ); + $page = $redbean->dispense( 'page' ); + $page->name = 'title'; + $redbean->store( $page ); + $page = $redbean->dispense( "page" ); + $page->name = "John's page"; + $idpage = $redbean->store( $page ); + $page2 = $redbean->dispense( "page" ); + $page2->name = "John's second page"; + $idpage2 = $redbean->store( $page2 ); + $a->associate( $page, $page2 ); + $redbean->freeze( TRUE ); + $page = $redbean->dispense( "page" ); + $page->sections = 10; + $page->name = "half a page"; + try { + $id = $redbean->store( $page ); + fail(); + } catch ( SQL $e ) { + pass(); + } + $post = $redbean->dispense( "post" ); + $post->title = "existing table"; + try { + $id = $redbean->store( $post ); + pass(); + } catch ( SQL $e ) { + fail(); + } + asrt( in_array( "name", array_keys( $writer->getColumns( "page" ) ) ), TRUE ); + asrt( in_array( "sections", array_keys( $writer->getColumns( "page" ) ) ), FALSE ); + $newtype = $redbean->dispense( "newtype" ); + $newtype->property = 1; + try { + $id = $redbean->store( $newtype ); + fail(); + } catch ( SQL $e ) { + pass(); + } + $logger = R::debug( TRUE, 1 ); + // Now log and make sure no 'describe SQL' happens + $page = $redbean->dispense( "page" ); + $page->name = "just another page that has been frozen..."; + $id = $redbean->store( $page ); + $page = $redbean->load( "page", $id ); + $page->name = "just a frozen page..."; + $redbean->store( $page ); + $page2 = $redbean->dispense( "page" ); + $page2->name = "an associated frozen page"; + $a->associate( $page, $page2 ); + $a->related( $page, "page" ); + $a->unassociate( $page, $page2 ); + $a->clearRelations( $page, "page" ); + $items = $redbean->find( "page", array(), array( "1" ) ); + $redbean->trash( $page ); + $redbean->freeze( FALSE ); + asrt( count( $logger->grep( "SELECT" ) ) > 0, TRUE ); + asrt( count( $logger->grep( "describe" ) ) < 1, TRUE ); + asrt( is_array( $logger->getLogs() ), TRUE ); + R::debug( FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Issue411.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Issue411.php new file mode 100644 index 0000000..ebfd3cc --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Issue411.php @@ -0,0 +1,63 @@ +text = 'abcd'; + R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['text'] ), TRUE ); + asrt( $columns['text'], 'varchar(191)' ); + $book = $book->fresh(); + $book->text = str_repeat( 'x', 190 ); + R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['text'] ), TRUE ); + asrt( $columns['text'], 'varchar(191)' ); + $book = $book->fresh(); + $book->text = str_repeat( 'x', 191 ); + R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['text'] ), TRUE ); + asrt( $columns['text'], 'varchar(191)' ); + $book = $book->fresh(); + $book->text = str_repeat( 'x', 192 ); + R::store( $book ); + $columns = R::inspect( 'book' ); + asrt( isset( $columns['text'] ), TRUE ); + asrt( $columns['text'], 'varchar(255)' ); + } +} + diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Parambind.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Parambind.php new file mode 100644 index 0000000..bed92c7 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Parambind.php @@ -0,0 +1,118 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + R::getDatabaseAdapter()->getDatabase()->setUseStringOnlyBinding( TRUE ); + try { + R::getAll( "select * from job limit ? ", array( 1 ) ); + fail(); + } catch (\Exception $e ) { + pass(); + } + try { + R::getAll( "select * from job limit :l ", array( ":l" => 1 ) ); + fail(); + } catch (\Exception $e ) { + pass(); + } + try { + R::exec( "select * from job limit ? ", array( 1 ) ); + fail(); + } catch (\Exception $e ) { + pass(); + } + try { + R::exec( "select * from job limit :l ", array( ":l" => 1 ) ); + fail(); + } catch (\Exception $e ) { + pass(); + } + R::getDatabaseAdapter()->getDatabase()->setUseStringOnlyBinding( FALSE ); + try { + R::getAll( "select * from job limit ? ", array( 1 ) ); + pass(); + } catch (\Exception $e ) { + fail(); + } + try { + R::getAll( "select * from job limit :l ", array( ":l" => 1 ) ); + pass(); + } catch (\Exception $e ) { + fail(); + } + try { + R::exec( "select * from job limit ? ", array( 1 ) ); + pass(); + } catch (\Exception $e ) { + fail(); + } + try { + R::exec( "select * from job limit :l ", array( ":l" => 1 ) ); + pass(); + } catch (\Exception $e ) { + fail(); + } + testpack( "Test findOrDispense" ); + $person = R::findOrDispense( "person", " job = ? ", array( "developer" ) ); + asrt( ( count( $person ) > 0 ), TRUE ); + $person = R::findOrDispense( "person", " job = ? ", array( "musician" ) ); + asrt( ( count( $person ) > 0 ), TRUE ); + $musician = array_pop( $person ); + asrt( intval( $musician->id ), 0 ); + try { + $adapter->exec( "an invalid query" ); + fail(); + } catch ( SQL $e ) { + pass(); + } + asrt( (int) $adapter->getCell( "SELECT 123" ), 123 ); + asrt( (int) $adapter->getCell( "SELECT ?", array( "987" ) ), 987 ); + asrt( (int) $adapter->getCell( "SELECT ?+?", array( "987", "2" ) ), 989 ); + asrt( (int) $adapter->getCell( "SELECT :numberOne+:numberTwo", array( + ":numberOne" => 42, ":numberTwo" => 50 ) ), 92 ); + $pair = $adapter->getAssoc( "SELECT 'thekey','thevalue' " ); + asrt( is_array( $pair ), TRUE ); + asrt( count( $pair ), 1 ); + asrt( isset( $pair["thekey"] ), TRUE ); + asrt( $pair["thekey"], "thevalue" ); + testpack( 'Test whether we can properly bind and receive NULL values' ); + asrt( $adapter->getCell( 'SELECT :nil ', array( ':nil' => 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT :nil ', array( ':nil' => NULL ) ), NULL ); + asrt( $adapter->getCell( 'SELECT ? ', array( 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT ? ', array( NULL ) ), NULL ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Preexist.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Preexist.php new file mode 100644 index 0000000..dcf4c2a --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Preexist.php @@ -0,0 +1,58 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + $page = $redbean->dispense( "page" ); + $page->name = "John's page"; + $idpage = $redbean->store( $page ); + $page2 = $redbean->dispense( "page" ); + $page2->name = "John's second page"; + $idpage2 = $redbean->store( $page2 ); + $a->associate( $page, $page2 ); + $adapter->exec( "ALTER TABLE " . $writer->esc( 'page' ) . " + CHANGE " . $writer->esc( 'name' ) . " " . $writer->esc( 'name' ) . " + VARCHAR( 254 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL " ); + $page = $redbean->dispense( "page" ); + $page->name = "Just Another Page In a Table"; + $cols = $writer->getColumns( "page" ); + asrt( $cols["name"], "varchar(254)" ); + $redbean->store( $page ); + pass(); // No crash? + $cols = $writer->getColumns( "page" ); + asrt( $cols["name"], "varchar(254)" ); //must still be same + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Setget.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Setget.php new file mode 100644 index 0000000..860299c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Setget.php @@ -0,0 +1,160 @@ +setTimeZone( new \DateTimeZone( 'Europe/Amsterdam' ) ); + $dt->setDate( 1981, 5, 1 ); + $dt->setTime( 3, 13, 13 ); + asrt( setget( $dt ), '1981-05-01 03:13:13' ); + $bean = R::dispense( 'bean' ); + $bean->dt = $dt; + } + + /** + * Tests R::getInsertID convenience method. + * + * @return void + */ + public function testGetInsertID() + { + R::nuke(); + $id = R::store( R::dispense( 'book' ) ); + $id2 = R::getInsertID(); + asrt( $id, $id2 ); + } + + /** + * Test numbers. + * + * @return void + */ + public function testNumbers() + { + asrt( setget( "-1" ), "-1" ); + asrt( setget( -1 ), "-1" ); + asrt( setget( "-0.25" ), "-0.25" ); + asrt( setget( -0.25 ), "-0.25" ); + asrt( setget( "1.0" ), "1" ); + asrt( setget( 1.0 ), "1" ); + asrt( setget( "3.20" ), "3.20" ); + asrt( setget( "13.20" ), "13.20" ); + asrt( setget( "134.20" ), "134.20" ); + asrt( setget( 3.21 ), '3.21' ); + asrt( setget( "0.12345678" ), "0.12345678" ); + asrt( setget( 0.12345678 ), "0.12345678" ); + asrt( setget( "-0.12345678" ), "-0.12345678" ); + asrt( setget( -0.12345678 ), "-0.12345678" ); + asrt( setget( "2147483647" ), "2147483647" ); + asrt( setget( 2147483647 ), "2147483647" ); + asrt( setget( -2147483647 ), "-2147483647" ); + asrt( setget( "-2147483647" ), "-2147483647" ); + asrt( setget( -4294967295 ), "-4294967295" ); + asrt( setget( "-4294967295" ), "-4294967295" ); + asrt( setget( 4294967295 ), "4294967295" ); + asrt( setget( "4294967295" ), "4294967295" ); + asrt( setget( "2147483648" ), "2147483648" ); + asrt( setget( "-2147483648" ), "-2147483648" ); + asrt( setget( "199936710040730" ), "199936710040730" ); + asrt( setget( "-199936710040730" ), "-199936710040730" ); + //Architecture dependent... only test this if you are sure what arch + //asrt(setget("2147483647123456"),"2.14748364712346e+15"); + //asrt(setget(2147483647123456),"2.14748364712e+15"); + } + + /** + * Test dates. + * + * @return void + */ + public function testDates() + { + asrt( setget( "2010-10-11" ), "2010-10-11" ); + asrt( setget( "2010-10-11 12:10" ), "2010-10-11 12:10" ); + asrt( setget( "2010-10-11 12:10:11" ), "2010-10-11 12:10:11" ); + asrt( setget( "x2010-10-11 12:10:11" ), "x2010-10-11 12:10:11" ); + } + + /** + * Test strings. + * + * @return void + */ + public function testStrings() + { + asrt( setget( "a" ), "a" ); + asrt( setget( "." ), "." ); + asrt( setget( "\"" ), "\"" ); + asrt( setget( "just some text" ), "just some text" ); + } + + /** + * Test booleans. + * + * @return void + */ + public function testBool() + { + asrt( setget( TRUE ), "1" ); + asrt( setget( FALSE ), "0" ); + asrt( setget( "TRUE" ), "TRUE" ); + asrt( setget( "FALSE" ), "FALSE" ); + } + + /** + * Test NULL. + * + * @return void + */ + public function testNull() + { + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "0123" ), "0123" ); + asrt( setget( "0000123" ), "0000123" ); + asrt( setget( NULL ), NULL ); + asrt( ( setget( 0 ) == 0 ), TRUE ); + asrt( ( setget( 1 ) == 1 ), TRUE ); + asrt( ( setget( TRUE ) == TRUE ), TRUE ); + asrt( ( setget( FALSE ) == FALSE ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Uuid.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Uuid.php new file mode 100644 index 0000000..5fc54ae --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Uuid.php @@ -0,0 +1,291 @@ +id, $book1ID ); + asrt( $book1->title, 'book 1' ); + $book2 = R::load( 'book', $book2ID ); + asrt( $book2->id, $book2ID ); + asrt( $book2->title, 'book 2' ); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->fresh()->with( 'LIMIT 1' )->ownPage ), 1 ); + asrt( count( $book1->fresh()->withCondition( ' title = ? ', array('page 2 of book 1'))->ownPage ), 1 ); + asrt( count($book2->ownPage), 1 ); + asrt( $book2->fresh()->countOwn( 'page' ), 1 ); + $page1 = R::load( 'page', $page1ID ); + asrt( count( $page1->sharedPage ), 0 ); + asrt( $page1->fetchAs( 'book' )->magazine->id, $book2ID ); + $page2 = R::load( 'page', $page2ID ); + asrt( count($page2->sharedPage), 1 ); + asrt( $page2->fresh()->countShared( 'page' ), 1 ); + $page3 = R::findOne( 'page', ' title = ? ', array( 'page 1 of book 2' ) ); + asrt( $page3->id, $page3ID ); + asrt( $page3->book->id, $book2ID ); + } + + /** + * Test Full fluid UUID support. + * + * @return void + */ + public function testFullSupport() + { + R::nuke(); + //Rewire objects to support UUIDs. + $oldToolBox = R::getToolBox(); + $oldAdapter = $oldToolBox->getDatabaseAdapter(); + $uuidWriter = new \UUIDWriterMySQL( $oldAdapter ); + $newRedBean = new OODB( $uuidWriter ); + $newToolBox = new ToolBox( $newRedBean, $oldAdapter, $uuidWriter ); + R::configureFacadeWithToolbox( $newToolBox ); + list( $mansion, $rooms, $ghosts, $key ) = R::dispenseAll( 'mansion,room*3,ghost*4,key' ); + $mansion->name = 'Haunted Mansion'; + $mansion->xownRoomList = $rooms; + $rooms[0]->name = 'Green Room'; + $rooms[1]->name = 'Red Room'; + $rooms[2]->name = 'Blue Room'; + $ghosts[0]->name = 'zero'; + $ghosts[1]->name = 'one'; + $ghosts[2]->name = 'two'; + $ghosts[3]->name = 'three'; + $rooms[0]->sharedGhostList = array( $ghosts[0], $ghosts[1] ); + $rooms[1]->sharedGhostList = array( $ghosts[0], $ghosts[2] ); + $rooms[2]->sharedGhostList = array( $ghosts[1], $ghosts[3], $ghosts[2] ); + $rooms[2]->xownKey = array( $key ); + //Can we store a bean hierachy with UUIDs? + $id = R::store( $mansion ); + asrt( is_string( $id ), TRUE ); + asrt( strlen( $id ), 36 ); + $haunted = R::load( 'mansion', $id ); + asrt( $haunted->name, 'Haunted Mansion' ); + asrt( is_string( $haunted->id ), TRUE ); + asrt( strlen( $haunted->id ), 36 ); + asrt( is_array( $haunted->xownRoomList ), TRUE ); + asrt( count( $haunted->ownRoom ), 3 ); + $rooms = $haunted->xownRoomList; + //Do some counting... + $greenRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Green Room' ) { + $greenRoom = $room; + break; + } + } + asrt( !is_null( $greenRoom ), TRUE ); + asrt( is_array( $greenRoom->with(' ORDER BY id ')->sharedGhostList ), TRUE ); + asrt( count( $greenRoom->sharedGhostList ), 2 ); + $names = array(); + foreach( $greenRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'one,zero'); + $rooms = $haunted->xownRoomList; + $blueRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Blue Room' ) { + $blueRoom = $room; + break; + } + } + asrt( !is_null( $blueRoom ), TRUE ); + asrt( is_array( $blueRoom->sharedGhostList ), TRUE ); + asrt( count( $blueRoom->sharedGhostList ), 3 ); + $names = array(); + foreach( $blueRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'one,three,two'); + $rooms = $haunted->xownRoomList; + $redRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Red Room' ) { + $redRoom = $room; break; + } + } + $names = array(); + foreach( $redRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'two,zero'); + asrt( !is_null( $redRoom ), TRUE ); + asrt( is_array( $redRoom->sharedGhostList ), TRUE ); + asrt( count( $redRoom->sharedGhostList ), 2 ); + //Can we repaint a room? + $redRoom->name = 'Yellow Room'; + $id = R::store($redRoom); + $yellowRoom = R::load( 'room', $id ); + asrt( $yellowRoom->name, 'Yellow Room'); + asrt( !is_null( $yellowRoom ), TRUE ); + asrt( is_array( $yellowRoom->sharedGhostList ), TRUE ); + asrt( count( $yellowRoom->sharedGhostList ), 2 ); + //Can we throw one ghost out? + array_pop( $yellowRoom->sharedGhost ); + R::store( $yellowRoom ); + $yellowRoom = $yellowRoom->fresh(); + asrt( $yellowRoom->name, 'Yellow Room'); + asrt( !is_null( $yellowRoom ), TRUE ); + asrt( is_array( $yellowRoom->sharedGhostList ), TRUE ); + asrt( count( $yellowRoom->sharedGhostList ), 1 ); + //can we remove one of the rooms? + asrt( R::count('key'), 1); + $list = $mansion->withCondition(' `name` = ? ', array('Blue Room'))->xownRoomList; + $room = reset($list); + unset($mansion->xownRoomList[$room->id]); + R::store($mansion); + asrt(R::count('room'), 2); + //and what about its dependent beans? + asrt(R::count('key'), 0); + asrt(R::count('ghost_room'), 3); + //and can we find ghosts? + $ghosts = R::find('ghost'); + asrt(count($ghosts), 4); + $ghosts = R::findAll('ghost', 'ORDER BY id'); + asrt(count($ghosts), 4); + $ghosts = R::findAll('ghost', 'ORDER BY id LIMIT 2'); + asrt(count($ghosts), 2); + $ghostZero = R::findOne('ghost', ' `name` = ? ', array( 'zero' ) ); + asrt( ($ghostZero instanceof OODBBean), TRUE ); + //can we create link properties on existing tables? + $blackRoom = R::dispense( 'room' ); + $blackRoom->name = 'Black Room'; + $ghostZero->link('ghost_room', array('mood'=>'grumpy'))->room = $blackRoom; + R::store($ghostZero); + $ghostZero = $ghostZero->fresh(); + $list = $ghostZero->sharedRoomList; + asrt(count($list), 3); + $ghostZero = $ghostZero->fresh(); + $list = $ghostZero->withCondition(' ghost_room.mood = ? ', array('grumpy'))->sharedRoomList; + asrt(count($list), 1); + //can we load a batch? + $ids = R::getCol('SELECT id FROM ghost'); + $ghosts = R::batch('ghost', $ids); + asrt(count($ghosts), 4); + //can we do an aggregation? + $ghosts = $greenRoom->aggr('ownGhostRoom', 'ghost', 'ghost'); + asrt(count($ghosts), 2); + //can we duplicate the mansion? + asrt(R::count('mansion'), 1); + asrt(R::count('room'), 3); + asrt(R::count('ghost'), 4); + $copy = R::dup($mansion); + R::store($copy); + asrt(R::count('mansion'), 2); + asrt(R::count('room'), 5); //black room does not belong to mansion 1 + asrt(R::count('ghost'), 4); + //can we do some counting using the list? + asrt( $copy->countOwn('room'), 2); + $rooms = $copy->withCondition(' `name` = ? ', array('Green Room'))->xownRoomList; + $room = reset($rooms); + asrt($room->countShared('ghost'), 2); + //Finally restore old toolbox + R::configureFacadeWithToolbox( $oldToolBox ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Writer.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Writer.php new file mode 100644 index 0000000..09cc1cf --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Mysql/Writer.php @@ -0,0 +1,744 @@ +queryRecordCount( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + $count2 = $writer->queryRecordCount( 'book', array( ), ' id = :id ', array( ':id' => $id ) ); + $count3 = $writer->queryRecordCount( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + $count4 = $writer->queryRecordCount( 'book', array( 'id' => $id ) ); + asrt( $count1, $count2 ); + asrt( $count2, $count3 ); + asrt( $count3, $count4 ); + R::nuke(); + $books = R::dispenseAll( 'book*4' ); + $ids = R::storeAll( $books[0] ); + $writer->deleteRecord( 'book', array( 'id' => $ids[0] ) ); + $writer->deleteRecord( 'book', array( 'id' => $ids[1] ), ' id = :id ', array( ':id' => $ids[1] ) ); + $writer->deleteRecord( 'book', NULL, ' id = :id ', array( ':id' => $ids[2] ) ); + $writer->deleteRecord( 'book', array(), ' id = :id ', array( ':id' => $ids[3] ) ); + asrt( R::count( 'book' ), 0 ); + R::nuke(); + $id = R::store( R::dispense( 'book' ) ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array(), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + } + + /** + * Tests wheter we can write a deletion query + * for MySQL using NO conditions but only an + * additional SQL snippet. + * + * @return void + */ + public function testWriteDeleteQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof MySQL ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + $id = R::store( $bean ); + asrt( R::count( 'bean' ), 1 ); + $queryWriter->deleteRecord( 'bean', array(), $addSql = ' id = :id ', $bindings = array( ':id' => $id ) ); + asrt( R::count( 'bean' ), 0 ); + } + + /** + * Tests wheter we can write a counting query + * for MySQL using conditions and an additional SQL snippet. + * + * @return void + */ + public function testWriteCountQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof MySQL ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $count = $queryWriter->queryRecordCount( 'bean', array( 'name' => 'b' ), $addSql = ' id > :id ', $bindings = array( ':id' => 0 ) ); + asrt( $count, 2 ); + } + + /** + * Tests whether we can write a MySQL join and + * whether the correct exception is thrown in case + * of an invalid join. + * + * @return void + */ + public function testWriteJoinSnippets() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof MySQL ), TRUE ); + $snippet = $queryWriter->writeJoin( 'book', 'page' ); //default must be LEFT + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'LEFT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'RIGHT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' RIGHT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'INNER' ); + asrt( ' INNER JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $exception = NULL; + try { + $snippet = $queryWriter->writeJoin( 'book', 'page', 'MIDDLE' ); + } + catch(\Exception $e) { + $exception = $e; + } + asrt( ( $exception instanceof RedException ), TRUE ); + $errorMessage = $exception->getMessage(); + asrt( is_string( $errorMessage ), TRUE ); + asrt( ( strlen( $errorMessage ) > 0 ), TRUE ); + asrt( $errorMessage, 'Invalid JOIN.' ); + } + + /** + * Test whether we can store JSON as a JSON column + * and whether this plays well with the other data types. + */ + public function testSetGetJSON() + { + /* a stub test in case full test cannot be performed, see below */ + R::useJSONFeatures( TRUE ); + asrt( R::getWriter()->scanType( '[1,2,3]', TRUE ), MySQL::C_DATATYPE_SPECIAL_JSON ); + R::useJSONFeatures( FALSE ); + global $travis; + if ($travis) return; + /* does not work on MariaDB */ + $version = strtolower( R::getCell('select version()') ); + if ( strpos( $version, 'mariadb' ) !== FALSE ) return; + // Check if database platform is MariaDB < 10.2 + $selectVersion = R::getDatabaseAdapter()->getCol( 'SELECT VERSION()' ); + list ( $version, $dbPlatform ) = explode( '-', reset ( $selectVersion ) ); + list( $versionMajor, $versionMinor, $versionPatch ) = explode( '.', $version ); + if ( $dbPlatform == "MariaDB" && $versionMajor <= 10 && $versionMinor < 2 ) { + // No support for JSON columns, abort test + return; + } + R::nuke(); + $bean = R::dispense('bean'); + $message = json_encode( array( 'message' => 'hello', 'type' => 'greeting' ) ); + $bean->data = $message; + R::store( $bean ); + $columns = R::inspect('bean'); + asrt( array_key_exists( 'data', $columns ), TRUE ); + asrt( ( $columns['data'] !== 'json' ), TRUE ); + R::useJSONFeatures( TRUE ); + R::nuke(); + $bean = R::dispense('bean'); + $message = array( 'message' => 'hello', 'type' => 'greeting' ); + $bean->data = $message; + R::store( $bean ); + $columns = R::inspect('bean'); + asrt( array_key_exists( 'data', $columns ), TRUE ); + asrt( $columns['data'], 'json' ); + $bean = $bean->fresh(); + $message = json_decode( $bean->data, TRUE ); + asrt( $message['message'], 'hello' ); + asrt( $message['type'], 'greeting' ); + $message['message'] = 'hi'; + $bean->data = $message; + R::store( $bean ); + pass(); + $bean = R::findOne( 'bean' ); + $message = json_decode( $bean->data ); + asrt( $message->message, 'hi' ); + $book = R::dispense( 'book' ); + $book->page = 'lorem ipsum'; + R::store( $book ); + $book = $book->fresh(); + asrt( $book->page, 'lorem ipsum' ); + $book2 = R::dispense( 'book' ); + $book2->page = array( 'chapter' => '1' ); + R::store( $book2 ); + pass(); //should not try to modify column and trigger exception + $book = $book->fresh(); + asrt( $book->page, 'lorem ipsum' ); + $columns = R::inspect('book'); + asrt( ( $columns['page'] !== 'json' ), TRUE ); + $building = R::dispense( 'building' ); + $building->year = 'MLXXVIII'; + R::store( $building ); + $shop = R::dispense( 'building' ); + $shop->year = '2010-01-01'; + R::store( $shop ); + $building = R::load( 'building', $building->id ); + asrt( $building->year, 'MLXXVIII' ); + $columns = R::inspect( 'building' ); + asrt( strpos( strtolower( $columns['year'] ), 'date' ), FALSE ); + $shop->anno = '2010-01-01'; + R::store( $shop ); + $columns = R::inspect( 'building' ); + asrt( $columns['anno'], 'date' ); + R::useJSONFeatures( FALSE ); + } + + /** + * Test Facade bind function method. + * Test for MySQL WKT spatial format. + */ + public function testFunctionFilters() + { + R::nuke(); + R::bindFunc( 'read', 'location.point', 'asText' ); + R::bindFunc( 'write', 'location.point', 'GeomFromText' ); + R::store(R::dispense('location')); + R::freeze( TRUE ); + try { + R::find('location'); + fail(); + } catch( SQL $exception ) { + pass(); + } + R::freeze( FALSE ); + try { + R::find('location'); + pass(); + } catch( SQL $exception ) { + fail(); + } + $location = R::dispense( 'location' ); + $location->point = 'POINT(14 6)'; + R::store($location); + $columns = R::inspect( 'location' ); + asrt( $columns['point'], 'point' ); + $location = $location->fresh(); + asrt( $location->point, 'POINT(14 6)' ); + R::nuke(); + $location = R::dispense( 'location' ); + $location->point = 'LINESTRING(0 0,1 1,2 2)'; + R::store($location); + $columns = R::inspect( 'location' ); + asrt( $columns['point'], 'linestring' ); + $location->bustcache = 2; + R::store($location); + $location = $location->fresh(); + asrt( $location->point, 'LINESTRING(0 0,1 1,2 2)' ); + R::nuke(); + $location = R::dispense( 'location' ); + $location->point = 'POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7,5 5))'; + R::store($location); + $columns = R::inspect( 'location' ); + asrt( $columns['point'], 'polygon' ); + $location->bustcache = 4; + R::store($location); + $location = $location->fresh(); + asrt( $location->point, 'POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7,5 5))' ); + R::bindFunc( 'read', 'location.point', NULL ); + $location->bustcache = 1; + R::store($location); + $location = $location->fresh(); + asrt( ( $location->point === 'POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7,5 5))' ), FALSE ); + $filters = AQueryWriter::getSQLFilters(); + asrt( is_array( $filters ), TRUE ); + asrt( count( $filters ), 2 ); + asrt( isset( $filters[ QueryWriter::C_SQLFILTER_READ] ), TRUE ); + asrt( isset( $filters[ QueryWriter::C_SQLFILTER_WRITE] ), TRUE ); + R::bindFunc( 'read', 'place.point', 'asText' ); + R::bindFunc( 'write', 'place.point', 'GeomFromText' ); + R::bindFunc( 'read', 'place.line', 'asText' ); + R::bindFunc( 'write', 'place.line', 'GeomFromText' ); + R::nuke(); + $place = R::dispense( 'place' ); + $place->point = 'POINT(13.2 666.6)'; + $place->line = 'LINESTRING(9.2 0,3 1.33)'; + R::store( $place ); + $columns = R::inspect( 'place' ); + asrt( $columns['point'], 'point' ); + asrt( $columns['line'], 'linestring' ); + $place = R::findOne('place'); + asrt( $place->point, 'POINT(13.2 666.6)' ); + asrt( $place->line, 'LINESTRING(9.2 0,3 1.33)' ); + R::bindFunc( 'read', 'place.point', NULL ); + R::bindFunc( 'write', 'place.point', NULL ); + R::bindFunc( 'read', 'place.line', NULL ); + R::bindFunc( 'write', 'place.line', NULL ); + } + + /** + * Test scanning and coding of values. + * + * @return void + */ + public function testScanningAndCoding() + { + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + $adapter->exec( "DROP TABLE IF EXISTS testtable" ); + asrt( in_array( "testtable", $adapter->getCol( "show tables" ) ), FALSE ); + $writer->createTable( "testtable" ); + asrt( in_array( "testtable", $adapter->getCol( "show tables" ) ), TRUE ); + asrt( count( array_diff( $writer->getTables(), $adapter->getCol( "show tables" ) ) ), 0 ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 1 ); + asrt( in_array( "id", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), FALSE ); + $writer->addColumn( "testtable", "c1", MySQL::C_DATATYPE_UINT32 ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 2 ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + foreach ( $writer->sqltype_typeno as $key => $type ) { + if ( $type < 100 ) { + asrt( $writer->code( $key, TRUE ), $type ); + } else { + asrt( $writer->code( $key, TRUE ), MySQL::C_DATATYPE_SPECIFIED ); + } + } + asrt( $writer->code( MySQL::C_DATATYPE_SPECIAL_DATETIME ), MySQL::C_DATATYPE_SPECIFIED ); + asrt( $writer->code( "unknown" ), MySQL::C_DATATYPE_SPECIFIED ); + asrt( $writer->scanType( FALSE ), MySQL::C_DATATYPE_BOOL ); + asrt( $writer->scanType( TRUE ), MySQL::C_DATATYPE_BOOL ); + asrt( $writer->scanType( 0 ), MySQL::C_DATATYPE_BOOL ); + asrt( $writer->scanType( 1 ), MySQL::C_DATATYPE_BOOL ); + asrt( $writer->scanType( INF ), MySQL::C_DATATYPE_TEXT7 ); + asrt( $writer->scanType( NULL ), MySQL::C_DATATYPE_BOOL ); + asrt( $writer->scanType( 2 ), MySQL::C_DATATYPE_UINT32 ); + asrt( $writer->scanType( 255 ), MySQL::C_DATATYPE_UINT32 ); //no more uint8 + asrt( $writer->scanType( 256 ), MySQL::C_DATATYPE_UINT32 ); + asrt( $writer->scanType( -1 ), MySQL::C_DATATYPE_DOUBLE ); + asrt( $writer->scanType( 1.5 ), MySQL::C_DATATYPE_DOUBLE ); + asrt( $writer->scanType( "abc" ), MySQL::C_DATATYPE_TEXT7 ); + asrt( $writer->scanType( str_repeat( 'abcd', 100000 ) ), MySQL::C_DATATYPE_TEXT32 ); + asrt( $writer->scanType( "2001-10-10", TRUE ), MySQL::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->scanType( "2001-10-10 10:00:00", TRUE ), MySQL::C_DATATYPE_SPECIAL_DATETIME ); + asrt( $writer->scanType( "2001-10-10" ), MySQL::C_DATATYPE_TEXT7 ); + asrt( $writer->scanType( "2001-10-10 10:00:00" ), MySQL::C_DATATYPE_TEXT7 ); + asrt( $writer->scanType( "1.23", TRUE ), MySQL::C_DATATYPE_SPECIAL_MONEY ); + asrt( $writer->scanType( "12.23", TRUE ), MySQL::C_DATATYPE_SPECIAL_MONEY ); + asrt( $writer->scanType( "124.23", TRUE ), MySQL::C_DATATYPE_SPECIAL_MONEY ); + asrt( $writer->scanType( str_repeat( "lorem ipsum", 100 ) ), MySQL::C_DATATYPE_TEXT16 ); + $writer->widenColumn( "testtable", "c1", MySQL::C_DATATYPE_UINT32 ); + $writer->addColumn( "testtable", "special", MySQL::C_DATATYPE_SPECIAL_DATE ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special'], TRUE ), MySQL::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->code( $cols['special'], FALSE ), MySQL::C_DATATYPE_SPECIFIED ); + $writer->addColumn( "testtable", "special2", MySQL::C_DATATYPE_SPECIAL_DATETIME ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special2'], TRUE ), MySQL::C_DATATYPE_SPECIAL_DATETIME ); + asrt( $writer->code( $cols['special'], FALSE ), MySQL::C_DATATYPE_SPECIFIED ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), MySQL::C_DATATYPE_UINT32 ); + $writer->widenColumn( "testtable", "c1", MySQL::C_DATATYPE_DOUBLE ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), MySQL::C_DATATYPE_DOUBLE ); + $writer->widenColumn( "testtable", "c1", MySQL::C_DATATYPE_TEXT7 ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), MySQL::C_DATATYPE_TEXT7 ); + $writer->widenColumn( "testtable", "c1", MySQL::C_DATATYPE_TEXT8 ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), MySQL::C_DATATYPE_TEXT8 ); + $id = $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "lorem ipsum" ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "lorem ipsum" ); + $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "ipsum lorem" ) ), $id ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "ipsum lorem" ); + $writer->deleteRecord( "testtable", array( "id" => array( $id ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( empty( $row ), TRUE ); + $writer->addColumn( "testtable", "c2", MySQL::C_DATATYPE_UINT32 ); + } + + /** + * (FALSE should be stored as 0 not as '') + * + * @return void + */ + public function testZeroIssue() + { + testpack( "Zero issue" ); + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $pdo = $adapter->getDatabase(); + $pdo->Execute( "DROP TABLE IF EXISTS `zero`" ); + $bean = $redbean->dispense( "zero" ); + $bean->zero = FALSE; + $bean->title = "bla"; + $redbean->store( $bean ); + asrt( count( $redbean->find( "zero", array(), " zero = 0 " ) ), 1 ); + R::store( R::dispense( 'hack' ) ); + testpack( "Test RedBean Security - bean interface " ); + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean = $redbean->load( "page", "13; drop table hack" ); + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + try { + $bean = $redbean->load( "page where 1; drop table hack", 1 ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean = $redbean->dispense( "page" ); + $evil = "; drop table hack"; + $bean->id = $evil; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + unset( $bean->id ); + $bean->name = "\"" . $evil; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean->name = "'" . $evil; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean->$evil = 1; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + unset( $bean->$evil ); + $bean->id = 1; + $bean->name = "\"" . $evil; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean->name = "'" . $evil; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + $bean->$evil = 1; + try { + $redbean->store( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + try { + $redbean->trash( $bean ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + try { + $redbean->find( "::", array(), "" ); + } catch (\Exception $e ) { + pass(); + } + $adapter->exec( "drop table if exists sometable" ); + testpack( "Test RedBean Security - query writer" ); + try { + $writer->createTable( "sometable` ( `id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT , PRIMARY KEY ( `id` ) ) ENGINE = InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ; drop table hack; --" ); + } catch (\Exception $e ) { + } + asrt( in_array( "hack", $adapter->getCol( "show tables" ) ), TRUE ); + testpack( "Test ANSI92 issue in clearrelations" ); + $pdo->Execute( "DROP TABLE IF EXISTS book_group" ); + $pdo->Execute( "DROP TABLE IF EXISTS author_book" ); + $pdo->Execute( "DROP TABLE IF EXISTS book" ); + $pdo->Execute( "DROP TABLE IF EXISTS author" ); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + set1toNAssoc( $a, $book, $author1 ); + set1toNAssoc( $a, $book, $author2 ); + pass(); + $pdo->Execute( "DROP TABLE IF EXISTS book_group" ); + $pdo->Execute( "DROP TABLE IF EXISTS book_author" ); + $pdo->Execute( "DROP TABLE IF EXISTS author_book" ); + $pdo->Execute( "DROP TABLE IF EXISTS book" ); + $pdo->Execute( "DROP TABLE IF EXISTS author" ); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->associate( $book, $author1 ); + $a->associate( $book, $author2 ); + pass(); + testpack( "Test Association Issue Group keyword (Issues 9 and 10)" ); + $pdo->Execute( "DROP TABLE IF EXISTS `book_group`" ); + $pdo->Execute( "DROP TABLE IF EXISTS `group`" ); + $group = $redbean->dispense( "group" ); + $group->name = "mygroup"; + $redbean->store( $group ); + try { + $a->associate( $group, $book ); + pass(); + } catch ( SQL $e ) { + fail(); + } + // Test issue SQL error 23000 + try { + $a->associate( $group, $book ); + pass(); + } catch ( SQL $e ) { + fail(); + } + asrt( (int) $adapter->getCell( "select count(*) from book_group" ), 1 ); //just 1 rec! + $pdo->Execute( "DROP TABLE IF EXISTS book_group" ); + $pdo->Execute( "DROP TABLE IF EXISTS author_book" ); + $pdo->Execute( "DROP TABLE IF EXISTS book" ); + $pdo->Execute( "DROP TABLE IF EXISTS author" ); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->unassociate( $book, $author1 ); + $a->unassociate( $book, $author2 ); + pass(); + $redbean->trash( $redbean->dispense( "bla" ) ); + pass(); + $bean = $redbean->dispense( "bla" ); + $bean->name = 1; + $bean->id = 2; + $redbean->trash( $bean ); + pass(); + } + + /** + * Test special data types. + * + * @return void + */ + public function testTypes() + { + testpack( 'Special data types' ); + $bean = R::dispense( 'bean' ); + $bean->date = 'someday'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'varchar(191)' ); + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'varchar(191)' ); + } + + /** + * Test date types. + * + * @return void + */ + public function testTypesDates() + { + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'date' ); + } + + /** + * Test money types. + * + * @return void + */ + public function testTypesMon() + { + $bean = R::dispense( 'bean' ); + $bean->amount = '22.99'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['amount'], 'decimal(10,2)' ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->amount = '-22.99'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['amount'], 'decimal(10,2)' ); + } + + + /** + * Date-time + * + * @return void + */ + public function testTypesDateTimes() + { + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10 10:00:00'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'datetime' ); + $bean = R::dispense( 'bean' ); + try { + $bean = R::dispense( 'bean' ); + $bean->title = 123; + $bean->setMeta( 'cast.title', 'invalid' ); + R::store( $bean ); + fail(); + } catch ( RedException $e ) { + pass(); + } catch (\Exception $e ) { + fail(); + } + $bean = R::dispense( 'bean' ); + $bean->title = 123; + $bean->setMeta( 'cast.title', 'text' ); + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['title'], 'text' ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->title = 123; + $bean->setMeta( 'cast.title', 'string' ); + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['title'], 'varchar(191)' ); + } + + /** + * Stored and reloads spatial data to see if the + * value is preserved correctly. + * + * @return void + */ + protected function setGetSpatial( $data ) + { + R::nuke(); + $place = R::dispense( 'place' ); + $place->location = $data; + R::store( $place ); + asrt( R::getCell( 'SELECT AsText(location) FROM place LIMIT 1' ), $data ); + } + + /** + * Can we manually add a MySQL time column? + * + * @return void + */ + public function testTime() + { + R::nuke(); + $clock = R::dispense('clock'); + $clock->time = '10:00:00'; + $clock->setMeta('cast.time', 'time'); + R::store( $clock ); + $columns = R::inspect('clock'); + asrt( $columns['time'], 'time' ); + $clock = R::findOne('clock'); + $clock->time = '12'; + R::store($clock); + $clock = R::findOne('clock'); + $time = $clock->time; + asrt( ( strpos( $time, ':' ) > 0 ), TRUE ); + } + + /** + * Can we use the 'ignoreDisplayWidth'-feature for MySQL 8 + * compatibility? + * + * @return void + */ + public function testWriterFeature() + { + $adapter = R::getToolBox()->getDatabaseAdapter(); + $writer = new \RedBeanPHP\QueryWriter\MySQL( $adapter ); + $writer->useFeature('ignoreDisplayWidth'); + asrt($writer->typeno_sqltype[MySQL::C_DATATYPE_BOOL],' TINYINT UNSIGNED '); + asrt($writer->typeno_sqltype[MySQL::C_DATATYPE_UINT32],' INT UNSIGNED '); + asrt($writer->sqltype_typeno['tinyint unsigned'],MySQL::C_DATATYPE_BOOL); + asrt($writer->sqltype_typeno['int unsigned'],MySQL::C_DATATYPE_UINT32); + //Can we also pass invalid features without errors? + $writer->useFeature('nonsense'); + pass(); + } + + /** + * Can we pass an options array to Writer Constructor? + * + * @return void + */ + public function testWriterOptions() + { + $adapter = R::getToolBox()->getDatabaseAdapter(); + $writer = new \RedBeanPHP\QueryWriter\MySQL( $adapter, array('noInitcode'=>TRUE) ); + pass(); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres.php new file mode 100644 index 0000000..950f22d --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres.php @@ -0,0 +1,34 @@ +id, $book1ID ); + asrt( $book1->title, 'book 1' ); + $book2 = R::load( 'book', $book2ID ); + asrt( $book2->id, $book2ID ); + asrt( $book2->title, 'book 2' ); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->fresh()->with( 'LIMIT 1' )->ownPage ), 1 ); + asrt( count( $book1->fresh()->withCondition( ' title = ? ', array('page 2 of book 1'))->ownPage ), 1 ); + asrt( count($book2->ownPage), 1 ); + asrt( $book2->fresh()->countOwn( 'page' ), 1 ); + $page1 = R::load( 'page', $page1ID ); + asrt( count( $page1->sharedPage ), 0 ); + asrt( $page1->fetchAs( 'book' )->magazine->id, $book2ID ); + $page2 = R::load( 'page', $page2ID ); + asrt( count($page2->sharedPage), 1 ); + asrt( $page2->fresh()->countShared( 'page' ), 1 ); + $page3 = R::findOne( 'page', ' title = ? ', array( 'page 1 of book 2' ) ); + asrt( $page3->id, $page3ID ); + asrt( $page3->book->id, $book2ID ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Foreignkeys.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Foreignkeys.php new file mode 100644 index 0000000..49f9efa --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Foreignkeys.php @@ -0,0 +1,292 @@ +addFK( 'a', 'b', 'c', 'd' ); //must fail + pass(); //survive without exception + asrt( $a, FALSE ); //must return FALSE + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $cover = R::dispense( 'cover' ); + list( $g1, $g2 ) = R::dispense( 'genre', 2 ); + $g1->name = '1'; + $g2->name = '2'; + $book->ownPage = array( $page ); + $book->cover = $cover; + $book->sharedGenre = array( $g1, $g2 ); + R::store( $book ); + $sql = "SELECT + tc.constraint_name, tc.table_name, kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name + JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name + WHERE constraint_type = 'FOREIGN KEY' AND (tc.table_name='book' OR tc.table_name='book_genre' OR tc.table_name='page');"; + $fks = R::getAll( $sql ); + $json = '[ + { + "constraint_name": "book_cover_id_fkey", + "table_name": "book", + "column_name": "cover_id", + "foreign_table_name": "cover", + "foreign_column_name": "id" + }, + { + "constraint_name": "page_book_id_fkey", + "table_name": "page", + "column_name": "book_id", + "foreign_table_name": "book", + "foreign_column_name": "id" + }, + { + "constraint_name": "book_genre_genre_id_fkey", + "table_name": "book_genre", + "column_name": "genre_id", + "foreign_table_name": "genre", + "foreign_column_name": "id" + }, + { + "constraint_name": "book_genre_book_id_fkey", + "table_name": "book_genre", + "column_name": "book_id", + "foreign_table_name": "book", + "foreign_column_name": "id" + } + ]'; + $j = json_encode( $fks ); + $j1 = json_decode( $j, TRUE ); + $j2 = json_decode( $json, TRUE ); + foreach ( $j1 as $jrow ) { + $s = json_encode( $jrow ); + $found = 0; + foreach ( $j2 as $k => $j2row ) { + if ( json_encode( $j2row ) === $s ) { + pass(); + unset( $j2[$k] ); + $found = 1; + } + } + if ( !$found ) fail(); + } + } + + /** + * Test constraint function directly in Writer. + * + * @return void + */ + public function testConstraint() + { + R::nuke(); + $database = R::getCell('SELECT current_database()'); + $sql = 'CREATE TABLE book (id SERIAL PRIMARY KEY)'; + R::exec( $sql ); + $sql = 'CREATE TABLE page (id SERIAL PRIMARY KEY)'; + R::exec( $sql ); + $sql = 'CREATE TABLE book_page ( + id SERIAL PRIMARY KEY, + book_id INTEGER, + page_id INTEGER + )'; + R::exec( $sql ); + $writer = R::getWriter(); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'book_page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 0 ); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 2 ); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 2 ); + } + + /** + * Test adding foreign keys. + * + * @return void + */ + public function testAddingForeignKey() + { + R::nuke(); + $database = R::getCell('SELECT current_database()'); + $sql = 'CREATE TABLE book ( + id SERIAL PRIMARY KEY + )'; + R::exec( $sql ); + $sql = 'CREATE TABLE page ( + id SERIAL PRIMARY KEY, + book_id INTEGER + )'; + R::exec( $sql ); + $writer = R::getWriter(); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 0 ); + $writer->addFK('page', 'page', 'book_id', 'id', TRUE); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 1 ); + //dont add twice + $writer->addFK('page', 'page', 'book_id', 'id', TRUE); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 1 ); + //even if it is different + $writer->addFK('page', 'page', 'book_id', 'id', FALSE); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 1 ); + R::nuke(); + $sql = 'CREATE TABLE book ( + id SERIAL PRIMARY KEY + )'; + R::exec( $sql ); + $sql = 'CREATE TABLE page ( + id SERIAL PRIMARY KEY, + book_id INTEGER + )'; + R::exec( $sql ); + $writer = R::getWriter(); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 0 ); + $writer->addFK('page', 'page', 'book_id', 'id', FALSE); + $sql = " + SELECT + COUNT(*) + FROM information_schema.key_column_usage AS k + LEFT JOIN information_schema.table_constraints AS c ON c.constraint_name = k.constraint_name + WHERE k.table_catalog = '$database' + AND k.table_schema = 'public' + AND k.table_name = 'page' + AND c.constraint_type = 'FOREIGN KEY'"; + $numFKS = R::getCell( $sql ); + asrt( (int) $numFKS, 1 ); + } + + /** + * Test whether we can manually create indexes. + * + * @return void + */ + public function testAddingIndex() + { + R::nuke(); + $sql = 'CREATE TABLE song ( + id SERIAL PRIMARY KEY, + album_id INTEGER, + category VARCHAR(255) + )'; + R::exec( $sql ); + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 1 ); + $writer = R::getWriter(); + $writer->addIndex( 'song', 'index1', 'album_id' ); + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 2 ); + //Cant add the same index twice + $writer->addIndex( 'song', 'index1', 'album_id' ); + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 2 ); + $writer->addIndex( 'song', 'index2', 'category' ); + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 3 ); + //Dont fail, just dont + try { + $writer->addIndex( 'song', 'index3', 'nonexistant' ); + pass(); + } catch( \Exception $e ) { + fail(); + } + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 3 ); + try { + $writer->addIndex( 'nonexistant', 'index4', 'nonexistant' ); + pass(); + } catch( \Exception $e ) { + fail(); + } + $indexes = R::getAll( " SELECT * FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'song' "); + asrt( count( $indexes ), 3 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Parambind.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Parambind.php new file mode 100644 index 0000000..fdfa517 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Parambind.php @@ -0,0 +1,51 @@ +name = "abc"; + $page->number = 2; + R::store( $page ); + R::exec( "insert into page (name) values(:name) ", array( ":name" => "my name" ) ); + R::exec( "insert into page (number) values(:one) ", array( ":one" => 1 ) ); + R::exec( "insert into page (number) values(:one) ", array( ":one" => "1" ) ); + R::exec( "insert into page (number) values(:one) ", array( ":one" => "1234" ) ); + R::exec( "insert into page (number) values(:one) ", array( ":one" => "-21" ) ); + pass(); + testpack( 'Test whether we can properly bind and receive NULL values' ); + $adapter = R::getDatabaseAdapter(); + asrt( $adapter->getCell( 'SELECT TEXT( :nil ) ', array( ':nil' => 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT TEXT( :nil ) ', array( ':nil' => NULL ) ), NULL ); + asrt( $adapter->getCell( 'SELECT TEXT( ? ) ', array( 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT TEXT( ? ) ', array( NULL ) ), NULL ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Partial.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Partial.php new file mode 100644 index 0000000..04fdb64 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Partial.php @@ -0,0 +1,112 @@ +property1 = 'value1'; + $id = R::store( $bean ); + R::freeze( TRUE ); + R::exec('ALTER TABLE bean ADD COLUMN "property2" BOOLEAN DEFAULT FALSE;'); + $bean = R::load( 'bean', $id ); + $bean->property1 = 'value1b'; + /* we cant save the bean, because there is an unsupported field type in the table */ + /* this was the bug... (or missing feature?) */ + try { + R::store( $bean ); + fail(); + } catch( SQLException $e ) { + asrt( strpos( $e->getMessage(), 'Invalid text representation' ) > 1, TRUE ); + asrt( $e->getSQLState(), '22P02' ); + } + /* solved by adding feature partial beans, now only the changed properties get saved */ + R::usePartialBeans( TRUE ); + $id = R::store( $bean ); + /* no exception... */ + pass(); + /* also test behavior of boolean column in general */ + $bean = R::load( 'bean', $id ); + asrt( $bean->property1, 'value1b' ); + asrt( $bean->property2, FALSE ); + $bean->property2 = TRUE; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + asrt( $bean->property1, 'value1b' ); + asrt( $bean->property2, TRUE ); + $bean->property2 = FALSE; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + asrt( $bean->property1, 'value1b' ); + asrt( $bean->property2, FALSE ); + $bean->property2 = 't'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + asrt( $bean->property1, 'value1b' ); + asrt( $bean->property2, TRUE ); + $bean->property2 = 'f'; + $id = R::store( $bean ); + $bean = R::load( 'bean', $id ); + asrt( $bean->property1, 'value1b' ); + asrt( $bean->property2, FALSE ); + /* but invalid text should not work */ + $bean->property2 = 'tx'; //instead of just 't' + try { + $id = R::store( $bean ); + fail(); + } catch( SQLException $e ) { + asrt( strpos( $e->getMessage(), 'Invalid text representation' ) > 1, TRUE ); + asrt( $e->getSQLState(), '22P02' ); + } + R::usePartialBeans( FALSE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Setget.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Setget.php new file mode 100644 index 0000000..87a931c --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Setget.php @@ -0,0 +1,138 @@ +setTimeZone( new \DateTimeZone( 'Europe/Amsterdam' ) ); + $dt->setDate( 1981, 5, 1 ); + $dt->setTime( 3, 13, 13 ); + asrt( setget( $dt ), '1981-05-01 03:13:13' ); + $bean = R::dispense( 'bean' ); + $bean->dt = $dt; + } + + /** + * Test numbers. + * + * @return void + */ + public function testNumbers() + { + asrt( setget( "-1" ), "-1" ); + asrt( setget( -1 ), "-1" ); + asrt( setget( "1.0" ), "1" ); + asrt( setget( 1.0 ), "1" ); + asrt( setget( "-0.25" ), "-0.25" ); + asrt( setget( -0.25 ), "-0.25" ); + asrt( setget( "3.20" ), "3.20" ); + asrt( setget( "13.20" ), "13.20" ); + asrt( setget( "134.20" ), "134.20" ); + asrt( setget( 3.21 ), '3.21' ); + asrt( setget( "0.12345678" ), "0.12345678" ); + asrt( setget( 0.12345678 ), "0.12345678" ); + asrt( setget( "-0.12345678" ), "-0.12345678" ); + asrt( setget( -0.12345678 ), "-0.12345678" ); + asrt( setget( "2147483647" ), "2147483647" ); + asrt( setget( 2147483647 ), "2147483647" ); + asrt( setget( -2147483647 ), "-2147483647" ); + asrt( setget( "-2147483647" ), "-2147483647" ); + asrt( setget( "2147483648" ), "2147483648" ); + asrt( setget( "-2147483648" ), "-2147483648" ); + asrt( setget( "199936710040730" ), "199936710040730" ); + asrt( setget( "-199936710040730" ), "-199936710040730" ); + } + + /** + * Test dates. + * + * @return void + */ + public function testDates() + { + asrt( setget( "2010-10-11" ), "2010-10-11" ); + asrt( setget( "2010-10-11 12:10" ), "2010-10-11 12:10" ); + asrt( setget( "2010-10-11 12:10:11" ), "2010-10-11 12:10:11" ); + asrt( setget( "x2010-10-11 12:10:11" ), "x2010-10-11 12:10:11" ); + } + + /** + * Test strings. + * + * @return void + */ + public function testStrings() + { + asrt( setget( "a" ), "a" ); + asrt( setget( "." ), "." ); + asrt( setget( "\"" ), "\"" ); + asrt( setget( "just some text" ), "just some text" ); + } + + /** + * Test booleans. + * + * @return void + */ + public function testBool() + { + asrt( setget( TRUE ), "1" ); + asrt( setget( FALSE ), "0" ); + asrt( setget( "TRUE" ), "TRUE" ); + asrt( setget( "FALSE" ), "FALSE" ); + } + + /** + * Test NULL. + * + * @return void + */ + public function testNull() + { + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( NULL ), NULL ); + asrt( ( setget( 0 ) == 0 ), TRUE ); + asrt( ( setget( 1 ) == 1 ), TRUE ); + asrt( ( setget( TRUE ) == TRUE ), TRUE ); + asrt( ( setget( FALSE ) == FALSE ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Trees.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Trees.php new file mode 100644 index 0000000..ca01e78 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Trees.php @@ -0,0 +1,317 @@ +title; + } + return implode( ',', $names ); + } + + /** + * Test trees + * + * @return void + */ + public function testCTETrees() + { + R::nuke(); + $pages = R::dispense(array( + '_type' => 'page', + 'title' => 'home', + 'ownPageList' => array(array( + '_type' => 'page', + 'title' => 'shop', + 'ownPageList' => array(array( + '_type' => 'page', + 'title' => 'wines', + 'ownPageList' => array(array( + '_type' => 'page', + 'title' => 'whiskies', + )) + )) + )) + )); + R::store( $pages ); + $whiskyPage = R::findOne( 'page', 'title = ?', array('whiskies') ); + asrt( $this->summarize( R::parents( $whiskyPage, ' ORDER BY title ASC ' ) ), 'home,shop,whiskies,wines' ); + asrt( $this->summarize( R::children( $whiskyPage, ' ORDER BY title ASC ' ) ), 'whiskies' ); + $homePage = R::findOne( 'page', 'title = ?', array('home') ); + asrt( $this->summarize( R::parents( $homePage, ' ORDER BY title ASC ' ) ), 'home' ); + asrt( $this->summarize( R::children( $homePage, ' ORDER BY title ASC ' ) ), 'home,shop,whiskies,wines' ); + $shopPage = R::findOne( 'page', 'title = ?', array('shop') ); + asrt( $this->summarize( R::parents( $shopPage, ' ORDER BY title ASC ' ) ), 'home,shop' ); + asrt( $this->summarize( R::children( $shopPage, ' ORDER BY title ASC ' ) ), 'shop,whiskies,wines' ); + $winePage = R::findOne( 'page', 'title = ?', array('wines') ); + asrt( $this->summarize( R::parents( $winePage, ' ORDER BY title ASC ' ) ), 'home,shop,wines' ); + asrt( $this->summarize( R::children( $winePage, ' ORDER BY title ASC ' ) ), 'whiskies,wines' ); + asrt( $this->summarize( R::children( $winePage, ' title NOT IN (\'wines\') ORDER BY title ASC ' ) ), 'whiskies' ); + asrt( $this->summarize( R::parents( $winePage, ' title NOT IN (\'home\') ORDER BY title ASC ' ) ), 'shop,wines' ); + asrt( $this->summarize( R::parents( $winePage, ' ORDER BY title ASC ', array() ) ), 'home,shop,wines' ); + asrt( $this->summarize( R::children( $winePage, ' ORDER BY title ASC ', array() ) ), 'whiskies,wines' ); + asrt( $this->summarize( R::children( $winePage, ' title NOT IN (\'wines\') ORDER BY title ASC ', array() ) ), 'whiskies' ); + asrt( $this->summarize( R::parents( $winePage, ' title NOT IN (\'home\') ORDER BY title ASC ', array() ) ), 'shop,wines' ); + asrt( $this->summarize( R::children( $winePage, ' title != ? ORDER BY title ASC ', array( 'wines' ) ) ), 'whiskies' ); + asrt( $this->summarize( R::parents( $winePage, ' title != ? ORDER BY title ASC ', array( 'home' ) ) ), 'shop,wines' ); + asrt( $this->summarize( R::children( $winePage, ' title != :title ORDER BY title ASC ', array( ':title' => 'wines' ) ) ), 'whiskies' ); + asrt( $this->summarize( R::parents( $winePage, ' title != :title ORDER BY title ASC ', array( ':title' => 'home' ) ) ), 'shop,wines' ); + asrt( R::countChildren( $homePage ), 3 ); + asrt( R::countParents( $whiskyPage ), 3 ); + asrt( R::countChildren( $winePage, ' title != :title ', array( ':title' => 'wines' ) ) , 1 ); + asrt( R::countChildren( $winePage, ' title = :title ', array( ':title' => 'wines' ) ) , 1 ); + asrt( R::countParents( $winePage, ' title != :title ', array( ':title' => 'home' ) ) , 2 ); + } + + /** + * Test CTE and Parsed Joins. + * + * @return void + */ + public function testCTETreesAndParsedJoins() + { + R::nuke(); + list($cards, $details, $colors) = R::dispenseAll('card*9,detail*4,color*2'); + $colors[0]->name = 'red'; + $colors[1]->name = 'black'; + $details[0]->points = 500; + $details[1]->points = 200; + $details[2]->points = 300; + $details[3]->points = 100; + $cards[0]->ownCardList = array( $cards[1] ); + $cards[1]->ownCardList = array( $cards[2], $cards[3] ); + $cards[2]->ownCardList = array( $cards[4], $cards[5] ); + $cards[3]->ownCardList = array( $cards[6], $cards[7], $cards[8] ); + $cards[0]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User0')); + $cards[1]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User1')); + $cards[2]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User2')); + $cards[3]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User3')); + $cards[4]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User4')); + $cards[5]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User5')); + $cards[6]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User6')); + $cards[7]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User7')); + $cards[8]->ownOwner[] = R::dispense(array('_type'=>'owner', 'name'=>'User8')); + $cards[0]->detail = $details[0]; + $cards[1]->detail = $details[0]; + $cards[2]->detail = $details[1]; + $cards[3]->detail = $details[2]; + $cards[4]->detail = $details[3]; + $cards[5]->detail = $details[3]; + $cards[6]->detail = $details[3]; + $cards[7]->detail = $details[3]; + $cards[8]->detail = $details[3]; + $colors[0]->sharedCardList = array( $cards[0], $cards[2], $cards[4], $cards[6], $cards[8] ); + $colors[1]->sharedCardList = array( $cards[1], $cards[3], $cards[5], $cards[7] ); + R::storeAll(array_merge($cards,$details,$colors)); + $cardsWith100Points = R::children($cards[0], ' @joined.detail.points = ? ', array(100)); + asrt(count($cardsWith100Points),5); + $cardsWith200Points = R::children($cards[0], ' @joined.detail.points = ? ', array(200)); + asrt(count($cardsWith200Points),1); + $cardsWith300Points = R::children($cards[0], ' @joined.detail.points = ? ', array(300)); + asrt(count($cardsWith200Points),1); + $cardsWith500Points = R::children($cards[0], ' @joined.detail.points = ? ', array(500)); + asrt(count($cardsWith200Points),1); + for($i=8; $i>=4; $i--) { + $cardsWithMoreThan100Points = R::parents($cards[$i], ' @joined.detail.points > ? ', array(100)); + asrt(count($cardsWithMoreThan100Points),3); + } + $cardsWithMoreThan200Points = R::parents($cards[8], ' @joined.detail.points > ? ', array(200)); + asrt(count($cardsWithMoreThan200Points),3); + $cardsWithMoreThan200Points = R::parents($cards[7], ' @joined.detail.points > ? ', array(200)); + asrt(count($cardsWithMoreThan200Points),3); + $cardsWithMoreThan200Points = R::parents($cards[6], ' @joined.detail.points > ? ', array(200)); + asrt(count($cardsWithMoreThan200Points),3); + $cardsWithMoreThan200Points = R::parents($cards[5], ' @joined.detail.points > ? ', array(200)); + asrt(count($cardsWithMoreThan200Points),2); + $cardsWithMoreThan200Points = R::parents($cards[4], ' @joined.detail.points > ? ', array(200)); + asrt(count($cardsWithMoreThan200Points),2); + for($i=8; $i>=2; $i--) { + $cardsWithMoreThan300Points = R::parents($cards[4], ' @joined.detail.points > ? ', array(300)); + asrt(count($cardsWithMoreThan200Points),2); + } + $black = R::children($cards[0], ' @shared.color.name = ? ', array('black')); + asrt(count($black),4); + $red = R::children($cards[0], ' @shared.color.name = ? ', array('red')); + asrt(count($red),5); + $black = R::parents($cards[8], ' @shared.color.name = ? ', array('black')); + asrt(count($black),2); + $red = R::parents($cards[6], ' @shared.color.name = ? ', array('red')); + asrt(count($red),2); + $found = R::children($cards[0], ' @own.owner.name = ? ', array('User0')); + asrt(count($found),1); + $found = R::children($cards[0], ' @own.owner.name IN (?,?) ', array('User1','User2')); + asrt(count($found),2); + $found = R::parents($cards[8], ' @own.owner.name IN (?,?) ', array('User3','User0')); + asrt(count($found),2); + $found = R::children($cards[8], ' @own.owner.name IN (?,?) ', array('User3','User0')); + asrt(count($found),0); + $found = R::children($cards[3], ' @own.owner.name IN (?,?) ', array('User3','User7')); + asrt(count($found),2); + } + + /** + * Test CTE and Parsed Joins with aliases as well + * as chained parsed joins. + * + * @return void + */ + public function testCTETreesAndParsedJoinsAndAliases() { + R::nuke(); + R::aliases(array( 'author' => 'person', 'chart' => 'image' )); + $person = R::dispense( 'person' ); + $ceo = R::dispense('person'); + $ceo->name = 'John'; + $role = R::dispense('role'); + $role->label = 'CEO'; + $ceo->sharedRoleList = array( $role ); + $person->name = 'James'; + $editor = R::dispense('role'); + $editor->label = 'editor'; + $person->sharedRoleList = array( $editor ); + $website = R::dispense('page'); + $about = R::dispense('page'); + $investor = R::dispense('page'); + $links = R::dispense('page'); + $category = R::dispense('category'); + $category->title = 'business'; + $about->sharedCategoryList = array( $category ); + $blog = R::dispense('page'); + $article = R::dispense('page'); + $investor->title = 'investors'; + $links->title = 'links'; + $about->title = 'about'; + $chart = R::dispense('image'); + $chart->file = 'report.jpg'; + $picture = R::dispense('image'); + $picture->file = 'logo.jpg'; + $picture->source = $website; + $website->ownImageList = array( $picture ); + $article->ownImageList = array( $picture ); + $investor->ownImageList = array( $picture ); + $about->ownImageList = array( $chart ); + $about->author = $ceo; + $about->coauthor = $person; + $article->title = 'a walk in the park'; + $website->ownPageList = array( $about, $blog ); + $blog->ownPageList = array( $article ); + $article->ownPageList = array( $links ); + $about->ownPageList = array( $investor ); + $article->author = $person; + R::store( $website ); + //joined-alias + $pages = R::children( $website, ' @joined.author.name = ? ', array('James') ); + asrt(count($pages), 1); + $page = reset($pages); + asrt($page->title, 'a walk in the park'); + //Chained joined-alias+shared + $pages = R::children( $website, ' @joined.author.shared.role.label = ? ', array('CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //joined-alias+shared parents + $pages = R::parents( $investor, ' @joined.author.shared.role.label = ? ', array('CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //joined-alias parents + $pages = R::parents( $links, ' @joined.author.name = ? ', array('James') ); + asrt(count($pages), 1); + $page = reset($pages); + asrt($page->title, 'a walk in the park'); + //own-alias + $pages = R::children( $website, '@own.chart.file = ?', array('report.jpg') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //own-alias parent + $pages = R::parents( $investor, '@own.chart.file = ?', array('report.jpg') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //own-alias parent + joined-alias+shared parents + $pages = R::parents( $investor, '@own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('report.jpg', 'CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //own-alias + Chained joined-alias+shared + $pages = R::children( $website, ' @own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('report.jpg', 'CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //own-alias + Chained joined-alias+shared + joined-alias + $pages = R::children( $website, ' @joined.author.name = ? AND @own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('John', 'report.jpg', 'CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //own-alias + Chained joined-alias+shared + joined-alias parents + $pages = R::parents( $investor, ' @joined.author.name = ? AND @own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('John', 'report.jpg', 'CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + $pages = R::parents( $links, ' @joined.author.name = ? AND @own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('James', 'logo.jpg', 'editor') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'a walk in the park'); + $pages = R::parents( $links, ' @joined.author.name = ? AND @own.chart.file = ? AND @joined.author.shared.role.label = ? ', array('James', 'report.jpg', 'editor') ); + asrt(count($pages),0); + //other variations + $pages = R::parents( $investor, ' @shared.category.title = ? ', array('business') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + R::aliases(array()); + //joined-alias - explicit/non-global + $pages = R::children( $website, ' @joined.person[as:author].name = ? ', array('James') ); + asrt(count($pages), 1); + $page = reset($pages); + asrt($page->title, 'a walk in the park'); + //Chained joined-alias+shared - explicit/non-global + $pages = R::children( $website, ' @joined.person[as:author].shared.role.label = ? ', array('CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //joined-alias+shared parents - explicit/non-global + $pages = R::parents( $investor, ' @joined.person[as:author].shared.role.label = ? ', array('CEO') ); + asrt(count($pages),1); + $page = reset($pages); + asrt($page->title,'about'); + //joined-alias parents - explicit/non-global + $pages = R::parents( $links, ' @joined.person[as:author].name = ? ', array('James') ); + asrt(count($pages), 1); + $page = reset($pages); + asrt($page->title, 'a walk in the park'); + //double joined-alias parents - explicit/non-global + $pages = R::children( $website, ' @joined.person[as:author/coauthor].name = ? ', array('James') ); + asrt(count($pages), 2); + //own-alias - explicit/non-global + $pages = R::children( $website, ' @own.image[alias:source].file = ?', array('logo.jpg') ); + asrt(count($pages),1); + $pages = R::children( $website, ' @own.image[alias:source].file = ?', array('report.jpg') ); + asrt(count($pages),0); + $pages = R::children( $about, ' @own.image[alias:source].file = ?', array('logo.jpg') ); + asrt(count($pages),0); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Uuid.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Uuid.php new file mode 100644 index 0000000..a842dec --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Uuid.php @@ -0,0 +1,278 @@ +id, $book1ID ); + asrt( $book1->title, 'book 1' ); + $book2 = R::load( 'book', $book2ID ); + asrt( $book2->id, $book2ID ); + asrt( $book2->title, 'book 2' ); + asrt( count( $book1->ownPage ), 2 ); + asrt( count( $book1->fresh()->with( 'LIMIT 1' )->ownPage ), 1 ); + asrt( count( $book1->fresh()->withCondition( ' title = ? ', array('page 2 of book 1'))->ownPage ), 1 ); + asrt( count($book2->ownPage), 1 ); + asrt( $book2->fresh()->countOwn( 'page' ), 1 ); + $page1 = R::load( 'page', $page1ID ); + asrt( count( $page1->sharedPage ), 0 ); + asrt( $page1->fetchAs( 'book' )->magazine->id, $book2ID ); + $page2 = R::load( 'page', $page2ID ); + asrt( count($page2->sharedPage), 1 ); + asrt( $page2->fresh()->countShared( 'page' ), 1 ); + $page3 = R::findOne( 'page', ' title = ? ', array( 'page 1 of book 2' ) ); + asrt( $page3->id, $page3ID ); + asrt( $page3->book->id, $book2ID ); + } + + /** + * Test Full fluid UUID support. + * + */ + public function testFullSupport() + { + + //Rewire objects to support UUIDs. + $oldToolBox = R::getToolBox(); + $oldAdapter = $oldToolBox->getDatabaseAdapter(); + $uuidWriter = new \UUIDWriterPostgres( $oldAdapter ); + $newRedBean = new OODB( $uuidWriter ); + $newToolBox = new ToolBox( $newRedBean, $oldAdapter, $uuidWriter ); + R::configureFacadeWithToolbox( $newToolBox ); + list( $mansion, $rooms, $ghosts, $key ) = R::dispenseAll( 'mansion,room*3,ghost*4,key' ); + $mansion->name = 'Haunted Mansion'; + $mansion->xownRoomList = $rooms; + $rooms[0]->name = 'Green Room'; + $rooms[1]->name = 'Red Room'; + $rooms[2]->name = 'Blue Room'; + $ghosts[0]->name = 'zero'; + $ghosts[1]->name = 'one'; + $ghosts[2]->name = 'two'; + $ghosts[3]->name = 'three'; + $rooms[0]->noLoad()->sharedGhostList = array( $ghosts[0], $ghosts[1] ); + $rooms[1]->noLoad()->sharedGhostList = array( $ghosts[0], $ghosts[2] ); + $rooms[2]->noLoad()->sharedGhostList = array( $ghosts[1], $ghosts[3], $ghosts[2] ); + $rooms[2]->xownKey = array( $key ); + //Can we store a bean hierachy with UUIDs? + $id = R::store( $mansion ); + asrt( is_string( $id ), TRUE ); + asrt( strlen( $id ), 36 ); + $haunted = R::load( 'mansion', $id ); + asrt( $haunted->name, 'Haunted Mansion' ); + asrt( is_string( $haunted->id ), TRUE ); + asrt( strlen( $haunted->id ), 36 ); + asrt( is_array( $haunted->xownRoomList ), TRUE ); + asrt( count( $haunted->ownRoom ), 3 ); + $rooms = $haunted->xownRoomList; + //Do some counting... + $greenRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Green Room' ) { + $greenRoom = $room; + break; + } + } + asrt( !is_null( $greenRoom ), TRUE ); + asrt( is_array( $greenRoom->with(' ORDER BY id ')->sharedGhostList ), TRUE ); + asrt( count( $greenRoom->sharedGhostList ), 2 ); + $names = array(); + foreach( $greenRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'one,zero'); + $rooms = $haunted->xownRoomList; + $blueRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Blue Room' ) { + $blueRoom = $room; + break; + } + } + asrt( !is_null( $blueRoom ), TRUE ); + asrt( is_array( $blueRoom->sharedGhostList ), TRUE ); + asrt( count( $blueRoom->sharedGhostList ), 3 ); + $names = array(); + foreach( $blueRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'one,three,two'); + $rooms = $haunted->xownRoomList; + $redRoom = NULL; + foreach( $rooms as $room ) { + if ( $room->name === 'Red Room' ) { + $redRoom = $room; break; + } + } + $names = array(); + foreach( $redRoom->sharedGhost as $ghost ) $names[] = $ghost->name; + sort($names); + $names = implode(',', $names); + asrt($names, 'two,zero'); + asrt( !is_null( $redRoom ), TRUE ); + asrt( is_array( $redRoom->sharedGhostList ), TRUE ); + asrt( count( $redRoom->sharedGhostList ), 2 ); + //Can we repaint a room? + $redRoom->name = 'Yellow Room'; + $id = R::store($redRoom); + $yellowRoom = R::load( 'room', $id ); + asrt( $yellowRoom->name, 'Yellow Room'); + asrt( !is_null( $yellowRoom ), TRUE ); + asrt( is_array( $yellowRoom->sharedGhostList ), TRUE ); + asrt( count( $yellowRoom->sharedGhostList ), 2 ); + //Can we throw one ghost out? + array_pop( $yellowRoom->sharedGhost ); + R::store( $yellowRoom ); + $yellowRoom = $yellowRoom->fresh(); + asrt( $yellowRoom->name, 'Yellow Room'); + asrt( !is_null( $yellowRoom ), TRUE ); + asrt( is_array( $yellowRoom->sharedGhostList ), TRUE ); + asrt( count( $yellowRoom->sharedGhostList ), 1 ); + //can we remove one of the rooms? + asrt( R::count('key'), 1); + $list = $mansion->withCondition(' "name" = ? ', array('Blue Room'))->xownRoomList; + $room = reset($list); + unset($mansion->xownRoomList[$room->id]); + R::store($mansion); + asrt(R::count('room'), 2); + //and what about its dependent beans? + asrt(R::count('key'), 0); + asrt(R::count('ghost_room'), 3); + //and can we find ghosts? + $ghosts = R::find('ghost'); + asrt(count($ghosts), 4); + $ghosts = R::findAll('ghost', 'ORDER BY id'); + asrt(count($ghosts), 4); + $ghosts = R::findAll('ghost', 'ORDER BY id LIMIT 2'); + asrt(count($ghosts), 2); + $ghostZero = R::findOne('ghost', ' "name" = ? ', array( 'zero' ) ); + asrt( ($ghostZero instanceof OODBBean), TRUE ); + //can we create link properties on existing tables? + $blackRoom = R::dispense( 'room' ); + $blackRoom->name = 'Black Room'; + $ghostZero->link('ghost_room', array('mood'=>'grumpy'))->room = $blackRoom; + R::store($ghostZero); + $ghostZero = $ghostZero->fresh(); + $list = $ghostZero->sharedRoomList; + asrt(count($list), 3); + $ghostZero = $ghostZero->fresh(); + $list = $ghostZero->withCondition(' ghost_room.mood = ? ', array('grumpy'))->sharedRoomList; + asrt(count($list), 1); + //can we load a batch? + $ids = R::getCol('SELECT id FROM ghost'); + $ghosts = R::batch('ghost', $ids); + asrt(count($ghosts), 4); + //can we do an aggregation? + $ghosts = $greenRoom->aggr('ownGhostRoom', 'ghost', 'ghost'); + asrt(count($ghosts), 2); + //can we duplicate the mansion? + asrt(R::count('mansion'), 1); + asrt(R::count('room'), 3); + asrt(R::count('ghost'), 4); + $copy = R::dup($mansion); + R::store($copy); + asrt(R::count('mansion'), 2); + asrt(R::count('room'), 5); //black room does not belong to mansion 1 + asrt(R::count('ghost'), 4); + //can we do some counting using the list? + asrt( $copy->countOwn('room'), 2); + $rooms = $copy->withCondition(' "name" = ? ', array('Green Room'))->xownRoomList; + $room = reset($rooms); + asrt($room->countShared('ghost'), 2); + //Finally restore old toolbox + R::configureFacadeWithToolbox( $oldToolBox ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Writer.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Writer.php new file mode 100644 index 0000000..861bf22 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Postgres/Writer.php @@ -0,0 +1,710 @@ +queryRecordCount( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + $count2 = $writer->queryRecordCount( 'book', array( ), ' id = :id ', array( ':id' => $id ) ); + $count3 = $writer->queryRecordCount( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + $count4 = $writer->queryRecordCount( 'book', array( 'id' => $id ) ); + asrt( $count1, $count2 ); + asrt( $count2, $count3 ); + asrt( $count3, $count4 ); + R::nuke(); + $books = R::dispenseAll( 'book*4' ); + $ids = R::storeAll( $books[0] ); + $writer->deleteRecord( 'book', array( 'id' => $ids[0] ) ); + $writer->deleteRecord( 'book', array( 'id' => $ids[1] ), ' id = :id ', array( ':id' => $ids[1] ) ); + $writer->deleteRecord( 'book', NULL, ' id = :id ', array( ':id' => $ids[2] ) ); + $writer->deleteRecord( 'book', array(), ' id = :id ', array( ':id' => $ids[3] ) ); + asrt( R::count( 'book' ), 0 ); + R::nuke(); + $id = R::store( R::dispense( 'book' ) ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array(), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + } + + /** + * Tests wheter we can write a deletion query + * for PostgreSQL using NO conditions but only an + * additional SQL snippet. + * + * @return void + */ + public function testWriteDeleteQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof PostgreSQL ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + $id = R::store( $bean ); + asrt( R::count( 'bean' ), 1 ); + $queryWriter->deleteRecord( 'bean', array(), $addSql = ' id = :id ', $bindings = array( ':id' => $id ) ); + asrt( R::count( 'bean' ), 0 ); + } + + /** + * Tests wheter we can write a counting query + * for PostgreSQL using conditions and an additional SQL snippet. + * + * @return void + */ + public function testWriteCountQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof PostgreSQL ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $count = $queryWriter->queryRecordCount( 'bean', array( 'name' => 'b' ), $addSql = ' id > :id ', $bindings = array( ':id' => 0 ) ); + asrt( $count, 2 ); + } + + /** + * Tests whether we can write a PostgreSQL join and + * whether the correct exception is thrown in case + * of an invalid join. + * + * @return void + */ + public function testWriteJoinSnippets() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof PostgreSQL ), TRUE ); + $snippet = $queryWriter->writeJoin( 'book', 'page' ); //default must be LEFT + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN "page" ON "page".id = "book".page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'LEFT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN "page" ON "page".id = "book".page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'RIGHT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' RIGHT JOIN "page" ON "page".id = "book".page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'INNER' ); + asrt( ' INNER JOIN "page" ON "page".id = "book".page_id ', $snippet ); + $exception = NULL; + try { + $snippet = $queryWriter->writeJoin( 'book', 'page', 'MIDDLE' ); + } + catch(\Exception $e) { + $exception = $e; + } + asrt( ( $exception instanceof RedException ), TRUE ); + $errorMessage = $exception->getMessage(); + asrt( is_string( $errorMessage ), TRUE ); + asrt( ( strlen( $errorMessage ) > 0 ), TRUE ); + asrt( $errorMessage, 'Invalid JOIN.' ); + } + + /** + * Test whether we can store JSON as a JSON column + * and whether this plays well with the other data types. + */ + public function testSetGetJSON() + { + global $travis; + if ($travis) return; + R::nuke(); + $bean = R::dispense('bean'); + $message = json_encode( array( 'message' => 'hello', 'type' => 'greeting' ) ) ; + $bean->data = $message; + R::store( $bean ); + $columns = R::inspect('bean'); + asrt( array_key_exists( 'data', $columns ), TRUE ); + asrt( ( $columns['data'] !== 'json' ), TRUE ); + R::useJSONFeatures( TRUE ); + R::nuke(); + $bean = R::dispense('bean'); + $message = array( 'message' => 'hello', 'type' => 'greeting' ); + $bean->data = $message; + R::store( $bean ); + $columns = R::inspect('bean'); + asrt( array_key_exists( 'data', $columns ), TRUE ); + asrt( $columns['data'], 'json' ); + $bean = $bean->fresh(); + $message = json_decode( $bean->data, TRUE ); + asrt( $message['message'], 'hello' ); + asrt( $message['type'], 'greeting' ); + $message['message'] = 'hi'; + $bean->data = $message; + R::store( $bean ); + pass(); + $bean = R::findOne( 'bean' ); + $message = json_decode( $bean->data ); + asrt( $message->message, 'hi' ); + $book = R::dispense( 'book' ); + $book->page = 'lorem ipsum'; + R::store( $book ); + $book = $book->fresh(); + asrt( $book->page, 'lorem ipsum' ); + $book2 = R::dispense( 'book' ); + $book2->page = array( 'chapter' => '1' ); + R::store( $book2 ); + pass(); //should not try to modify column and trigger exception + $book = $book->fresh(); + asrt( $book->page, 'lorem ipsum' ); + $columns = R::inspect('book'); + asrt( ( $columns['page'] !== 'json' ), TRUE ); + $building = R::dispense( 'building' ); + $building->year = 'MLXXVIII'; + R::store( $building ); + $shop = R::dispense( 'building' ); + $shop->year = '2010-01-01'; + R::store( $shop ); + $building = R::load( 'building', $building->id ); + asrt( $building->year, 'MLXXVIII' ); + $columns = R::inspect( 'building' ); + asrt( strpos( strtolower( $columns['year'] ), 'date' ), FALSE ); + $shop->anno = '2010-01-01'; + R::store( $shop ); + $columns = R::inspect( 'building' ); + asrt( $columns['anno'], 'date' ); + R::useJSONFeatures( FALSE ); + } + + /** + * Test scanning and coding. + * + * @return void + */ + public function testScanningAndCoding() + { + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + $adapter->exec( "DROP TABLE IF EXISTS testtable" ); + asrt( in_array( "testtable", $writer->getTables() ), FALSE ); + $writer->createTable( "testtable" ); + asrt( in_array( "testtable", $writer->getTables() ), TRUE ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 1 ); + asrt( in_array( "id", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), FALSE ); + $writer->addColumn( "testtable", "c1", 1 ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 2 ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + foreach ( $writer->sqltype_typeno as $key => $type ) { + if ( $type < 100 ) { + asrt( $writer->code( $key, TRUE ), $type ); + } else { + asrt( $writer->code( $key ), PostgreSQL::C_DATATYPE_SPECIFIED ); + } + } + asrt( $writer->code( PostgreSQL::C_DATATYPE_SPECIAL_DATETIME ), PostgreSQL::C_DATATYPE_SPECIFIED ); + asrt( $writer->code( "unknown" ), PostgreSQL::C_DATATYPE_SPECIFIED ); + asrt( $writer->scanType( FALSE ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( TRUE ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( NULL ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 2 ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 255 ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 256 ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( -1 ), PostgreSQL::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 1.5 ), PostgreSQL::C_DATATYPE_DOUBLE ); + asrt( $writer->scanType( INF ), PostgreSQL::C_DATATYPE_TEXT ); + asrt( $writer->scanType( "abc" ), PostgreSQL::C_DATATYPE_TEXT ); + asrt( $writer->scanType( "2001-10-10", TRUE ), PostgreSQL::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->scanType( "2001-10-10 10:00:00", TRUE ), PostgreSQL::C_DATATYPE_SPECIAL_DATETIME ); + asrt( $writer->scanType( "2001-10-10 10:00:00" ), PostgreSQL::C_DATATYPE_TEXT ); + asrt( $writer->scanType( "2001-10-10" ), PostgreSQL::C_DATATYPE_TEXT ); + asrt( $writer->scanType( str_repeat( "lorem ipsum", 100 ) ), PostgreSQL::C_DATATYPE_TEXT ); + $writer->widenColumn( "testtable", "c1", PostgreSQL::C_DATATYPE_TEXT ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), PostgreSQL::C_DATATYPE_TEXT ); + $writer->addColumn( "testtable", "special", PostgreSQL::C_DATATYPE_SPECIAL_DATE ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special'], TRUE ), PostgreSQL::C_DATATYPE_SPECIAL_DATE ); + asrt( $writer->code( $cols['special'], FALSE ), PostgreSQL::C_DATATYPE_SPECIFIED ); + $writer->addColumn( "testtable", "special2", PostgreSQL::C_DATATYPE_SPECIAL_DATETIME ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols['special2'], TRUE ), PostgreSQL::C_DATATYPE_SPECIAL_DATETIME ); + asrt( $writer->code( $cols['special'], FALSE ), PostgreSQL::C_DATATYPE_SPECIFIED ); + $id = $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "lorem ipsum" ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "lorem ipsum" ); + $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "ipsum lorem" ) ), $id ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "ipsum lorem" ); + $writer->deleteRecord( "testtable", array( "id" => array( $id ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( empty( $row ), TRUE ); + } + + /** + * (FALSE should be stored as 0 not as '') + * + * @return void + */ + public function testZeroIssue() + { + testpack( "Zero issue" ); + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $bean = $redbean->dispense( "zero" ); + $bean->zero = FALSE; + $bean->title = "bla"; + $redbean->store( $bean ); + asrt( count( $redbean->find( "zero", array(), " zero = 0 " ) ), 1 ); + testpack( "Test ANSI92 issue in clearrelations" ); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + set1toNAssoc( $a, $book, $author1 ); + set1toNAssoc( $a, $book, $author2 ); + pass(); + } + + /** + * Various. + * Tests whether writer correctly handles keyword 'group' and SQL state 23000 issue. + * These tests remain here to make sure issues 9 and 10 never happen again. + * However this bug will probably never re-appear due to changed architecture. + * + * @return void + */ + public function testIssue9and10() + { + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $adapter = $toolbox->getDatabaseAdapter(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->associate( $book, $author1 ); + $a->associate( $book, $author2 ); + pass(); + testpack( "Test Association Issue Group keyword (Issues 9 and 10)" ); + R::nuke(); + $group = $redbean->dispense( "group" ); + $group->name = "mygroup"; + $redbean->store( $group ); + try { + $a->associate( $group, $book ); + pass(); + } catch ( SQL $e ) { + fail(); + } + // Test issue SQL error 23000 + try { + $a->associate( $group, $book ); + + pass(); + } catch ( SQL $e ) { + fail(); + } + asrt( (int) $adapter->getCell( "select count(*) from book_group" ), 1 ); //just 1 rec! + } + + /** + * Test various. + * Test various somewhat uncommon trash/unassociate scenarios. + * (i.e. unassociate unrelated beans, trash non-persistant beans etc). + * Should be handled gracefully - no output checking. + * + * @return void + */ + public function testVaria() + { + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->unassociate( $book, $author1 ); + $a->unassociate( $book, $author2 ); + pass(); + $redbean->trash( $redbean->dispense( "bla" ) ); + pass(); + $bean = $redbean->dispense( "bla" ); + $bean->name = 1; + $bean->id = 2; + $redbean->trash( $bean ); + pass(); + } + + /** + * Test special types. + * + * @return void + */ + public function testTypes() + { + testpack( 'Special data types' ); + $bean = R::dispense( 'bean' ); + $bean->date = 'someday'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'text' ); + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'text' ); + } + + /** + * Test dates. + * + * @return void + */ + public function testTypesDates() + { + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'date' ); + } + + /** + * Datetime. + * + * @return void + */ + public function testTypesDateTimes() + { + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10 10:00:00'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'timestamp without time zone' ); + } + + /** + * Test spatial data types. + * + * @return void + */ + public function testTypesPoints() + { + $bean = R::dispense( 'bean' ); + $bean->point = '(92,12)'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['point'], 'point' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->point, '(92,12)' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->point, '(92,12)' ); + } + + /** + * Test points. + * + * @return void + */ + public function testTypesDecPoints() + { + $bean = R::dispense( 'bean' ); + $bean->point = '(9.2,1.2)'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['point'], 'point' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->point, '(9.2,1.2)' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->point, '(9.2,1.2)' ); + } + + /** + * Test polygons. + * + * @return void + */ + public function testPolygons() + { + $bean = R::dispense( 'bean' ); + $bean->polygon = '((0,0),(1,1),(2,0))'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['polygon'], 'polygon' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->polygon, '((0,0),(1,1),(2,0))' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->polygon, '((0,0),(1,1),(2,0))' ); + $bean = R::dispense( 'bean' ); + $bean->polygon = '((0,0),(1.2,1),(2,0.3))'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['polygon'], 'polygon' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->polygon, '((0,0),(1.2,1),(2,0.3))' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->polygon, '((0,0),(1.2,1),(2,0.3))' ); + } + + /** + * Test multi points. + * + * @return void + */ + public function testTypesMultiDecPoints() + { + $bean = R::dispense( 'bean' ); + $bean->line = '[(1.2,1.4),(2.2,34)]'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['line'], 'lseg' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->line, '[(1.2,1.4),(2.2,34)]' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->line, '[(1.2,1.4),(2.2,34)]' ); + } + + /** + * More points... + * + * @return void + */ + public function testTypesWeirdPoints() + { + $bean = R::dispense( 'bean' ); + $bean->circle = '<(9.2,1.2),7.9>'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['circle'], 'circle' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->circle, '<(9.2,1.2),7.9>' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->circle, '<(9.2,1.2),7.9>' ); + } + + /** + * Test money types. + * + * @return void + */ + public function testTypesMon() + { + $bean = R::dispense( 'bean' ); + $bean->amount = '22.99'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['amount'], 'numeric' ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->amount = '-22.99'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['amount'], 'numeric' ); + } + + /** + * Test money data type. + * + * @return void + */ + public function testTypesMoney() + { + $bean = R::dispense( 'bean' ); + $bean->money = '$123.45'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['money'], 'money' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->money, '$123.45' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->money, '$123.45' ); + $bean->money = '$123,455.01'; + R::store($bean); + $bean = $bean->fresh(); + asrt( $bean->money, '$123,455.01' ); + } + + /** + * Test negative money data type. + * + * @return void + */ + public function testTypesNegativeMoney() + { + $bean = R::dispense( 'bean' ); + $bean->money = '-$123.45'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['money'], 'money' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->money, '-$123.45' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->money, '-$123.45' ); + } + + /** + * Issue #340 + * Redbean is currently picking up bcrypt hashed passwords + * (which look like this: $2y$12$85lAS....SnpDNVGPAC7w0G) + * as PostgreSQL money types. + * Then, once R::store is called on the bean, it chokes and throws the following error: + * PHP Fatal error: Uncaught [22P02] - SQLSTATE[22P02]: Invalid text representation: 7 ERROR: + * invalid input syntax for type money: .... + * + * @return void + */ + public function testTypesInvalidMoney() + { + $bean = R::dispense( 'bean' ); + $bean->nomoney = '$2y$12$85lAS'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['nomoney'], 'text' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->nomoney, '$2y$12$85lAS' ); + $bean->note = 'taint'; + R::store( $bean ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->nomoney, '$2y$12$85lAS' ); + } + + /** + * Test types of strings. + * + * @return void + */ + public function testTypesStrings() + { + $bean = R::dispense( 'bean' ); + $bean->data = 'abcdefghijk'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + $bean = R::load( 'bean', $bean->id ); + asrt( $bean->data, 'abcdefghijk' ); + $bean->data = '(1,2)'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + $bean->data = '[(1.2,1.4),(2.2,34)]'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + $bean->data = '<(9.2,1.2),7.9>'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + $bean->data = '$25'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + $bean->data = '2012-10-10 10:00:00'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['data'], 'text' ); + } + + /** + * Can we manually add a Postgres time column without a time zone + * and with a time zone? + * + * @return void + */ + public function testTime() + { + R::nuke(); + $clock = R::dispense('clock'); + $clock->time = '10:00:00'; + $clock->setMeta('cast.time', 'time'); + R::store( $clock ); + $columns = R::inspect('clock'); + asrt( $columns['time'], 'time without time zone' ); + R::nuke(); + $clock = R::dispense('clock'); + $clock->time = '10:00:00 PST'; + $clock->setMeta('cast.time', 'time with time zone'); + R::store( $clock ); + $columns = R::inspect('clock'); + asrt( $columns['time'], 'time with time zone' ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Pretest.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Pretest.php new file mode 100644 index 0000000..ff88f00 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Pretest.php @@ -0,0 +1,32 @@ +title = 'a'; + R::store( $book ); + try { + R::getWriter()->addIndex( 'book' , '\'', 'title' ); + pass(); + } catch( \Exception $e ) { + fail(); + } + R::getWriter()->addIndex( 'book' , '\'', 'title' ); + pass(); + } + + /** + * Test foreign keys with SQLite. + * + * @return void + */ + public function testForeignKeysWithSQLite() + { + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $cover = R::dispense( 'cover' ); + list( $g1, $g2 ) = R::dispense( 'genre', 2 ); + $g1->name = '1'; + $g2->name = '2'; + $book->ownPage = array( $page ); + $book->cover = $cover; + $book->sharedGenre = array( $g1, $g2 ); + R::store( $book ); + $fkbook = R::getAll( 'pragma foreign_key_list(book)' ); + $fkgenre = R::getAll( 'pragma foreign_key_list(book_genre)' ); + $fkpage = R::getAll( 'pragma foreign_key_list(page)' ); + asrt( $fkpage[0]['from'], 'book_id' ); + asrt( $fkpage[0]['to'], 'id' ); + asrt( $fkpage[0]['table'], 'book' ); + asrt( count( $fkgenre ), 2 ); + if ( $fkgenre[0]['from'] == 'book' ) { + asrt( $fkgenre[0]['to'], 'id' ); + asrt( $fkgenre[0]['table'], 'book' ); + } + if ( $fkgenre[0]['from'] == 'genre' ) { + asrt( $fkgenre[0]['to'], 'id' ); + asrt( $fkgenre[0]['table'], 'genre' ); + } + asrt( $fkbook[0]['from'], 'cover_id' ); + asrt( $fkbook[0]['to'], 'id' ); + asrt( $fkbook[0]['table'], 'cover' ); + } + + /** + * Constrain test for SQLite Writer. + * + * @return void + */ + public function testConstrain() + { + R::nuke(); + $sql = 'CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT ) '; + R::exec( $sql ); + $sql = 'CREATE TABLE page ( id INTEGER PRIMARY KEY AUTOINCREMENT ) '; + R::exec( $sql ); + $sql = 'CREATE TABLE book_page ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id INTEGER, + page_id INTEGER + ) '; + R::exec( $sql ); + $sql = 'PRAGMA foreign_key_list("book_page")'; + $fkList = R::getAll( $sql ); + asrt( count( $fkList), 0 ); + $writer = R::getWriter(); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $sql = 'PRAGMA foreign_key_list("book_page")'; + $fkList = R::getAll( $sql ); + asrt( count( $fkList), 2 ); + $writer->addFK( 'book_page', 'book', 'book_id', 'id', TRUE ); + $writer->addFK( 'book_page', 'page', 'page_id', 'id', TRUE ); + $sql = 'PRAGMA foreign_key_list("book_page")'; + $fkList = R::getAll( $sql ); + asrt( count( $fkList), 2 ); + } + + /** + * Test adding foreign keys. + * + * @return void + */ + public function testAddingForeignKeys() + { + R::nuke(); + + $sql = 'CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT ) '; + R::exec( $sql ); + $sql = 'CREATE TABLE page ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER ) '; + R::exec( $sql ); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 0 ); + $writer = R::getWriter(); + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + $writer->addFK('page', 'book', 'book_id', 'id', FALSE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + R::nuke(); + $sql = 'CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT ) '; + R::exec( $sql ); + $sql = 'CREATE TABLE page ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER ) '; + R::exec( $sql ); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 0 ); + $writer = R::getWriter(); + $writer->addFK('page', 'book', 'book_id', 'id', FALSE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + $writer->addFK('page', 'book', 'book_id', 'id', TRUE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + $writer->addFK('page', 'book', 'book_id', 'id', FALSE); + asrt( count( R::getAll(' PRAGMA foreign_key_list("page") ') ), 1 ); + } + + /** + * Test whether we can manually create indexes. + * + * @return void + */ + public function testAddingIndex() + { + R::nuke(); + $sql = 'CREATE TABLE song ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + album_id INTEGER, + category TEXT + ) '; + R::exec( $sql ); + $writer = R::getWriter(); + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 0 ); + $writer->addIndex( 'song', 'index1', 'album_id' ); + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 1 ); + $writer->addIndex( 'song', 'index1', 'album_id' ); + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 1 ); + $writer->addIndex( 'song', 'index2', 'category' ); + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 2 ); + try { + $writer->addIndex( 'song', 'index1', 'nonexistant' ); + pass(); + } catch ( \Exception $ex ) { + fail(); + } + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 2 ); + try { + $writer->addIndex( 'nonexistant', 'index1', 'nonexistant' ); + pass(); + } catch ( \Exception $ex ) { + fail(); + } + $indexes = R::getAll('PRAGMA index_list("song") '); + asrt( count( $indexes ), 2 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Parambind.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Parambind.php new file mode 100644 index 0000000..b3013da --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Parambind.php @@ -0,0 +1,60 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + asrt( (int) $adapter->getCell( "SELECT 123" ), 123 ); + asrt( (int) $adapter->getCell( "SELECT ?", array( "987" ) ), 987 ); + asrt( (int) $adapter->getCell( "SELECT ?+?", array( "987", "2" ) ), 989 ); + asrt( (int) $adapter->getCell( + "SELECT :numberOne+:numberTwo", + array( + ":numberOne" => 42, + ":numberTwo" => 50 ) + ), + 92 + ); + $pair = $adapter->getAssoc( "SELECT 'thekey','thevalue' " ); + asrt( is_array( $pair ), TRUE ); + asrt( count( $pair ), 1 ); + asrt( isset( $pair["thekey"] ), TRUE ); + asrt( $pair["thekey"], "thevalue" ); + testpack( 'Test whether we can properly bind and receive NULL values' ); + asrt( $adapter->getCell( 'SELECT :nil ', array( ':nil' => 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT :nil ', array( ':nil' => NULL ) ), NULL ); + asrt( $adapter->getCell( 'SELECT ? ', array( 'NULL' ) ), 'NULL' ); + asrt( $adapter->getCell( 'SELECT ? ', array( NULL ) ), NULL ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Rebuild.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Rebuild.php new file mode 100644 index 0000000..81ad405 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Rebuild.php @@ -0,0 +1,63 @@ +getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->xownPage[] = $page; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->xownPage ), 1 ); + asrt( (int) R::getCell( 'SELECT COUNT(*) FROM page' ), 1 ); + R::trash( $book ); + asrt( (int) R::getCell( 'SELECT COUNT(*) FROM page' ), 0 ); + $book = R::dispense( 'book' ); + $page = R::dispense( 'page' ); + $book->xownPage[] = $page; + $id = R::store( $book ); + $book = R::load( 'book', $id ); + asrt( count( $book->xownPage ), 1 ); + asrt( (int) R::getCell( 'SELECT COUNT(*) FROM page' ), 1 ); + $book->added = 2; + R::store( $book ); + $book->added = 'added'; + R::store( $book ); + R::trash( $book ); + asrt( (int) R::getCell( 'SELECT COUNT(*) FROM page' ), 0 ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Setget.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Setget.php new file mode 100644 index 0000000..8f6412d --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Setget.php @@ -0,0 +1,134 @@ +setTimeZone( new \DateTimeZone( 'Europe/Amsterdam' ) ); + $dt->setDate( 1981, 5, 1 ); + $dt->setTime( 3, 13, 13 ); + asrt( setget( $dt ), '1981-05-01 03:13:13' ); + $bean = R::dispense( 'bean' ); + $bean->dt = $dt; + } + + /** + * Test numbers. + * + * @return void + */ + public function testNumbers() + { + asrt( setget( "-1" ), "-1" ); + asrt( setget( -1 ), "-1" ); + asrt( setget( "-0.25" ), "-0.25" ); + asrt( setget( -0.25 ), "-0.25" ); + asrt( setget( "1.0" ), "1" ); + asrt( setget( 1.0 ), "1" ); + asrt( setget( "0.12345678" ), "0.12345678" ); + asrt( setget( 0.12345678 ), "0.12345678" ); + asrt( setget( "-0.12345678" ), "-0.12345678" ); + asrt( setget( -0.12345678 ), "-0.12345678" ); + asrt( setget( "2147483647" ), "2147483647" ); + asrt( setget( 2147483647 ), "2147483647" ); + asrt( setget( -2147483647 ), "-2147483647" ); + asrt( setget( "-2147483647" ), "-2147483647" ); + asrt( setget( "2147483648" ), "2147483648" ); + asrt( setget( "-2147483648" ), "-2147483648" ); + asrt( setget( "199936710040730" ), "199936710040730" ); + asrt( setget( "-199936710040730" ), "-199936710040730" ); + } + + /** + * Test dates. + * + * @return void + */ + public function testDates() + { + asrt( setget( "2010-10-11" ), "2010-10-11" ); + asrt( setget( "2010-10-11 12:10" ), "2010-10-11 12:10" ); + asrt( setget( "2010-10-11 12:10:11" ), "2010-10-11 12:10:11" ); + asrt( setget( "x2010-10-11 12:10:11" ), "x2010-10-11 12:10:11" ); + } + + /** + * Test strings. + * + * @return void + */ + public function testStrings() + { + asrt( setget( "a" ), "a" ); + asrt( setget( "." ), "." ); + asrt( setget( "\"" ), "\"" ); + asrt( setget( "just some text" ), "just some text" ); + } + + /** + * Test booleans. + * + * @return void + */ + public function testBool() + { + asrt( setget( TRUE ), "1" ); + asrt( setget( FALSE ), "0" ); + asrt( setget( "TRUE" ), "TRUE" ); + asrt( setget( "FALSE" ), "FALSE" ); + } + + /** + * Test NULL. + * + * @return void + */ + public function testNull() + { + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( "NULL" ), "NULL" ); + asrt( setget( NULL ), NULL ); + asrt( ( setget( 0 ) == 0 ), TRUE ); + asrt( ( setget( 1 ) == 1 ), TRUE ); + asrt( ( setget( TRUE ) == TRUE ), TRUE ); + asrt( ( setget( FALSE ) == FALSE ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Writer.php b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Writer.php new file mode 100644 index 0000000..55dd4a4 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/RedUNIT/Sqlite/Writer.php @@ -0,0 +1,406 @@ + 0'); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + asrt( count($logs), 0 ); //if no cache clear, then would have been 2 + $bean = R::loadForUpdate('bean', $id); + $logs = R::getDatabaseAdapter()->getDatabase()->getLogger()->grep( AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + asrt( count($logs), 0 ); //if no cache clear, then would have been 2 + R::getWriter()->setUseCache( FALSE ); + R::debug( FALSE ); + R::nuke(); + } + + /** + * Test whether optimizations do not have effect on Writer query outcomes. + * + * @return void + */ + public function testWriterSpeedUp() + { + R::nuke(); + $id = R::store( R::dispense( 'book' ) ); + $writer = R::getWriter(); + $count1 = $writer->queryRecordCount( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + $count2 = $writer->queryRecordCount( 'book', array( ), ' id = :id ', array( ':id' => $id ) ); + $count3 = $writer->queryRecordCount( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + $count4 = $writer->queryRecordCount( 'book', array( 'id' => $id ) ); + asrt( $count1, $count2 ); + asrt( $count2, $count3 ); + asrt( $count3, $count4 ); + R::nuke(); + $books = R::dispenseAll( 'book*4' ); + $ids = R::storeAll( $books[0] ); + $writer->deleteRecord( 'book', array( 'id' => $ids[0] ) ); + $writer->deleteRecord( 'book', array( 'id' => $ids[1] ), ' id = :id ', array( ':id' => $ids[1] ) ); + $writer->deleteRecord( 'book', NULL, ' id = :id ', array( ':id' => $ids[2] ) ); + $writer->deleteRecord( 'book', array(), ' id = :id ', array( ':id' => $ids[3] ) ); + asrt( R::count( 'book' ), 0 ); + R::nuke(); + $id = R::store( R::dispense( 'book' ) ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array( 'id' => $id ), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', NULL, ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + $record = $writer->queryRecord( 'book', array(), ' id = :id ', array( ':id' => $id ) ); + asrt( is_array( $record ), TRUE ); + asrt( is_array( $record[0] ), TRUE ); + asrt( isset( $record[0]['id'] ), TRUE ); + asrt( (int) $record[0]['id'], $id ); + } + + /** + * Tests wheter we can write a deletion query + * for SQLite using NO conditions but only an + * additional SQL snippet. + * + * @return void + */ + public function testWriteDeleteQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof SQLiteT ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + $id = R::store( $bean ); + asrt( R::count( 'bean' ), 1 ); + $queryWriter->deleteRecord( 'bean', array(), $addSql = ' id = :id ', $bindings = array( ':id' => $id ) ); + asrt( R::count( 'bean' ), 0 ); + } + + /** + * Tests wheter we can write a counting query + * for SQLite using conditions and an additional SQL snippet. + * + * @return void + */ + public function testWriteCountQuery() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof SQLiteT ), TRUE ); + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->name = 'a'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $bean = R::dispense( 'bean' ); + $bean->name = 'b'; + R::store( $bean ); + $count = $queryWriter->queryRecordCount( 'bean', array( 'name' => 'b' ), $addSql = ' id > :id ', $bindings = array( ':id' => 0 ) ); + asrt( $count, 2 ); + } + + /** + * Tests whether we can write a SQLite join and + * whether the correct exception is thrown in case + * of an invalid join. + * + * @return void + */ + public function testWriteJoinSnippets() + { + $queryWriter = R::getWriter(); + asrt( ( $queryWriter instanceof SQLiteT ), TRUE ); + $snippet = $queryWriter->writeJoin( 'book', 'page' ); //default must be LEFT + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'LEFT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' LEFT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'RIGHT' ); + asrt( is_string( $snippet ), TRUE ); + asrt( ( strlen( $snippet ) > 0 ), TRUE ); + asrt( ' RIGHT JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $snippet = $queryWriter->writeJoin( 'book', 'page', 'INNER' ); + asrt( ' INNER JOIN `page` ON `page`.id = `book`.page_id ', $snippet ); + $exception = NULL; + try { + $snippet = $queryWriter->writeJoin( 'book', 'page', 'MIDDLE' ); + } + catch(\Exception $e) { + $exception = $e; + } + asrt( ( $exception instanceof RedException ), TRUE ); + $errorMessage = $exception->getMessage(); + asrt( is_string( $errorMessage ), TRUE ); + asrt( ( strlen( $errorMessage ) > 0 ), TRUE ); + asrt( $errorMessage, 'Invalid JOIN.' ); + } + + /** + * Test scanning and coding. + * + * @return void + */ + public function testScanningAndCoding() + { + $toolbox = R::getToolBox(); + $adapter = $toolbox->getDatabaseAdapter(); + $writer = $toolbox->getWriter(); + $redbean = $toolbox->getRedBean(); + $pdo = $adapter->getDatabase(); + $a = new AssociationManager( $toolbox ); + asrt( in_array( "testtable", $writer->getTables() ), FALSE ); + $writer->createTable( "testtable" ); + asrt( in_array( "testtable", $writer->getTables() ), TRUE ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 1 ); + asrt( in_array( "id", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), FALSE ); + $writer->addColumn( "testtable", "c1", 1 ); + asrt( count( array_keys( $writer->getColumns( "testtable" ) ) ), 2 ); + asrt( in_array( "c1", array_keys( $writer->getColumns( "testtable" ) ) ), TRUE ); + foreach ( $writer->sqltype_typeno as $key => $type ) { + asrt( $writer->code( $key ), $type ); + } + asrt( $writer->code( "unknown" ), 99 ); + asrt( $writer->scanType( FALSE ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( NULL ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 2 ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 255 ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 256 ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( -1 ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 1.5 ), SQLiteT::C_DATATYPE_NUMERIC ); + asrt( $writer->scanType( 2147483648 - 1 ), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( 2147483648 ), SQLiteT::C_DATATYPE_TEXT ); + asrt( $writer->scanType( -2147483648 + 1), SQLiteT::C_DATATYPE_INTEGER ); + asrt( $writer->scanType( -2147483648 ), SQLiteT::C_DATATYPE_TEXT ); + asrt( $writer->scanType( INF ), SQLiteT::C_DATATYPE_TEXT ); + asrt( $writer->scanType( "abc" ), SQLiteT::C_DATATYPE_TEXT ); + asrt( $writer->scanType( '2010-10-10' ), SQLiteT::C_DATATYPE_NUMERIC ); + asrt( $writer->scanType( '2010-10-10 10:00:00' ), SQLiteT::C_DATATYPE_NUMERIC ); + asrt( $writer->scanType( str_repeat( "lorem ipsum", 100 ) ), SQLiteT::C_DATATYPE_TEXT ); + $writer->widenColumn( "testtable", "c1", 2 ); + $cols = $writer->getColumns( "testtable" ); + asrt( $writer->code( $cols["c1"] ), 2 ); + $id = $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "lorem ipsum" ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "lorem ipsum" ); + $writer->updateRecord( "testtable", array( array( "property" => "c1", "value" => "ipsum lorem" ) ), $id ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( $row[0]["c1"], "ipsum lorem" ); + $writer->deleteRecord( "testtable", array( "id" => array( $id ) ) ); + $row = $writer->queryRecord( "testtable", array( "id" => array( $id ) ) ); + asrt( empty( $row ), TRUE ); + } + + /** + * (FALSE should be stored as 0 not as '') + * + * @return void + */ + public function testZeroIssue() + { + testpack( "Zero issue" ); + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $bean = $redbean->dispense( "zero" ); + $bean->zero = FALSE; + $bean->title = "bla"; + $redbean->store( $bean ); + asrt( count( $redbean->find( "zero", array(), " zero = 0 " ) ), 1 ); + testpack( "Test ANSI92 issue in clearrelations" ); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + set1toNAssoc( $a, $book, $author1 ); + set1toNAssoc( $a, $book, $author2 ); + pass(); + } + + /** + * Various. + * Tests whether writer correctly handles keyword 'group' and SQL state 23000 issue. + * These tests remain here to make sure issues 9 and 10 never happen again. + * However this bug will probably never re-appear due to changed architecture. + * + * @return void + */ + public function testIssue9and10() + { + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $adapter = $toolbox->getDatabaseAdapter(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->associate( $book, $author1 ); + $a->associate( $book, $author2 ); + pass(); + testpack( "Test Association Issue Group keyword (Issues 9 and 10)" ); + $group = $redbean->dispense( "group" ); + $group->name = "mygroup"; + $redbean->store( $group ); + try { + $a->associate( $group, $book ); + pass(); + } catch ( SQL $e ) { + fail(); + } + // Test issue SQL error 23000 + try { + $a->associate( $group, $book ); + pass(); + } catch ( SQL $e ) { + print_r( $e ); + fail(); + } + asrt( (int) $adapter->getCell( "select count(*) from book_group" ), 1 ); //just 1 rec! + } + + /** + * Test various. + * Test various somewhat uncommon trash/unassociate scenarios. + * (i.e. unassociate unrelated beans, trash non-persistant beans etc). + * Should be handled gracefully - no output checking. + * + * @return void + */ + public function testVaria2() + { + $toolbox = R::getToolBox(); + $redbean = $toolbox->getRedBean(); + $a = new AssociationManager( $toolbox ); + $book = $redbean->dispense( "book" ); + $author1 = $redbean->dispense( "author" ); + $author2 = $redbean->dispense( "author" ); + $book->title = "My First Post"; + $author1->name = "Derek"; + $author2->name = "Whoever"; + $a->unassociate( $book, $author1 ); + $a->unassociate( $book, $author2 ); + pass(); + $redbean->trash( $redbean->dispense( "bla" ) ); + pass(); + $bean = $redbean->dispense( "bla" ); + $bean->name = 1; + $bean->id = 2; + $redbean->trash( $bean ); + pass(); + } + + /** + * Test special data types. + * + * @return void + */ + public function testSpecialDataTypes() + { + testpack( 'Special data types' ); + $bean = R::dispense( 'bean' ); + $bean->date = 'someday'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'TEXT' ); + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::nuke(); + $bean = R::dispense( 'bean' ); + $bean->date = '2011-10-10'; + R::store( $bean ); + $cols = R::getColumns( 'bean' ); + asrt( $cols['date'], 'NUMERIC' ); + } + + /** + * Test renewed error handling in SQLite. + * In fluid mode ignore table/column not exists (HY000 + code 1). + * In frozen mode ignore nothing. + * + * @return void + */ + public function testErrorHandling() + { + R::nuke(); + R::store( R::dispense( 'book' ) ); + R::freeze( FALSE ); + R::find( 'book2', ' id > 0' ); + pass(); + R::find( 'book', ' id2 > ?' ); + pass(); + $exception = NULL; + try { + R::find( 'book', ' id = ?', array( 0, 1 ) ); + } catch( \Exception $e ) { + $exception = $e; + } + asrt( ( $exception instanceof SQL ), TRUE ); + R::freeze( TRUE ); + $exception = NULL; + try { + R::find( 'book2', ' id > 0' ); + } catch( \Exception $e ) { + $exception = $e; + } + asrt( ( $exception instanceof SQL ), TRUE ); + $exception = NULL; + try { + R::find( 'book', ' id2 > 0' ); + } catch( \Exception $e ) { + $exception = $e; + } + asrt( ( $exception instanceof SQL ), TRUE ); + } +} diff --git a/vendor/gabordemooij/redbean/testing/cli/plugins/myhooks.php b/vendor/gabordemooij/redbean/testing/cli/plugins/myhooks.php new file mode 100644 index 0000000..3869994 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/cli/plugins/myhooks.php @@ -0,0 +1,27 @@ + 'localhost', + 'schema' => 'cubase', + 'user' => 'dba', + 'pass' => '' +); + +$colorMap[ 'CUBRID' ] = '0;35'; + +$dsn = "cubrid:host={$ini['CUBRID']['host']};port=33000;dbname={$ini['CUBRID']['schema']}"; +R::addDatabase( 'CUBRID', $dsn, $ini['CUBRID']['user'], $ini['CUBRID']['pass'], FALSE ); +R::selectDatabase( 'CUBRID' ); +R::exec( 'AUTOCOMMIT IS ON' ); diff --git a/vendor/gabordemooij/redbean/testing/cli/runperf.php b/vendor/gabordemooij/redbean/testing/cli/runperf.php new file mode 100644 index 0000000..d339433 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/cli/runperf.php @@ -0,0 +1,96 @@ +getTargetDrivers(); + +foreach ( $drivers as $driver ) { + + if ( !isset( $ini[$driver] ) ) continue; + if ( !isset( $_SERVER['argv'][1])) die('Missing parameter. Usage: php runperf.php '); + + $method = $_SERVER['argv'][1]; + + if ($method === 'setup') { + + echo 'Setup...'.PHP_EOL; + + $test->$method(); + + echo 'READY'.PHP_EOL; + + } else { + + $times = 100; + if (isset($_SERVER['argv'][2])) { + $times = (int) $_SERVER['argv'][2]; + } + + echo "Performing test: $method with driver $driver ".PHP_EOL; + + for ($j=0; $j<$times; $j++) { + + $t1 = microtime( TRUE ); + + $test->$method(); + + $t2 = microtime( TRUE ); + + $d[] = ($t2 - $t1); + + } + + $s = array_sum($d); + $a = ($s / $times); + $mx = max($d); + $mn = min($d); + + echo PHP_EOL."AVG: $a, MAX: $mx, MIN: $mn, TOTAL: $s, TIMES: $times ".PHP_EOL; + } +} diff --git a/vendor/gabordemooij/redbean/testing/cli/runtests.php b/vendor/gabordemooij/redbean/testing/cli/runtests.php new file mode 100644 index 0000000..b28bba2 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/cli/runtests.php @@ -0,0 +1,351 @@ + '0;31', + 'pgsql' => '0;32', + 'sqlite' => '0;34', + 'CUBRID' => '0;35', +); + +@include 'cli/test_hook.php'; + +//Configure the databases +if ( isset( $ini['mysql'] ) ) { + $dsn = "mysql:host={$ini['mysql']['host']};dbname={$ini['mysql']['schema']}"; + + R::addDatabase( 'mysql', $dsn, $ini['mysql']['user'], $ini['mysql']['pass'], FALSE ); + R::addDatabase( 'mysqlc', $dsn, $ini['mysql']['user'], $ini['mysql']['pass'], FALSE ); + R::addDatabase( 'mysqlc2', $dsn, $ini['mysql']['user'], $ini['mysql']['pass'], FALSE ); + + R::selectDatabase( 'mysql' ); + + R::exec( ' SET GLOBAL sql_mode="" ' ); +} + +//HHVM on Travis does not yet support PostgreSQL. +if ( defined( 'HHVM_VERSION' ) ) { + unset( $ini['pgsql'] ); +} else { + if ( isset( $ini['pgsql'] ) ) { + $dsn = "pgsql:host={$ini['pgsql']['host']};dbname={$ini['pgsql']['schema']}"; + R::addDatabase( 'pgsql', $dsn, $ini['pgsql']['user'], $ini['pgsql']['pass'], FALSE ); + R::addDatabase( 'pgsqlc', $dsn, $ini['pgsql']['user'], $ini['pgsql']['pass'], FALSE ); + R::addDatabase( 'pgsqlc2', $dsn, $ini['pgsql']['user'], $ini['pgsql']['pass'], FALSE ); + } +} + +if ( isset( $ini['sqlite'] ) ) { + R::addDatabase( 'sqlite', 'sqlite:' . $ini['sqlite']['file'], NULL, NULL, FALSE ); + R::selectDatabase( 'sqlite' ); +} + +if ( isset( $ini['CUBRID'] ) ) { + $dsn = "cubrid:host={$ini['CUBRID']['host']};port=33000;dbname={$ini['CUBRID']['schema']}"; + R::addDatabase( 'CUBRID', $dsn, $ini['CUBRID']['user'], $ini['CUBRID']['pass'], FALSE ); + R::selectDatabase( 'CUBRID' ); + R::exec( 'AUTOCOMMIT IS ON' ); +} + +// Function to activate a driver +function activate_driver( $d ) +{ + R::selectDatabase( $d ); +} + +$arguments = $_SERVER['argc']; + +$mode = 'all'; +if ( $arguments > 1 ) { + $mode = $_SERVER['argv'][1]; +} + +$classSpec = null; +if ( $arguments > 2 ) { + $classSpec = $_SERVER['argv'][2]; +} + +$covFilter = null; +if ( $arguments > 3 ) { + $covFilter = $_SERVER['argv'][3]; +} + +$path = 'RedUNIT/'; + +// Possible Selections +$packList = array(); + +$allPacks = array( + 'Blackhole/Version', + 'Blackhole/Toolbox', + 'Blackhole/Fusebox', + 'Blackhole/Labels', + 'Blackhole/Tainted', + 'Blackhole/Meta', + 'Blackhole/Import', + 'Blackhole/Export', + 'Blackhole/Glue', + 'Blackhole/Plugins', + 'Blackhole/Debug', + 'Blackhole/Stub', + 'Base/Productivity', + 'Base/Quickexport', + 'Base/Hybrid', + 'Base/Concurrency', + 'Base/Dispense', + 'Base/Logging', + 'Base/Cursors', + 'Base/Indexes', + 'Base/Issue408', + 'Base/Threeway', + 'Base/Chill', + 'Base/Arrays', + 'Base/Boxing', + 'Base/Typechecking', + 'Base/Observers', + 'Base/Database', + 'Base/Exceptions', + 'Base/Foreignkeys', + 'Base/Namedparams', + 'Base/Prefixes', + 'Base/Copy', + 'Base/Dup', + 'Base/Update', + 'Base/Utf8', + 'Base/Batch', + 'Base/Bean', + 'Base/Writecache', + 'Base/Relations', + 'Base/Association', + 'Base/Aliasing', + 'Base/Cross', + 'Base/Finding', + 'Base/Partial', + 'Base/Facade', + 'Base/Frozen', + 'Base/Fuse', + 'Base/Tags', + 'Base/Xnull', + 'Base/Largenum', + 'Base/Issue90', + 'Base/Issue259', + 'Base/Issue303', + 'Base/Nuke', + 'Base/Keywords', + 'Base/Count', + 'Base/With', + 'Base/Joins', + 'Base/Traverse', + 'Base/Misc', + 'Base/Via', + 'Base/PullRequest530', + 'Base/Issue841', + 'Mysql/Preexist', + 'Mysql/Double', + 'Mysql/Writer', + 'Mysql/Freeze', + 'Mysql/Setget', + 'Mysql/Foreignkeys', + 'Mysql/Parambind', + 'Mysql/Uuid', + 'Mysql/Bigint', + 'Mysql/Issue411', + 'Postgres/Setget', + 'Postgres/Foreignkeys', + 'Postgres/Parambind', + 'Postgres/Writer', + 'Postgres/Uuid', + 'Postgres/Bigint', + 'Postgres/Partial', + 'Postgres/Trees', + 'Sqlite/Setget', + 'Sqlite/Foreignkeys', + 'Sqlite/Parambind', + 'Sqlite/Writer', + 'Sqlite/Rebuild', + 'CUBRID/Writer', + 'CUBRID/Setget', +); + +$suffix = array( + 'Blackhole/Misc', + 'Base/Close' +); + +// Default (mode == all) +if ( $mode == 'all' ) { + $packList = $allPacks; +} else { + foreach ( $allPacks as $pack ) { + if ( strpos( $pack, $mode ) === 0 ) $packList[] = $pack; + } + //Add plugin pack to list. + if ( count($packList) === 0 && count($extraTestsFromHook) === 0 ) { + $packList[] = $mode; + } +} + +// Always include the last ones. +$packList = array_unique(array_merge( $packList, $suffix, $extraTestsFromHook )); +$j = 0; +foreach ( $packList as $testPack ) { + $j++; + if ( file_exists( $path . $testPack . '.php' ) ) require( $path . $testPack . '.php' ); + elseif ( file_exists( $hookPath . $testPack . '.php') ) require( $hookPath . $testPack . '.php' ); + $testPack = str_replace( '../', '', $testPack ); + if ($j === 1 && $classSpec) { + $testClass = $classSpec; + } else { + $testClassName = str_replace( ' ', '\\', ( str_replace( '/', ' ', $testPack ) ) ); + $testClass = '\\RedUNIT\\' . ucfirst( $testClassName ); + } + $test = new $testClass(); + $drivers = $test->getTargetDrivers(); + maintestpack( str_replace( '_', ' ', get_class( $test ) ) ); + $round = 0; + $test->setRound( $round ); + if ( $drivers && is_array( $drivers ) ) { + foreach ( $drivers as $driver ) { + if ( !isset( $ini[$driver] ) ) continue; + echo PHP_EOL; + echo '===== DRIVER : (' . $driver . ') ====='; + echo PHP_EOL; + echo PHP_EOL; + if ( isset( $colorMap[$driver] ) ) { + echo "\033[{$colorMap[$driver]}m"; + } + activate_driver( $driver ); + $currentDriver = $driver; + $test->setCurrentDriver( $driver ); + $test->prepare(); + $test->run(); + $test->cleanUp(); + if ( isset ( $colorMap[$driver] ) ) { + echo "\033[0m"; + } + echo PHP_EOL; + $test->setRound( ++$round ); + } + } else { + $test->prepare(); + $test->run(); + $test->cleanUp(); + } +} + +if (!$xdebugSupported) { + echo 'Done. No report - XDEBUG not installed.'; + exit(0); +} + +$report = xdebug_get_code_coverage(); +$misses = 0; +$hits = 0; + +$covLines = array(); +foreach( $report as $file => $lines ) { + + $pi = pathinfo( $file ); + + if ( $covFilter !== null ) { + if ( strpos( $file, $covFilter ) === FALSE ) continue; + } else { + if ( strpos( $file, '/rb.php' ) === FALSE ) { + if ( strpos( $file, 'phar/' ) === FALSE ) continue; + if ( strpos( $file, '.php' ) === FALSE ) continue; + if ( strpos( $file, 'RedBeanPHP' ) === FALSE ) continue; + } + } + $covLines[] = '***** File:'.$file.' ******'; + $fileData = file_get_contents( $file ); + $fileLines = explode( "\n", $fileData ); + $i = 1; + foreach( $fileLines as $covLine ) { + if ( isset( $lines [$i] ) ) { + if ( $lines[$i] === 1 ) { + $covLines[] = '[ OK ] '.$covLine; + $hits ++; + } else if ( $lines[$i] === -1 ){ + $covLines[] = '[ MISSED! ] '.$covLine; + $misses ++; + } else { + $covLines[] = '[ - ] '.$covLine; + } + } else { + $covLines[] = '[ - ] '.$covLine; + } + $i ++; + } +} +$covFile = implode( "\n", $covLines ); +@file_put_contents( 'cli/coverage_log.txt', $covFile ); +if ( $hits > 0 ) { + $perc = ( $hits / ( $hits + $misses ) ) * 100; +} else { + $perc = 0; +} +echo 'Code Coverage: '.PHP_EOL; +echo 'Hits: '.$hits.PHP_EOL; +echo 'Misses: '.$misses.PHP_EOL; +echo 'Percentage: '.$perc.' %'.PHP_EOL; +exit( 0 ); diff --git a/vendor/gabordemooij/redbean/testing/cli/test_hook_example.php b/vendor/gabordemooij/redbean/testing/cli/test_hook_example.php new file mode 100644 index 0000000..ab86d26 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/cli/test_hook_example.php @@ -0,0 +1,14 @@ + add 1 additional path which will be searched for unit tests + * $extraTestsFromHook -> array with additional test packs (will be executed if you pass 'all' argument to CLI runner) + * + * + */ \ No newline at end of file diff --git a/vendor/gabordemooij/redbean/testing/cli/testcontainer/put-rb-file-here.txt b/vendor/gabordemooij/redbean/testing/cli/testcontainer/put-rb-file-here.txt new file mode 100644 index 0000000..796d8aa --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/cli/testcontainer/put-rb-file-here.txt @@ -0,0 +1,5 @@ +/* + +Put the rb.php file in this directory + +*/ diff --git a/vendor/gabordemooij/redbean/testing/config/test-dist.ini b/vendor/gabordemooij/redbean/testing/config/test-dist.ini new file mode 100644 index 0000000..e2d1b8a --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/config/test-dist.ini @@ -0,0 +1,17 @@ +; Test suite database config +; Rename this file to test.ini if you are done + +[mysql] +host = "localhost" +schema = "" +user = "" +pass = "" + +[pgsql] +host = "localhost" +schema = "" +user = "" +pass = "" + +[sqlite] +file = "/tmp/database.db" diff --git a/vendor/gabordemooij/redbean/testing/config/test-travis.ini b/vendor/gabordemooij/redbean/testing/config/test-travis.ini new file mode 100644 index 0000000..f6dd1ed --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/config/test-travis.ini @@ -0,0 +1,18 @@ +; Test suite database config +; Rename this file to test.ini if you are done + +[mysql] +host = "localhost" +schema = "oodb" +user = "root" +pass = "" + +[pgsql] +host = "localhost" +schema = "oodb" +user = "postgres" +pass = "" + +[sqlite] +file = "/tmp/oodb.db" + diff --git a/vendor/gabordemooij/redbean/testing/helpers/classes.php b/vendor/gabordemooij/redbean/testing/helpers/classes.php new file mode 100644 index 0000000..5afabae --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/helpers/classes.php @@ -0,0 +1,635 @@ +signal( $eventname, $info ); } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Observer Mock + */ +class ObserverMock implements \RedBeanPHP\Observer +{ + public $event = FALSE; + public $info = FALSE; + public function onEvent( $event, $info ){ $this->event = $event; $this->info = $info; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A test model to test FUSE functions. + */ +class Model_Band extends RedBeanPHP\SimpleModel +{ + public function after_update() { } + private $notes = array(); + public function update() + { + if ( count( $this->ownBandmember ) > 4 ) throw new Exception( 'too many!' ); + } + public function __toString(){ return 'bigband'; } + public function setProperty( $prop, $value ) { $this->$prop = $value; } + public function checkProperty( $prop ) { return isset( $this->$prop ); } + public function setNote( $note, $value ){ $this->notes[ $note ] = $value; } + public function getNote( $note ) { return $this->notes[ $note ]; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + */ +class Model_Box extends RedBeanPHP\SimpleModel +{ + public function delete() { $a = $this->bean->ownBottle; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + */ +class Model_Cocoa extends RedBeanPHP\SimpleModel +{ + public function update(){} +} +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + */ +class Model_Taste extends RedBeanPHP\SimpleModel +{ + public function after_update() + { + asrt( count( $this->bean->ownCocoa ), 0 ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + */ +class Model_Coffee extends RedBeanPHP\SimpleModel +{ + public static $defaults = array(); + + public function dispense() + { + if ( count( self::$defaults ) && !$this->bean->id ) { + foreach (self::$defaults as $key => $value) { + $this->{$key} = $value; + } + } + } + + public function __jsonSerialize() + { + return array_merge( + $this->bean->export(), + array( 'description' => "{$this->bean->variant}.{$this->bean->strength}" ) + ); + } + + public function update() + { + while ( count( $this->bean->ownSugar ) > 3 ) { + array_pop( $this->bean->ownSugar ); + } + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + */ +class Model_Test extends RedBeanPHP\SimpleModel +{ + public function update() + { + if ( $this->bean->item->val ) { + $this->bean->item->val = 'Test2'; + $can = R::dispense( 'can' ); + $can->name = 'can for bean'; + $s = reset( $this->bean->sharedSpoon ); + $s->name = "S2"; + $this->bean->item->deep->name = '123'; + $this->bean->ownCan[] = $can; + $this->bean->sharedPeas = R::dispense( 'peas', 10 ); + $this->bean->ownChip = R::dispense( 'chip', 9 ); + } + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Shared helper class for tests. + * A Model class for testing Models/FUSE and related features. + * Used in Blackhole/Export. + */ +global $lifeCycle; +class Model_Bandmember extends RedBeanPHP\SimpleModel +{ + public function open(){ global $lifeCycle; $lifeCycle .= "\n called open: " . $this->id; } + public function dispense(){ global $lifeCycle; $lifeCycle .= "\n called dispense() " . $this->bean; } + public function update() { global $lifeCycle; $lifeCycle .= "\n called update() " . $this->bean; } + public function after_update(){ global $lifeCycle; $lifeCycle .= "\n called after_update() " . $this->bean; } + public function delete(){ global $lifeCycle; $lifeCycle .= "\n called delete() " . $this->bean; } + public function after_delete(){ global $lifeCycle; $lifeCycle .= "\n called after_delete() " . $this->bean; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * A custom BeanHelper to test custom FUSE operations in + * Blackhole/Fusebox + */ +class Model_Soup extends \RedBeanPHP\SimpleModel +{ + private $flavour = ''; + public function taste() { return 'A bit too salty'; } + public function setFlavour( $flavour ) { $this->flavour = $flavour; } + public function getFlavour(){ return $this->flavour; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * A custom BeanHelper to test custom FUSE operations in + * Base/Fuse. + */ +class SoupBeanHelper extends \RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper +{ + public function getModelForBean( \RedBeanPHP\OODBBean $bean ) + { + if ( $bean->getMeta( 'type' ) === 'meal' ) { + $model = new Model_Soup; + $model->loadBean( $bean ); + return $model; + } else { + return parent::getModelForBean( $bean ); + } + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Base/Boxing and Base/Misc to test boxing of beans. + * Just a plain model for use with a bean with nothing in it. + */ +class Model_Boxedbean extends \RedBeanPHP\SimpleModel{} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Mysql/Uuid, Postgres/Uuid and Base/Association. Meant + * to be a versatile, generic test model. + */ +class Model_Ghost_House extends \RedBeanPHP\SimpleModel +{ + public static $deleted = FALSE; + public function delete() { self::$deleted = TRUE; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Mysql/Uuid, Postgres/Uuid and Base/Association. Meant + * to be a versatile, generic test model for N-M relations. + */ +class Model_Ghost_Ghost extends \RedBeanPHP\SimpleModel +{ + public static $deleted = FALSE; + public function delete() { self::$deleted = TRUE; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Mock class for testing purposes. Used in Base/Association and + * Base/Foreignkeys to emit errors to test handling of errors + * originating from the Query Writer. + */ +class FaultyWriter extends \RedBeanPHP\QueryWriter\MySQL +{ + protected $sqlState; + public function setSQLState( $sqlState ){ $this->sqlState = $sqlState; } + public function addUniqueConstraint( $sourceType, $destType ){ + $exception = new \RedBeanPHP\RedException\SQL; + $exception->setSQLState( $this->sqlState ); + throw $exception; + } + protected function getKeyMapForType( $type ){throw new \RedBeanPHP\RedException\SQL;} +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Mock class to test default implementations in AQueryWriter. + */ +class NullWriter extends \RedBeanPHP\QueryWriter\AQueryWriter {} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Base/Foreignkeys (testFKInspect) to test foreign keys. +*/ +class ProxyWriter extends \RedBeanPHP\QueryWriter\AQueryWriter { + public static function callMethod( $object, $method, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL ) { + return $object->$method( $arg1, $arg2, $arg3 ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Mock class to test proper model name + * beautificattion for link table beans in FUSE. + */ +class Model_PageWidget extends RedBean_SimpleModel { + private static $test = ''; + public static function getTestReport(){ return self::$test; } + public function update(){ self::$test = 'didSave'; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Mock class to test proper model name + * beautificattion for link table beans in FUSE. + */ +class Model_Gadget_Page extends RedBean_SimpleModel { + private static $test = ''; + public static function getTestReport(){ return self::$test;} + public function update(){ self::$test = 'didSave'; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Mock class to test proper model name + * beautificattion for link table beans in FUSE. + */ +class Model_A_B_C extends RedBean_SimpleModel { + private static $test = ''; + public static function getTestReport(){ return self::$test; } + public function update() { self::$test = 'didSave'; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Base/Update to test SQL filters with links + */ +class Model_BookBook extends \RedBean_SimpleModel { + public function delete() { + asrt($this->bean->shelf, 'x13'); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Base/Fuse (error handling in Fuse) and + * Base/Issue408 (export issue). + */ +class Model_Feed extends \RedbeanPHP\SimpleModel { + public function update() { $this->bean->post = json_encode( $this->bean->post );} + public function open() { $this->bean->post = json_decode( $this->bean->post, TRUE );} +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * UUID QueryWriter for MySQL for testing purposes. + * Used in Mysql/Uuid to test if RedBeanPHP can be used with a + * UUID-strategy. While UUID keys are not part of the RedBeanPHP core, + * examples are given on the website and this test makes sure those examples + * are working as expected. + */ +class UUIDWriterMySQL extends \RedBeanPHP\QueryWriter\MySQL { + protected $defaultValue = '@uuid'; + const C_DATATYPE_SPECIAL_UUID = 97; + public function __construct( \RedBeanPHP\Adapter $adapter ) + { + parent::__construct( $adapter ); + $this->addDataType( self::C_DATATYPE_SPECIAL_UUID, 'char(36)' ); + } + public function createTable( $table ) + { + $table = $this->esc( $table ); + $sql = " + CREATE TABLE {$table} ( + id char(36) NOT NULL, + PRIMARY KEY ( id )) + ENGINE = InnoDB DEFAULT + CHARSET=utf8mb4 + COLLATE=utf8mb4_unicode_ci "; + $this->adapter->exec( $sql ); + } + public function updateRecord($table, $updateValues, $id = NULL) + { + $flagNeedsReturnID = (!$id); + if ($flagNeedsReturnID) R::exec('SET @uuid = uuid() '); + $id = parent::updateRecord( $table, $updateValues, $id ); + if ( $flagNeedsReturnID ) $id = R::getCell('SELECT @uuid'); + return $id; + } + public function getTypeForID(){return self::C_DATATYPE_SPECIAL_UUID;} +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * UUID QueryWriter for PostgreSQL for testing purposes. + * Used in Postgres/Uuid to test if RedBeanPHP can be used with a + * UUID-strategy. While UUID keys are not part of the RedBeanPHP core, + * examples are given on the website and this test makes sure those examples + * are working as expected. + */ +class UUIDWriterPostgres extends \RedBeanPHP\QueryWriter\PostgreSQL { + + protected $defaultValue = 'uuid_generate_v4()'; + const C_DATATYPE_SPECIAL_UUID = 97; + + public function __construct( \RedBeanPHP\Adapter $adapter ) + { + parent::__construct( $adapter ); + $this->addDataType( self::C_DATATYPE_SPECIAL_UUID, 'uuid' ); + } + + public function createTable( $table ) + { + $table = $this->esc( $table ); + $this->adapter->exec( " CREATE TABLE $table (id uuid PRIMARY KEY); " ); + } + + public function getTypeForID() + { + return self::C_DATATYPE_SPECIAL_UUID; + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * This diagnostic bean class adds a method to read the current + * status of the modifier flags. Used to test interactions with + * beans and monitor the effect on the internal flags. + */ +class DiagnosticBean extends \RedBeanPHP\OODBBean { + + /** + * Returns current status of modification flags. + * + * @return string + */ + public function getModFlags() + { + $modFlags = ''; + if ($this->aliasName !== NULL) $modFlags .= 'a'; + if ($this->fetchType !== NULL) $modFlags .= 'f'; + if ($this->noLoad === TRUE) $modFlags .= 'n'; + if ($this->all === TRUE) $modFlags .= 'r'; + if ($this->withSql !== '') $modFlags .= 'w'; + return $modFlags; + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * This is diagnostic class that allows access to otherwise + * protected methods.Used to test FUSE hooks in Base/Fuse.php + * Subclassed by Model_Probe. + */ +class DiagnosticModel extends \RedBeanPHP\SimpleModel +{ + + private $logs = array(); + public function open() { $this->logs[] = array('action' => 'open','data'=> array('id' => $this->id));} + public function dispense(){$this->logs[] = array('action' => 'dispense','data' => array('bean' => $this->bean));} + public function update(){$this->logs[] = array('action' => 'update','data' => array('bean' => $this->bean));} + public function after_update(){$this->logs[] = array('action' => 'after_update','data'=> array('bean' => $this->bean));} + public function delete(){$this->logs[] = array('action' => 'delete','data'=> array('bean' => $this->bean));} + public function after_delete(){$this->logs[] = array('action' => 'after_delete','data' => array('bean' => $this->bean));} + public function getLogs(){return $this->logs;} + public function getLogActionCount( $action = NULL ) + { + if ( is_null( $action ) ) return count( $this->logs ); + $counter = 0; + foreach( $this->logs as $log ) if ( $log['action'] == $action ) $counter ++; + return $counter; + } + public function clearLog(){return $this->logs = array();} + public function getDataFromLog( $logIndex = 0, $property ){return $this->logs[$logIndex]['data'][$property];} +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Base/Database (testDatabaseCapabilityChecker) to check + * database capabilities. +*/ +class DatabaseCapabilityChecker extends \RedBeanPHP\Driver\RPDO { + + public function __construct( \PDO $pdo ) + { + $this->pdo = $pdo; + } + + public function checkCapability( $capID ) + { + return $this->hasCap( $capID ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Used in Test Suite Base/Bean (testToStringOverride) + * to test string overrides. + */ +class Model_String extends \RedBeanPHP\SimpleModel { + public function __toString() { + return base64_encode( $this->bean->text ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * This is diagnostic class that allows access to otherwise + * protected methods.Used to test FUSE hooks in Base/Fuse.php + */ +class Model_Probe extends DiagnosticModel {}; + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Class to mock adapter. + * Inspects behavior of classes interacting with the adapter class + * by capturing the method invocations. + */ +class Mockdapter implements \RedBeanPHP\Adapter { + + public function answer( $id ) + { + $error = "error{$id}"; + $property = "answer{$id}"; + if (isset($this->$error)) throw $this->$error; + if (isset($this->$property)) return $this->$property; + } + + public function getSQL(){} + public function exec( $sql, $bindings = array(), $noevent = FALSE ){ return $this->answer('Exec'); } + public function get( $sql, $bindings = array() ){ return $this->answer('GetSQL'); } + public function getRow( $sql, $bindings = array() ){ return array(); } + public function getCol( $sql, $bindings = array() ){ return $this->answer('GetCol'); } + public function getCell( $sql, $bindings = array() ){ return ''; } + public function getAssoc( $sql, $bindings = array() ){ return array(); } + public function getAssocRow( $sql, $bindings = array() ){ return array(); } + public function getInsertID(){} + public function getAffectedRows(){} + public function getCursor( $sql, $bindings = array() ){} + public function getDatabase(){} + public function startTransaction(){} + public function commit(){} + public function rollback(){} + public function close(){} + public function setOption( $optionKey, $optionValue ){} + public function getDatabaseServerVersion(){ return 'Mock'; } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Custom Logger class. + */ +class CustomLogger extends \RedBeanPHP\Logger\RDefault +{ + + private $log; + public function getLogMessage(){ return $this->log; } + public function log() { $this->log = func_get_args(); } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * This is diagnostic class that allows access to otherwise + * protected methods. + * Class to test protected method hasCap in RPDO. + */ +class TestRPO extends \RedBeanPHP\Driver\RPDO { + public function testCap( $cap ) { + return $this->hasCap( $cap ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * Class to mock PDO behavior. + */ +class MockPDO extends \PDO { + public $attributes = array(); + public function __construct() { } + public function setAttribute( $att, $val = NULL ){ $this->attributes[ $att ] = $val; } + public function getDiagAttribute( $att ){ return $this->attributes[ $att ]; } + public function getAttribute( $att ) { + if ($att == \PDO::ATTR_SERVER_VERSION) return '5.5.3'; + return 'x'; + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * DiagnosticCUBRIDWriter + * Class for stub test for CUBRID database support. + */ +class DiagnosticCUBRIDWriter extends \RedBeanPHP\QueryWriter\CUBRID { + public function callMethod( $method, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL, $arg4 = NULL, $arg5 = NULL ) { + return $this->$method( $arg1, $arg2, $arg3, $arg4, $arg5 ); + } +} + +/** + * Test utility class. + * This class is meant for testing purposes only and should + * never be used for anything else than RedBeanPHP Unit Testing. + * This is an error class that allows RedBeanPHP Unit Tests to + * test error handling. + * Test Model that throws an exception upon update(). + */ +class Model_Brokentoy extends \RedbeanPHP\SimpleModel { + public function update(){ + throw new \Exception('Totally on purpose.'); + } +} + + +define('REDBEAN_OODBBEAN_CLASS', '\DiagnosticBean'); diff --git a/vendor/gabordemooij/redbean/testing/helpers/functions.php b/vendor/gabordemooij/redbean/testing/helpers/functions.php new file mode 100644 index 0000000..77b79d9 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/helpers/functions.php @@ -0,0 +1,520 @@ +" . $text; + } else { + echo "\n" . $text; + } +} + +/** + * Tests whether a === b. The minimalistic core of this little + * unit test framework. + * + * @global integer $tests + * + * @param mixed $a value for A + * @param mixed $b value for B + */ +function asrt( $a, $b ) +{ + if ( $a === $b ) { + global $tests; + + $tests++; + + print( "[" . $tests . "]" ); + } else { + printtext( "FAILED TEST: EXPECTED $b BUT GOT: $a " ); + + fail(); + } +} + +/** + * called when a test is passed. prints the test number to the screen. + */ +function pass() +{ + global $tests; + + $tests++; + + print( "[" . $tests . "]" ); +} + +/** + * called when a test fails. shows debug info and exits. + */ +function fail() +{ + printtext( "FAILED TEST" ); + + debug_print_backtrace(); + + exit( 1 ); +} + +/** + * prints out the name of the current test pack. + * + * @param string $name name of the test pack + */ +function testpack( $name ) +{ + printtext( "\n\tSub testpack: " . $name . " \n\t" ); +} + +/** + * prints out the name of the current test pack. + * + * @param string $name name of the test pack + */ +function maintestpack( $name ) +{ + printtext( "\n\nTestpack: " . $name . " \n\t" ); +} + +/** + * Quickly resolves the formatted table name + */ +function tbl( $table ) +{ + return R::$writer->getFormattedTableName( $table ); +} + +/** + * Quickly resolves the formatted ID + */ +function ID( $table ) +{ + return R::$writer->getIDField( $table ); +} + +/** + * Emulates legacy function for use with older tests. + */ +function set1toNAssoc( $a, \RedBeanPHP\OODBBean $bean1, \RedBeanPHP\OODBBean $bean2 ) +{ + $type = $bean1->getMeta( "type" ); + + $a->clearRelations( $bean2, $type ); + $a->associate( $bean1, $bean2 ); + + if ( count( $a->related( $bean2, $type ) ) === 1 ) { + ; + } else { + throw new \RedBeanPHP\RedException\SQL( "Failed to enforce 1-N Relation for $type " ); + } +} + +/** + * Returns all property values of beans as a + * comma separated string sorted. + * + * @param array $beans beans + * @param string $property name of the property + * + * @return string $values values + */ +function getList( $beans, $property ) +{ + $items = array(); + + foreach ( $beans as $bean ) { + $items[] = $bean->$property; + } + + sort( $items ); + + return implode( ",", $items ); +} + +/** + * Helper function to test IDs + * + * @param array $array array + */ +function testids( $array ) +{ + foreach ( $array as $key => $bean ) { + asrt( intval( $key ), intval( $bean->getID() ) ); + } +} + +/** + * Group modifier function. Tests random modifications + * of groups of beans. For use with tests that test N:1 relation mapping + * features. + * + * @param mixed $book3 book + * @param mixed $quotes quotes + * @param mixed $pictures pictures + * @param mixed $topics topics + */ +function modgr( $book3, $quotes, $pictures, $topics ) +{ + $key = array_rand( $quotes ); + + $quote = $quotes[$key]; + + $keyPic = array_rand( $pictures ); + + $picture = $pictures[$keyPic]; + + $keyTop = array_rand( $topics ); + + $topic = $topics[$keyTop]; + + if ( rand( 0, 1 ) ) { + $f = 0; + + foreach ( $book3->ownQuote as $z ) { + if ( $z->note == $quote->note ) { + $f = 1; + + break; + } + } + if ( !$f ) { + //echo "\n add a quote "; + + $book3->ownQuote[] = $quote; + } + } + + if ( rand( 0, 1 ) ) { + $f = 0; + + foreach ( $book3->ownPicture as $z ) { + if ( $z->note == $picture->note ) { + $f = 1; + + break; + } + } + if ( !$f ) { + // echo "\n add a picture "; + + $book3->ownPicture[] = $picture; + } + } + + if ( rand( 0, 1 ) ) { + $f = 0; + + foreach ( $book3->sharedTopic as $z ) { + if ( $z->note == $topic->note ) { + $f = 1; + + break; + } + } + + if ( !$f ) { + $book3->sharedTopic[] = $topic; + } + } + + if ( rand( 0, 1 ) && count( $book3->ownQuote ) > 0 ) { + $key = array_rand( $book3->ownQuote ); + + unset( $book3->ownQuote[$key] ); + } + + if ( rand( 0, 1 ) && count( $book3->ownPicture ) > 0 ) { + $key = array_rand( $book3->ownPicture ); + + unset( $book3->ownPicture[$key] ); + } + + if ( rand( 0, 1 ) && count( $book3->sharedTopic ) > 0 ) { + $key = array_rand( $book3->sharedTopic ); + + unset( $book3->sharedTopic[$key] ); + } + + if ( rand( 0, 1 ) && count( $book3->ownPicture ) > 0 ) { + $key = array_rand( $book3->ownPicture ); + + $book3->ownPicture[$key]->change = rand( 0, 100 ); + } + + if ( rand( 0, 1 ) && count( $book3->ownQuote ) > 0 ) { + $key = array_rand( $book3->ownQuote ); + + $book3->ownQuote[$key]->change = 'note ch ' . rand( 0, 100 ); + } + + if ( rand( 0, 1 ) && count( $book3->sharedTopic ) > 0 ) { + $key = array_rand( $book3->sharedTopic ); + + $book3->sharedTopic[$key]->change = rand( 0, 100 ); + } +} + +/** + * SetGet function, sets a value in a bean and retrieves it again + * after storage, useful for tests that want to make sure the same value + * that gets in, comes out again. + * + * @param mixed $val the value that needs to be preserved + * + * @return mixed $val the value as returned after storage-retrieval cycle. + */ +function setget( $val ) +{ + R::nuke(); + + $bean = R::dispense( "page" ); + + $bean->prop = $val; + + $id = R::store( $bean ); + + $bean = R::load( "page", $id ); + + return $bean->prop; +} + +/** + * Wrapper function to test BeanCan Server, does the boring + * plumming work. + * + * @param mixed $data Data for JSON-RPC request object + * @param mixed $params Parameters for JSON-RPC request object + * @param string $id Identification of JSON-RPC request to connect to response + * + * @return string $out Output JSON from BeanCan server. + */ +function fakeBeanCanServerRequest( $data, $params = NULL, $id = "1234", $whiteList = 'all' ) +{ + $j = array( + "jsonrpc" => "2.0", + "method" => $data, + "params" => $params, + "id" => $id + ); + + $can = new \RedBeanPHP\Plugin\BeanCan; + + $request = json_encode( $j ); + + $can->setWhitelist( $whiteList ); + + $out = $can->handleJSONRequest( $request ); + + return $out; +} + +/** + * Candy Cane Factory. Produces lots of candy canes. + * + * @return array $canes canes + */ +function candy_canes() +{ + $canes = R::dispense( 'cane', 10 ); + + $i = 0; + + foreach ( $canes as $k => $cane ) { + $canes[$k]->label = 'Cane No. ' . ( $i++ ); + } + + $canes[0]->cane = $canes[1]; + $canes[1]->cane = $canes[4]; + $canes[9]->cane = $canes[4]; + $canes[6]->cane = $canes[4]; + $canes[4]->cane = $canes[7]; + $canes[8]->cane = $canes[7]; + + return $canes; +} + +/** + * Returns an array containing the index names of all + * indexes on the specified table name. + * + * @param $tableNoQ table name without quotes or backticks + * + * @return array + */ +function getIndexes( $tableNoQ ) +{ + $writer = R::getWriter(); + + if ( ( $writer instanceof \RedBeanPHP\QueryWriter\MySQL ) || ( $writer instanceof \RedBeanPHP\QueryWriter\CUBRID ) ) { + $indexes = array(); + $list = R::getAll( "SHOW INDEX FROM `{$tableNoQ}`" ); + foreach( $list as $listItem ) { + $indexes[] = $listItem['Key_name']; + } + return $indexes; + } + + if ( ( $writer instanceof \RedBeanPHP\QueryWriter\SQLiteT ) ) { + $indexes = array(); + $list = R::getAll( " pragma index_list(`{$tableNoQ}`) " ); + foreach( $list as $listItem ) { + $indexes[] = $listItem['name']; + } + return $indexes; + } + + if ( ( $writer instanceof \RedBeanPHP\QueryWriter\PostgreSQL ) ) { + return R::getCol( " SELECT indexname FROM pg_indexes WHERE tablename = '{$tableNoQ}' AND schemaname = 'public' " ); + } + + return array(); +} + +function are_cols_in_unique( $type, $properties ) +{ + sort( $properties ); + $propertyFootprint = implode( ',', $properties ); + $uniques = get_uniques_for_type( $type ); + foreach( $uniques as $unique ) { + sort( $unique ); + $uniqueFootprint = implode( ',', $unique ); + if ( $uniqueFootprint === $propertyFootprint ) return TRUE; + } + return FALSE; +} + +function get_uniques_for_type( $type ) +{ + $list = array(); + $writer = R::getWriter(); + $adapter = R::getDatabaseAdapter(); + global $currentDriver; + switch( $currentDriver ) { + case 'mysql': + $table = $writer->esc( $type, TRUE ); + $columns = $adapter->get(' + SELECT + information_schema.key_column_usage.constraint_name, + information_schema.key_column_usage.column_name + FROM + information_schema.table_constraints + INNER JOIN information_schema.key_column_usage + ON ( + information_schema.table_constraints.constraint_name = information_schema.key_column_usage.constraint_name + AND information_schema.table_constraints.constraint_schema = information_schema.key_column_usage.constraint_schema + AND information_schema.table_constraints.constraint_catalog = information_schema.key_column_usage.constraint_catalog + ) + WHERE + information_schema.table_constraints.table_schema IN (SELECT DATABASE()) + AND information_schema.table_constraints.table_name = ? + AND information_schema.table_constraints.constraint_type = \'UNIQUE\' + ', array( $table ) ); + $uniques = array(); + foreach( $columns as $column ) { + if ( !isset( $uniques[ $column['constraint_name'] ] ) ) $uniques[ $column['constraint_name'] ] = array(); + $uniques[ $column['constraint_name'] ][] = $column['column_name']; + } + $list = $uniques; + break; + case 'pgsql': + $table = $writer->esc( $type, TRUE ); + $columns = $adapter->get(' + SELECT + information_schema.key_column_usage.constraint_name, + information_schema.key_column_usage.column_name + FROM + information_schema.table_constraints + INNER JOIN information_schema.key_column_usage + ON ( + information_schema.table_constraints.constraint_name = information_schema.key_column_usage.constraint_name + AND information_schema.table_constraints.constraint_schema = information_schema.key_column_usage.constraint_schema + AND information_schema.table_constraints.constraint_catalog = information_schema.key_column_usage.constraint_catalog + ) + WHERE + information_schema.table_constraints.table_catalog = current_database() + AND information_schema.key_column_usage.table_schema = ANY( current_schemas( FALSE ) ) + AND information_schema.table_constraints.table_name = ? + AND information_schema.table_constraints.constraint_type = \'UNIQUE\' + ', array( $table ) ); + $uniques = array(); + foreach( $columns as $column ) { + if ( !isset( $uniques[ $column['constraint_name'] ] ) ) $uniques[ $column['constraint_name'] ] = array(); + $uniques[ $column['constraint_name'] ][] = $column['column_name']; + } + $list= $uniques; + break; + case 'sqlite': + $uniques = array(); + $table = $writer->esc( $type, TRUE ); + $indexes = $adapter->get( "PRAGMA index_list({$table})" ); + foreach( $indexes as $index ) { + if ( $index['unique'] == 1 ) { + $info = $adapter->get( "PRAGMA index_info({$index['name']})" ); + if ( !isset( $uniques[$index['name']] ) ) $uniques[$index['name']] = array(); + foreach( $info as $piece ) { + $uniques[$index['name']][] = $piece['name']; + } + } + } + $list = $uniques; + break; + case 'CUBRID': + try { + $sqlCode = $adapter->get("SHOW CREATE TABLE `{$type}`"); + } catch ( \Exception $e ) { + $sqlCode = ''; + } + if (!isset($sqlCode[0])) return array(); + $matches = array(); + preg_match_all('/CONSTRAINT\s+\[([\w_]+)\]\s+UNIQUE\s+KEY\s+\(([^\)]+)\)/', $sqlCode[0]['CREATE TABLE'], $matches); + $list = array(); + if (!isset($matches[0])) return $list; + $max = count($matches[0]); + for($i = 0; $i < $max; $i++) { + $columns = explode(',', $matches[2][$i]); + foreach( $columns as $key => $column ) { + $columns[$key] = trim( $column, '[] '); + } + $list[ $matches[1][$i] ] = $columns; + } + break; + } + return $list; +} + +//array_column PHP 7 implementation +function colfield( $objects, $field ) { + $ids = array(); + foreach($objects as $object) $ids[] = $object->{$field}; + return $ids; +} \ No newline at end of file diff --git a/vendor/gabordemooij/redbean/testing/notes.txt b/vendor/gabordemooij/redbean/testing/notes.txt new file mode 100644 index 0000000..c3ea810 --- /dev/null +++ b/vendor/gabordemooij/redbean/testing/notes.txt @@ -0,0 +1,8 @@ + +Test notes +========== + +* PostgreSQL tests perform a text-to-money cast test, make sure lc_monetary is set to en_US +in your postgres.conf +* PostgreSQL tests need the ossp extension, to enable this, install the postgres-contrib package and run SQL: CREATE EXTENSION "uuid-ossp"; +* XDebug is NOT required but recommended