From 471228b944ef3bb8c812a1233934a4ef16f6759e Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:20:49 +0700 Subject: [PATCH 01/11] chore: add project logo --- logo.png | Bin 0 -> 146781 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 logo.png diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f67efc5edc80c63baf66200d3670ef864857bcbe GIT binary patch literal 146781 zcmZ^Jbx>VR@Ffx?IKe%*6I_G4yE_kecM0z965QP$8rymuG+O4gvt@p>R znpZP-rss6`>2o_=K~5YI4i^p#3=C0HLPQA+41x*_415?C`eVd64>jZC@f9GUBnt-S zNdX4-BM1!a>0{`}AsCnoBN*7R0T>u}8WAKSp4j zC1pinj=^EUzpzzm$*z42kvofOI2#)}n{WdhO+FrA%#6(J^o&gOjGQWrOx#SI+$T5LK`>u*E0ZF-bVwt(aCDU@VF8Yq%k zjV|F(BmzRj(K#P50&Ztc<=nT0Oq}vk&NoF9__Wi{!nN zbOQ^lSKNr)I?rVBUzeU2!bEL?%O{#L&q}W3f@@?c_C`PXhH&d^e!NGG_n1~T)0SbP`|xBf6KJM=Yq?!RrHnyziX3aZxj zMNB9u`|Ww2yC0mWgi71mAg<0f)kBfaa(R3l#$M^aC9oN;2XPR}BiyY9v`%z*0=Znf zUR|{G?{V0fL_#qvF{&^S!G8pTul8S0Kfg!u`rV4tXn7GtHYjDtNSS4P>}6|y`kz$* zpO`9g(;q~bG)hxj-aG6TcZfWQv?ox?LLGQf9mK+5vGW>*U8M$iyY5%B9k}Wp)LrwsNabXi<>ZKa8bv$Vqe40y$EzUz%P{a_33jZy{?O8G`nBVdmVz`?gV3u_vGJnCr~jII=YMtuyS?^3n-`UiBxMhe zzuc{Bt9iMRRb1Z+{fbMG44!y03Vy288b#V+@oepSMPD$Mf44WWfNNVQD2O3X`vZA$ zF_QvE(}q7urX-XqlvXo@lAg}LolJB+^4uWf8%SAN0_S~u;tqnMTt4RwfGuUv2iISg z$=gp|bjX!qp#n94GcuAF+;@c^TcR!acl$M$wi(@~NmQQJ$jx1M&D6H9aG7up=Q&@q zpjd+r-QA+00i?CGBK>X=Ho7cx3k`J2_5E07vptpW+c3~(^1gDM0N{Z>=z(Jj=?d>8Kqn?I+ zRl#|6S~+jc?U|P8d*WCB>$!rdKD4&#-#SqLtrHC4VnZ#e`Rk3v^CDBf?{13(t;scf zsqkkmp9c9>=J+T|3B6F|3oVsK6;x}mV#qY4^S(nY&Dn3T=ud_=2cWufVs!v8` z7w;JxPZ{q$H$AU1SOF1&f#4*q)cIFV0=4Qw=nBS~BDsvYY^#(AV>zz~_nm*DX#%8HVP=#E9nO4ZPyA}PuwQGzf8SF}+2Kaa>`RwKB?x%>L4Bj3I)Qcwv z$u+pDB@o)(+dz)fp|D@MnB_c37eW350BWLUA6BnLBBq*$U%P*$=w#JstBOzh1bbX*;LSBK1g+I;=`*+hWsZ z3x%hf7bqrba|~K&MU9o!=Y4tx;(7cg=qjQk5^tO<_&<)kST-zT|l3nW}73iOvaHP2>^@nZrX2daYZ}xeO6(p#1ue`|Fk5|`J{fK zR9d@WfI(3N(D=Ys%|Dn<8kw{IfWZ^4zx@5On1CC@-yLY@Meo^{x@>?hSO-T#0pK@Z zqA6e?2v3eBS1#`_60P4sJ=kVqu+|i%Dorm7UAwJruDe<8QU=gjhDPt$r0Yia+y2xp z6%M`i|7+7Nx{$EK)5sVBpp^`vIMwu|jO7CHmRH2C5&QdS%P0f!-q^&exi#n1y{eDpfBet#@E3?U zhQvjlw+hkw_RY9?sLP4t)iF#Tk%Tw@FEBaBP@~XppKzZh1s#kE~OV84o>_CFz>ul6Ql2B zIr*Z4e{f@c!1d%mPt-&}4K!dzBf)CZf}^|Z%)E`^=!)40L}osli!GH(OhvJOJOCca zjX;E})8;0or6yt{SNG<6*?l^Ws4Cie*z`?$-hAU3off^*Sg2$-l>V{m;r?-@SpQwA z5^Hcl8FUN~)5;1CoVO{p-S@tE8n3I@RI!w%u?hBk3rjy)gEh5VYCl`@89P3?e~;}s zDFszAYBTmqJ2g#2zm9F*lIfONd*Hg;>pkf8!Y5R!1FWgI31ngMqQz&w;&ZO?H^P1Y zpQP_@Y;L7Qy82ZitovH=+-vp3`G_UQD3eN5)gOEQA#bCc#YpGHu5SRKnvf-$1&)?7 zoazJ<9|d(q%T_-v))ntI(-QrN0EAx1kOZEpjJ^6jm_Ffg9Pit`ml#D-V?7E>1NyL$ zl|GyugwGBAk%Su1%ZX97XH7MtPnvJoAiVdLVZ!B&<#Qr$+q^U&Gkm<;B(yRVk<;Kf~o6|;oADwbt z5CrsqfwZv6rznS0$t|kbdd3@R;Z4{<8ka=;kq0yqyadWNN(xh_Nx~h?1aP(mfp{$@ zmtchgwK|Z&f@=_*YxrQ-!0#zof?HA005``ji7N z`Uw?s7BFX`?zGkH$A5disxj}+LQpxNq>u_t@0FJCd=kN28iFp41i0Jm8yxT`u9Zuj zMZy!)bs*?E5W|myEu}P3Rb-+IW9~A++vCDgHljk0D$8S>M^^#0;0jolp;R8#Ie0>@ zWU{H1Br_gX0As+NFjh>#A4(J&p#{EVh^Vi@f-=k$ z)~duqNJo$to&@lI_lE3%GSs2`{=(aI)!aPT+-E8BUrb>_{`sOkTlK$x5j{D${(6B? z@RW1Y1{0M}>Lp4P{pBSAA9zz7cNDE~qdl6K5t=7iuC-Mi3f zSrh#YfZ5td^E53hisF(1YhHDFsIb-Y2o4w>&S(}zR?+*?*gM6mHWW7Ozx*RNBfHq` zcJB8}MWGtysHl*N95Wv)z(`1=E{aK%d*_1z-2O`fA8*m~7;-1=k4UTpsdX5VFhPY! z62{bZ`^q+-*1NcPC=vYpwJ(PmfIotM5Kt$mV)e-?j}^LKs3ufyQh|#Q=GSl4B3SYH zOOS@2w-~qb--Z8?l&wnfJ|i&i4ODPIL@DX#K}o_U+RlW!A5M>9;3rVce4-r{XQX=Y zBpV$&!a-L9V3M=wfB+3Oreg7#9LE`9s(@RD(rlJ2RyU$Yx`z)$9t!-+d}3b#o$)hI zKCBbzzcrXhq@3DJ3*qqb+4JVOS9 zzlQ>7$MTg>?kW*C8K0gUFjpI;`1L*(5OvoK4Ksa4I8#s(1D0OeA# z*X9%Yz+^$Og4SgTfAMFnLRw6Si9hz&3$Z>gSI)#51agSF`Ar`XV-ga3mIzPlh1XbU zw`Hb)%6Ire5-h`>={6YcgcgpPTGzxF=I%;n9IIP0ILw(f&KYiTMDn^oMgP;zV&SZ? zlC5m1`*1;{O2>FL>hnrbs1todC6{hl08+`nd|}8-I>xje1#f{Q`^B+WN`~hm<^DaC zuBJSUO3lg|Utu9!N(9A*nQ|TfX+Ex;RMNVk7(lsu38ZAh$T*c;>5THuwBtg)l3TTS zm#(mAlJTlYr=DM+Ikbm6)cO9m_$ZZ^{AO59(pl2s_ZN)SPv7Ux&5sj_dZ5k7+In$2(>^%fVWTd$k)%p79!^yes&8%_@6IU#}8GN|)KsdDdsnPAi?`%UWdvItr{ufwb=Zy`D>o}?e9>Dsjp^sAA?B7+6G&1itR z$Vd_onxGE;A%zgrCE=YP1DxPig)a4gSychVDvR%b(*od?V2u!psn+9@po&k4Z|1JN zJn6&igukXDO1t-silrv>|ADJ6VyG_>#`{P1J;gCSwjvOVYmF`|r5bt-v>aq&l2|D* z=V`PSm!AkiuWP3bs>vJ`%Jp(9cQ%DZimsne*?l zZz*RtO?D+qEx~WaXG${T{W3jj!CHaBRLKIvEajgNWz4diu|xl%;wOpd;io6wnpqap z<-zXlldDoPCX?Lvg|?%1s5-fv*{v;N?mp&y@BsGyBUw07Y`P_rn+M`@ALQc~Ubi~? z3ts%Ldt1!=Q$ve)Jx*VQ22Sys@@s?68%ZhoLd5FH= zT8$L^Bl+A8N#`9p*FO}+44pC@I*%g73@u@ATn9`}TLU*u=jfvrDtXmDuN2gd;L0W9 zN3|sXmB0~EoL3jR^+4C9;6|AgI`^Te#-GFNZ-h7SimI6~sjm2=Wdh7jiXA6|U{DMZ zA(q*m?|Qu6e-QCIuDIpa?$}Nt?-lXCi`aR5Rz^;zH*g|O3hue-zJW&|7Y!R!ROjU; z*Jh!fUyD@JLHZrQY_2%}gwV&P^zCb(s0ODH@ItSAwR7}k(>JVd?^9qpPDCVw@%|{~ zF^G~kSyo+N+HY~7_Xg8iS4mn>5S7*{mLrm<((ONIU=C{IJ_1A$gJi@LdRhal_a7Z@ zB;lNsubz*lyE!THno^X4X7R}TV9sazIDF$@7k%Ped29{c?`}uusblxH(epfVZxrnh&*MC_=k#hQbs~* zss0{hQ=6v(RNK?prO-NAZ>6Lb0@Ol@bs4W0?ThJg576+uCH){QucZd};F_#H2R8OhVG{CNv)mzD%5eQ ztv5MuvIv`qR|^y^a=xFSK2hZ6v#t88B2-2wUm^3aF>DvE%NIwwi|`!UONA}6lc}Qh zq_J`q39=j#zTEe~{}z_U(-hSvBtZ>K@~J5r9-DB1J;zh;gU#XSZ@U&1GjtCqAbL+b ze|<)F+?CchzFsnPl^8I#Fc9Xaa3d(s$5y`&BN@AsMoBFkCr!}dlrQ@MuWlauKm|HR+XPlvDMcesAAa^W;xSi9lsGgc zzNxS(szxyp&9~#!n`*bt;K!4${`qYtS1pr#m?r2dD=al@xcRs}I>}WN89a`#+OH8Q zjVVp3lfBJfYEn9+Q(y(l3iE2hT&uM^)owl1=~#jd+sv>hiGKHQ`o^8qywhpt+>>HumMYdBdzmLq*zSS&-Ef%nB7(iWVPfOGo=1Rl8M%K z)10Ifuy%d+W-@|`cfW8F$(>Ia$R5Bultkyb!B1zw?_Q3)ZqD!DP#cDj>NV+nAVDC6 zs@vUQKihH3baR)&csz=*rjT1`?ewBvFEzmm?%4CFHw@KKXGi5v|U zk!3SN0461w963_R$kuKpSf%-lHbAlXOU<~*#KJ=)?)677$1n#L7L1elDUrpAK#DdP z>kNj;UoPM_b3ZKMa2#Smw7uJQ>@~kpI_12;G~t;@(E+PU3v{XY-ajL9m=EKwn&M~l zY_a+!zR0@P4IQViE?a8jjVsSUyBR)E)8*iN<3q$3>qeq|f;hMjYK3uiT77@B8{hP4 z*SjmvK~3KXVk0Mk62whBXg;gG`w)Ilu$hpdkEt{Sjzwc$Utw-G?q9$iFMg)&iAF_8 zH<4Eh18GW`$^gtg69Dh2E$KlI3Q=K8$<&U|MXrHqPv3FNu}|uw+m>P>7!fF*<_~nb z(R}Z)T%Mc#x(u#D;y3L=#ej2jj=rAWpTZv$JD$^rV9X6V@{MNmd*Ur@6+8v`z1kI~t@TdB4W{+4 z)QYAAnH`z}Zmy@Ju*Yc?Fm=gnR5(Qcc)#o-WEa6^}P;(OYN_05$V;t#u|p92CZ zDUwM}Qm?r$_#pZbL!d?KKAxrdN)F!dgq?TUZr5ui-<6-Sbjd*_b9a45N4A{}xI0dn z5)m8b{bA+^Gg~Z4R)gd6S!?y15ej*(1)wUJYHz0&xiW8 z7K`k>_c^bdMp1$b?#=bC%~RX~9yghX_IJGt8}`X^^h@C4U9P;>zhABC5;Z>Lp%u`4 zQ?F$(5W_OmAoL9=^zkv^*(qc+jbAIrCU^yR0auzEDYc;|-(?`E`|{Ed6Z!PdIphWN zvBbvECsHoi?RVUP8e%uGQXtw=ox#|WKz%6zOJB-ihPk&-oBRucfDQd1((?Ni@{xb+ zE1R|#l+xmSe%TsW;V{(%WWpcwie#-ntU$-rVT+c;`Y!C&-E($Y1x@K#;?-ktw~qH$ z1iN-U_yUI4P9SH)Ln!!*et)fOD&0QvB0z%rTee%yn>Tb~xeaYhI66_`Fit`U) zv=(srf3Dd`C|tnbsaDH12IE&OuV zc(vucWEDQ?B}mha7Lw;*wK>&H+Dg;Ey9NRDA1W*8(RJmyM~9s?8vf!7u_sJ2!k1~O z&9Si|cCz-15X@^Awo+P~GAk--@3A39pU)O zAGpEzx#2DaNfhJC=s+MRY(TvSG5e=`^4hy2O~LAu5F-$gtxq`g$c!A^ILJXu5q$Q= zHp}5_BU^Qau-vtFCWhvWlBHdxNoiE07$;|{oc@oIEoP_oi-5f%DzKPVjkX~_-47jk z((!OO$MjESV!_YvW2$0^5FJ;?zPuwuD^CbJ$W%c!gO@6j6k&yea<$bUX({~`M5kDA z=W#rsf8TY8q_NT(In#y#Q#p1;5@$!;NP$dWTY4?a3$tyIPV6Fch*o5tfUYCE#P+UC zU_vgSsea|or9WnPv&K?8>sYcfz`<5S*HD<)Q3jkE!6+#q!qSki91bnIsaUPJZ$*qU zfp?6Fn=&5z*rWbPNr%C^c5<;G!LCksxMCz*1W#B!0~GbhH$K>)U*Fw7=n?#SzyJ%3 zNMqQ2nCaEsH`XjIzyBjVZdK6wCKCSH{=4XfLR1KWb{Q`RWz4b#`(%=*%yl!_KoD7zK1Pz zyw-KRpzPf(Jz^Uht9eB>BayP)>dCJGjNP^KnFb$I1-Sfa&ReGz&uaHSGLsdU#{v3J z;+1oBwG;FDQUn)O523W3u&L%-)m7p-J=5K<#1vX+QyUV_!_RVT#lpzc9pn^AP9xjg z-!UbcC9*oXAyor|=76oY@8~(iYV&>-^Oj|G8V(m#9EL_@Qs+WZ5GS^G@KVH3!f=yb zZyZdIK~`xM)>5tqE>wmjaYG7%Ml$MfD;9nTJt~wu#rx0V&xO(O@9~50{t|Ep*7HWA z0XuN7V+?>lPT2<kHWp3 z;W74GvG2iN`P2A7!f4q;W?1NT0$nO5djFxwaIy4uf-_M#@kyshb@UYoBAls23Zhii zsRlEp)Yqctqf-^Zl27-w62a+624$n(XNLLNQ4JUr3Q#dBDMT#dvFZq~4?t2yeJ$?K z@~YE$1Mv6F93j#fFp6$LTU)LFAV9#FIn;s@i@r@M0d67T1}dAVuOcl`?c30=uIY6O ztR9gf=Cx}nk)P%N)-ciJJNVAkn4Mq;b};H?T>Jb+KH~zWKzHMaq^~om`Mf z8T_hofD{f2;CL9~37Dn40N?>=`fZ};a$->O=o3tK?|-Su-d?2kO#g3 z4W7V+Ww`vuZA;v?OpJ=z#VC5i6ksw*or5y!s3chB(09z3L3r55IH(tx=J#?l$HQ%1 zOSGnrivpYFZ9@KP1r=Wq^&9pyzo91crI2mQ4=qH|p4Pg4xT+c3 zeC&M$Ay4-fzefe%jT=!uH{Xz>O&Kj?A1;6fWvm;0+-7OCn&{==sh{Z0f~li(>P2ck zkQ1sSFI}RF?ErD`lO{dpEyP%NDJ!?+MAK|WiAm8s8#V*Q5?R2!0bC$;Mz555%=A-! zpddXe0-*)0C-KE>FPAHU4&moKsD%o9$0}ABVbsHPBcp} z3S(+J3-(@%J&RAw=hM@DB@^=XEg{0pW@XB5&>L#&%p7+cW7mA^Q63lX9uV7xvR@wY z_(s>T`9i|*Sn?iFCS4tUjOU*QNxsat-qE1=}kCvT{O3 zl9K|JOYTMMZMdAaL6SGebJhEfrrs|MueCl7I(WS&f@7APf`d4U!utq$T2v=NfYI&=adJs z%I6Ru?ua-Lm@q1+)H`d_Z*WZa*3Au@>b4e}BfHJT3OgOtgtu)0ARUAt)8< zjXw3r&#f`?Fkd)cLmJK96KI|y0j3?>*nH#m@!r+MQeqO1#U!rM(p^K1WwbMC`0xTn zsTtbEsvlzC1D*7r{EmG5t{4bkdJodmc_vK(=|8h`6+{tL)JUDmI(^mb=eXNN=XNsd zippl)Pdu#6+XJK*-WjCga&UH>C2Si&ClX03s<}a^RLLeP zII^J5F9dQ|OUy=;u5B@cDI#WWJ~|@W=*ue1Ss!G?)QeOoWZzXyk*SiX)Tr@pLP24* zn*uHwYA(m*4o{z9@%x{nalfng!_L@u`p~XRkC@Zdp?-t#<@A7h#w5|LHH%{_lJE*M z$VuLWwHjX%Vv6G!F%~)L6BIUF7(+COr4>(lB{hpNp{a|h)<}3uRp?|BLxnnPsSxdE zrrY@vhmFY2WSgmH3hE|(uwwBl*;NVjD9N6MCNI53@_$hn0z1(%yVP@^c#laj3V^SI zKUGdljMr!m4FSXSrE`gyk4&G{|;-K?Q=k+h$BuV3I-gc zWoXNI<_1|E?IR*9u0RC5|JfQ39Da(C2v4J+9ow_NLxVPham3>rT45x4!vgle+mvZwY%He*4B;#HKtr%^_siUvV&? zP9}fOscHt&wp#zQLrF<|_5+ntp_z}^NFpW#@tik&4?${&1QM_fu6n$p6lvmMuBoI! zLy2R%KC)|~Ubmw;1sG$|AKpz?|D1>?2-mD{bH@wKn146}lfK%l&Y|O4C1E$}rVQK! zELnvDqb3EoR#Ee(xpd1kbsE(SRp^9JmS`=l^b#o+62i;y zAm&il@7nA1XZ8%`>+UxL2H$$vU;sK;Kh*7F)8Gxeg3cTIvg2~%(zP!CkMq?6`1{lS z!zvDkQ!5T;w|8E9d;X3AsL}FbGcYS^xIETr+TdPnMt8AZgFyZz*lia*hQdvG{NCMx z76pB6?b?VcOK53f^%t$Am4Fnez30$ zca;};pAd5*$4V)msR_@aoR0LZUFho9bjn zIY??R4J+QOZQ<;J*2c9*93vazFfobJGt`$oAdo>)Z3xFJYM;Wxnt;*9O&$i^GWRgG z9?+5-%Ky^5=CEnofpI39#FL zX$8zb6V9XEuMPBexYb?$ex)N=8rsf-Jna3fkyXNh-@W}sNgW%Q^oc&=R#Qh3YlAzG zn2`%pG&(N@!joJ^w(o|{)RT1Y&PFE7i2?0EU&3yYd|byF3Jpt#q}~Na>4ujwz=?PD z_vr086)a*xU5C{m9f#;oeJAYfH;4H0v3$qeYG28}C2ut8ekD(~9B(NX zE3M-0C>K(c`I~A#_Y7&_uHE#lO*OJ#SPwl%gM~*-v`O>`NLuRz!z&KS z3gJUz#6+3vJ#aFx2tEil+?+@oe&Pd${XdTwxkTk4$_GKp`?uZK)QA0;?AQuf_8 zTe{r7z%(X7gREIZs>TVQ^YuBc)mwEl!Omi`sVbTWS04Cuupqo|`@r1nx5s_{eEED6 zkm`fCs`tR`d7w|1lo%sWzjXTJ)yUFwLm8Yev z92zLs2gUa^^ltu%k=nZz!SKE4Sue`#Vx1jVQ^hO|Thb@y2`Xz}H>_2MKEMFb)s<^s!HAk?7ak{+i3#y0SF7$5d&@AcsBVjVBTFz* z>tIC_tIwG(-P;7s_BSc3oR>ZK%6i^7tJzPi>o2=z%w7v@H_bWU26ijms@$)q^^s3B z74Sq#`5p}!X++c*<;*1~zw%9y;$-;PV%$PeBFeRI$NuPLD1djO``##jaNhYdaM34* zRR57A(Cgg9vt=BUI*mFwzg9szL`{)ehmg$jxGmyQ+c|i(l0_ZF@Hg*^k5X=X#%1MC z$tDX#hgerXrp=DKZ__7zKeKFF<3zrU&>*K>;oKH&_yuRo!V@WNX4l7M8Aij%;3)u3 zouxf$S?H}hbi}uyK?y<&M30#K7B{9pCU+Bq-Arr+8lMx4Iot~E-I z-#`~~$STVHHo9&I7ic-?c#{$~ZBF6V3WR6>bq}nQ$tvVbRk8@!%g@D7fj?I#saohG}fZO`On$NvK455*0OE*{1G%Z3grG|C^u*m38r-{#A)4Mbl)U==(<>+&= zEZF`J8}!V5f-9TNy-|Uj3$cs<3OP;aY11W(wcW68U^l)7=bAegJsY8p9h;gx*ouYeWMg8b0+HUeyvs^HWx>|OT@4UZ_a3xGKKld~pOY8Mh-fHmQ z+RHBF9$3%1GY%{Nru|G7Xu+JD9*v^Z1s}k~*cc)B!DV7=Z2-usMNN@QulEf%-RlUb zF%gD-%LN%}LhO@9?Bhz-OQoic8Ah%tj;4XnnHj}d*@HLF=q6L)w&2=}^9w&LEk1_` z9|wD2#u6BAT@pL{5~idWczovdg<#~;E!0`Ya#)?}n!`l&6{ zXAwM--mw=A4nXK8RD}K>Im?3mD945(wt8m&Ls%!)my~SFzDXOqSpS7&; zeRN2OMT89}G?le&JHv-^y6@b2o}TmI;B$?OW{g&a6xk zAP|9lv}4>uLDuB@oR~BN!ockpKVZw5kEzh+|F8rwKmm;uZy?wpi(@4n zXmL|3&J|6-V2p{RjsKznS#fv!*t~k+WTh=)yt}cy_)%I|w@etU)}@Oq>%o%QV8SkCBe*0+-UdtxXhLG+ypbbbF*74snT9nYf(u3j><)%2DcgNA&arSs z+v(_ME)J9&qTX2?PC?eR9^o?^Gs~02H76N4G_Zg5+HY3adE*u>2@IKq-tv54l#6sK z!a?6hzUr|h=kz8!%igUge z3q9N(x%L)#`$aLMKE{=a;D`*b7rBQ6yYnDcQSGWKb&GH5^Gp_;N0{m-#^#lu{Oe2W zb7#z8keNLzK*gtNIcu}_@%FYly)-bgvRK3>-P9*ilt6jjQ>NWw;OW1QUfgw##$^txXGH9{@O8(S+W>UwE{n;z>I^Uvq zUHI;$Q5?&$8!NC6Q4{_Yomh9ARMww&mfx+{{L%05pMVyj4RQUkKnz0aW?->`9w+4O z&X)JZ=5>Xg=RS)q4*C&J%9-p1@agtYqI184OjyTXo_a`KuUTO|4~*mx`4jH884-ly zWy445FBu*$OzZ=24jI&KQ!nzM3NMm5*&AbZN^Mb>J%A)lo+8csf|Fk>ppI_O`_UBv zLcwj7!<};8XpQzwg@p_Jj3Cv=ZBdmI&9u`V6S|tWyPh-6>NbcYG^;`5yr1xz-7NXS zj?3(^k#-xcQA1r}h>kMY-*eSx|H9~FSqmbcs+T(e*hH7nvg3};cDONfHf|P8L_%(s z7AoRxt)Kki4l)ifvMRZ{LP{xpji^fGhaU;DJL$!}lg2+xTtE{UCq}CpBTC8+R(bq& zi?y8LjNt$Nl_z6b_EP~lZ<68Ea?hSmoK(BCWOaFWdDPhw=)k98<3s*LA9P?yP47;FpTV|1vC&GyfHoZVD4BW~wO9 zx1iaH04;s^g0CXnSRWSLre>+%Qb(wU4kB?HDm#d6y{(ssGRWTz)N=Zv?WdrFQAPr* z7pR#3c*$>B*zUz4+EDl?%q)b98{;tJDH!Y}_4Y+FM4T{SjZwsLLVYFFR4p_ngLuZ| zbaIu2U+R}zrK)AcOj@K(?#Z8Q&Df9d!4}Ys^eGCd_e_hob+WG`B&Om_W5%@W+9jjC za_uwQld8q9!cFoHiX2~%+!ammc+wQQ5F zl6~gd>NNR3bQs1@-FIa;S~%+cjt2S+*M=;$;g?F z#~Dkm|Lf2m)e@<28^J+Mruw0~hl4yA9b>CpzbXEyiZVT&cP-GDHPgzbp1ufY0xvrz=;m!)A?o%as-thc#faxibKb1y2L6 zsllBi)|l9wc8S=(a@d84c4H)+&^Rm9jIBmK(L>VDK%|T7X=>Jd=&b;l}KFr}%u?wR{zK zV2F0&F0x@W3%yc~baV|Ab~_P9uH_v!!KIV} z@PW(n#ib7(jAUM^_Ez*;BEh#;xjW<}!ZL)>xXcrd*v)j@ymKUmTO`Wkh`lQ9mYe5y>ivMRftcyGpZz`^xhF-7V7N0_ zluQUp)Vyjs0r5z6^eS^|X}K|-HWS(M6W!+3ZL>1xyQ{NTv`;C_R6l*Tr?F>+@Fc5M z!ODsAYK;(JZs{Uav3ifC$QJl&MoU{7`l#S?rlUrpb?)Z6L>|{tYt3eA>3rZ2con5o z;RGsaFXFx@pIv$n&IDb(JBUpjLd#(QxZ)1Wahmx@axAap%qCKpp*?x!(h`I_EEwJ> z`3DEFI&~O*P>8H6Cmz}-B{ZK8+m6@*;(TU_Pgt3geG*Y{0FuAWdS?)mLO(&TJdy>< zSwXX(A6!i*zW83-3_R-eHBXN9W$o`~Y`Fo0E`%0R_U4qR6DsW%Oe7Vi-ay(k;+7 zicai;7BF*^=`SPFJTZPv*B3(~a|5;4rk{{Aj$Ji-^LTh8#xTs{>W8kRxW4jMyDv;R zTp_=-OmBv$&PA<+pZCxC>~VYZPMaj?!fOC?7NLpi%W+Vs-mKfHgvYSrgZ(9;F9PfdUa)MR6litREs$;&nl16+^Ngu&~iaW z6~w;6S0;NuVs3p@^PAe~a8@!lsw?&~-Y+ZG+(we0x}V>|26nubb*K`99jLDs@-byJ zA3$SVk;xk4#<@d{*NCplxK7Cq^$m9+>bpbNXHoQWd1`x?FC_3h%}%#d zul%P^P3mxatRSK;)I=rf2XEe$<)#ye4=qD%dUARh;oEoBc4C>4>q)r=#L6#Ec-+~8 zidok^%`uGxhu>XeS@V-{S3)uFh;&e+$$24fE|qpBmw; ziz&Mc!h&wNHhVN6I|u@uzBo3X@!^tsPXP5YWY(Rtf1old8Tb#ZFgMqZU+KjzWBp1q zd%yWszcunLZbf})^60hSd$cdD-6`nL%Gs)zk&7x@sf)wWg>7bbsTpLn(H|%F#mBFzg)mE@Fw$Qc-4q**XTep?tap z$QU{#&9sgF4>yV{`L&&F?e``vTHLH)rDjSx#~V)8XFTv(B9!f;Z&(xlvhxU(Dsar$ z3yFld?}qNmCVVgtlRRYlfa53UnLtQ0Q5%pzg6F@pS{(@wYdz#H8e>pZRBmOB)90FR zzGAt*-H;(F4t#+!HkI^lm<+{*Dgf5dom&zS2e5^dmmk^i#5#5EJev%`>vgZC@|D2w z{XxcWzZ%0|_2PfsGb#2Gq88FjGmnKq411s%E>CA#a=#3m*3B67cD3Iz3R1ax33(P` zgViI>Ts7!7h-zS}qzP;@HOw8y5q7u=KK0&+T)x`2GuuBdfuCEggG-KWSsM5 zeYRP{ls{e@`K6h0-$5(E{HK{3)`$Q(imR}Po}4#MLI$*-FR=bvl%6>xzn*AQ89_Zu z=ex$X_(G^V1baI2yWj9`-TCMFuWQ`&B@g0cpbF3vD^&W&;;J(tp57hY@?}7(!9}}v z?Ba>j5Y6y*{IKpgfpASxr`q<=me%ZOBu4dm^8y`Rk*+DinEv+OR z5S`-$stCvUoH=l6IZlwYFDtZ7S`*nI2wkibg*xNVMvdz~8@-s^t!r6Z->eHZX^ z@ORXhq0IvutVBo(Kf7Nkqar# z&CVXoYEB%9WH6cw#;SWfE~(!tF99&Lh)_|2_kh@%>gPditZ(HvV*wDgLw61LeSSsF zJ`Ww^I)7Lh{CdX9u7pqXgNX#PM9N9Bc~9jkqh zV&x7?UJg_Bh~QKJ?g1%JTvrY<;PrHnaiP4bjyw~VkKk5y?641(EcWYBl=BBOeGO|P zSZo*jfvWQ@Mv1biLY}KFA_OQ&5PNF_9x;0Feh;JB?aOaAHPew@2cd;s04xbXlOE5- zSX{%>h*>c-1r5SPx+71oq%QJ$CoqEIhEA#Rnf`c;o={2M(Yo zj!_>yjw~*q3riGQVZgbDZ1V3JZ?FU=#o3>V8WV|Ft2h^w=}}M3P;A~zxpfQ0rmalu z*v`zk7qId2C$izfOE5dO3;k&*r$I`r`Z5Uum1l1(j`b-39iCwCm;O=reBdt_eC7YJ zSr^$n^9>*-KFUdp(qTQIGf>WEG#S?xchMShGrGeK604-Z@uSFiO+ss5Z5mh~X7>@P zm`rpz#{a$INoS`Psv3_yB}qI^Ncz;l!N~Y49Yc?$zziE9XN4oSvpPI|Ea*$jlTnyP4@-m$BiZ%P?EFIcB6k>5fZ$7<9}? z>}EMuT^fL|+9u9rxQ*2+J=NyI%N^z4ZW1_*V-OaPE&>1La zGgb}%ZD|WKpNu3rCHuLV#3|o}bD4-A)SDtZ&9^_g*HeSGUU9k?S>e1!7LKRpg@5rp z6VnofpivOmVRdBpfib8_LP;l*P0U(IkxyY7gGXRmMmN1mWY#)^MXBqWo~_wnERzhG zyY*)7zU3C~|JeI<=L=rKu5WxfQy1c56O=MAWiuS7O}S_sjaeht@JW z17$5BGY-Z*s9~rd@usx45q-{~-inZ=xX)rq+cCsC&REFat=M-YjY#r&dnHN3&$P;M zXZz1JGwz6`;ihW3E}BQ$b#iTJ(k+<+Yg{Bnh~RJ%v+>E#)}1eWAse6cH2T|jNzt2j*9E9;9dm|+ zgl6Gd)aoLuRIPLCO5vU}<25CHZ@6o2*-PV@4s2=65Gz5C!dzriAYx>Za{*eP;xt{qu?_lH792XW9Q-w0D2Gm+p6yjE$(DVoRoj_ol{){Vpv1OE~XjnE}Z zGn!zG^rRhnJTfe1#!05qWOT`{%WFcS!6>~xYD$#a-)_z9!O^fR3Z%%(8LzhW4uI2m z*xI>!RH<=;vathZ49prB?%Tus1AocxPkjWt<6JgfbFFTE%G25O#A}&6_af=T ze75sy(}W_Bes?k5K-6bxG-~`3S>#_}!F={3&)68xl+<2NVxqfPDMXz3))=rubnys- z1AFzv?YD6BYd3J>=If~MxeGaRgiRB|j((rXUP(2qeVDb0QHG-G4dQXa`$i+4?yK10 zaYARHtOfW%Bj-BOtS4g-Bx4rPrUBv&606Ovq(@(3XBYR`NFUpt8$WU>Tj)T8ZbaJK z8{?u8ZLOPcD{F(`vg$_2Q-ngjm|l;f=#6o{wLf=_A0o}v zx-7l1&BP~b13;0wrl*#z{Q^5&WO(0hhWFgb{*V3*dp2&t?7To{uDG0;i!W#9;!Bu3 z?*jTe&J{DW89d99at4sxasGo<+jt%#b1C5%t#^+Kg)u%Mq@^^5(%_#(>)b`v0`;*Y z3=Zwnh5PQ}`0iUde*4#`?!O=Q9iX?g$P7bfrwW+uVZ|`8FsN#zD2Zhg8i^!ViWBUx z9?%&mXF0}&@s2#e+B8z)cZ`0g);brD{~}2?a?_@@m%Iw0W=9i6Dy`0@MIn@q=L1aR*@ZlVJ1?QapZPx_~MpMZ95ZI;;rvMR5 zuLn}N5K%R)`z#B>DoU3JWyMIYN6xCPMZ~-^s)#0crZLipGE=YouKlxP)nY^tW9Ux` z6BKL`0V;-b2dHj2!17Hua=h$uxa?C+O;b%wAk)*B4I6ZF`wk|y?qG7;c8Zx<%+xgW zdzfAiDGFyCRW)|7M7=P_@Wct0j~-=k{20SyhpFd|Qy)KpEY2efi}VHqior6IqRe2~ z+;drE%~TIitO~Wo86|*7>8cuA5HbEa7GKn<<%J#z0vNr2I`!DvMQ5O#_3+ef?y^yB z`aDc9Qo=eT97u%LZpLDw65lEG@tGpSPLk6l`MuqjX*MQNq&nKc#C3gmaU**^8K zpyUf=!eGq?z6KE!Vv-~q7^4N#b!n#rRf@7ijf==Y@B>K}%@wYTF<=TGfz{z;aJQn5 zo&jQ8wqyFUXG~BqPW*~z7X;6~sHHGMuP{va3ycVLJ-{vvsTUPJ;?l`mF<1<9rof6| zO@S5MUnYVUwHRwDR57ab1*TETLNIC(QH&AFvTzYxK6{#qP+Ln?ThyovQ#ZzM+hD{Q zWzIEdF)Gmz@lsr@T5Pjd8)>@^Yag9~ayBD5uKnlM!<@NW;{PduMROB?s>N27lhZ1M z^!dRhqa+QAl8ln1R>?z5$;Ytfo&u555{_uA+X)VVv`|Axl6jC)=2$CvTjWg?3>X(W zX>DhqoHC4|R${b9gwk-wsgXQnie%At!^v0#{f^qUW!+!`GnqFa)HX%DsV_-jtz%Ub zpsro|c~bV*LR{+ni2{sK&>9iz55tLGB`}NO`4WXf;_tCcsD!ln?)!laWe~#g`#wWb zi7@dY=P5UepF~+nbEx>VOLE6%U0Qb+tv_@I%2|z3W28AOr%$<)#YGOZ3& zo-Hvb#)k{W-DDFy!BqQlA%L5cz?icDJsjUq@<-(-Bm$EdLxH<$gt-r-zhDT*e#Qd^W_ zHf;OnB^U~YzsCVFF=ahQd^C{6t*W}>dup8_Vm!;`!jJQ-qNHd#gVwb(Q2p~`Bll_I zvBkc{NEG~r=bx}m&z`ieedv$;VxCP^xBV95+hP5oGf>WEWWg;eG?RK}onB;8yjZ~b zzH1H6k5C}Yb$o`Q?-Gr*7@t)#%$!UeT46}jvre%7W7$91wC(H z$|TzmT_)tS=%sj&yvA-10+pEWaRk5rc{%=CWbNRg(kCo7*cJZ1}U5sDJ5%-?#@|1=nRyz z8Tr#JVK$S`a`T@hJ@aZCLQxkg3R3i#+;KkpOvzHc%%o8YwXOq+n{;d0==yO7(k$Gi z(V%H$o6Tc@8sd_`%n0g|VMFuO{BaUzTUIpA$6VFcpo2)!57LY+2BR$7iowJL{asgN z8|^S2elRIQ?WQoW_(3pgP5eCCBm*T4U2S1=8|zf!7*9JFtS45gX~-otvbW>i^PFk3 zf@UiiF2)=62LF86It}=_7fuAkxN6nGpbyWDxBiXaZf%uFrjCeLiO+m~j+*@#Dp|)q zAs8P1e41LaF&g&CNKw4DFg49a+K;#d>bB@{={J2VbJu@? zd%y5`HVo#Vs$-@}aUldrb0WqMRB1c-PN=Os2*j`k?TNO|AwQl*8RgJvW=;cfuBF0v z(ptFf7Quuvsnc^Et$8zQ62Rns@Y)}6u0Z3= z#+3qjItC>)L}rwzANV74B8k;>3n40_cWZ+HY??FS{YdR3KLIchH85aPVL$>?)fbhFFGz_hoZYmhpzxWCAlvn>d-Sd>cr#ko`^6thH)X$-IB=N!sf%GP5bB?e4G={B<%#tY!?3IU%&2CaOLPmPZCE z8S}0KxM&%x&h=s>up~ib+MHxaKH$f1{dUG}JDV&tm$HNmHjON@zJJI`ErWOK_ z=rpc%PKOB1UAC0<(VOI+b?c2E$&56?RZ%vRlfEWU`;0pzNaF=)Qdeg&9s>hqC6#`A z20!lmM}zi0AL(}GQLcabkattYNMq&{OcV+F6W_O)pye>5BXgJ$m6tzBF1qj%7%aP9 z!>XTMWl58QF=AarKqe#^jr1TfI!QDe43VZghg=-z3!%9zR{28l?9xv1?32OoV~oq| zqP07=vImpXQ1n(5advQMJiSG#o*P;{L}EBr&ASXWT~BGa@DgtlMbD@E1Wz-ZtH!y- zswN_LJvbEa$xNTlDbz|Ckwso{yTV z)FLJe`;D1nJC0Sk|&7LtJB{Osf>BO)ZQ#LYN4+cv<&uHv6 z7r7?gOzyG@XA|5^6vDh!CN8>?jZb?nNP$88JVm1R>CAnKPpyy1&p(q5yLaJwdv~_= zTA7JE^`fm`b>ZM(b>Q@ zHk$P8M>4!ZmuVX_z}0c)405rb@GhJ%OUZ}uxQsgAaUlg1#YwiD3V`_?G(WF&Ao24G z?1qLgL?5P@#~p`3FKg!Di#Ia9OM3WsI{xo0NeFsOs_&Ih?PNH>1;m8FqMsK-M@>zBJCmPOOMi^ot zyB-q7&S_xG44Y^jNw#<}lp0%YoLJC|G!_U0E7@J9;aI$JVx3!Ov9>I3*v#cW@FTGO zVhJ*`#2}p+cTRJVZ(rS$tQ)mkSwtKmFOEhra2DGa!Rk`}cUVrz8%7}bg6 zI$WFwW02k?{jJ-in4Ct+K31L5$V%b#J=R5e2~c|G&17}88eY06pibJTuh%sw;|=Gn zrYJ(vj4>HW4K9sz#Ha3TN#e*jJB}MzeW+t9kcy48f5p3);;v?qWpNq}k4)?Nfc9e1&G)Cil2c zB;`N1AC9K@?o7KrXA@Sq`;9?;3iJ^29WGAjuoR2U*NM-PwD!;(KG!ou3^h=kccnb} zm;a;Q{6GE!hraeTwoQ~2bxll#FG0LY7CF^6CIj=4bCkK!DUHW1*)@SJZ*p7tGv!zp zzc)?&WOLe@43be^G`(*QfWh_bkHi=jt>v&WT=IRdV)h%qQ;;$aj=6*DOn^K_6oahr zg%Vt;7Ez@%2Be0CBP`v2x6a*tC&%u-m+Ig{EIoKX^`ZUf;yl(E>d6_VcU_>9JI-V3 z{PXE;+s4eUOPRjpDk(N?15;vA&(6rI$CHXT%GaG$DHBs-(?Ctr#naH;XKu?dSfm?t z6+K7GhdDZ!wY3PX)mwe1j@x`rD>ju7E%mrG;`l>H8|Ql~UWGJsIn%mWR|dVQ(1^#Y zA-qqSP-U=i99f=2*g4kK6`L?`O-Yye2}cRf}@XlIL1t57`Q+47|S^1t-9-}qJb zU;kw`6$KOG^j#sTq7;VOst=_nF;tNp$(;pb7|HMi=Hy1^ABs<=cPlg|ahdExTYF3E zX&f)q>q(qZ;xsj}MwV!nVb;5QjBvcFI9wF$dgYIC{wse{=uM~e0nOgLn0@7_&u>E~0l?DnkP`2^@@ZC~K zM;)8SB=abASWP0O<)|Jo`3bVbFbyjEAl{?cB=ia?O z<>XXzF!J|82t8F*=Qfn_rspWm?!o2JBAmGq0V%Mmlvh1VuKAVU()-`>CJub;Z`d$A z#;gbhE|SJt>qF_opavn@Con9L;mUL<(sHvkh8w?5?z+*;IB7EF*>oeDz(SgkL?xMI zY3>EEZnhpHbu1WDAYvF|IWo7*!p?KJ>{UO@h5zJ7gqiKm43RNo&>3>y1ifbS=u+FS z#`ZYlRHK_tyz9+U`}#)~u-N@~>b}4G2oHVcQ!L$n8@;24nH6QD0aFy%i2{)l^??dj z-1RIKdPV73b%Ro4PaI)z^HEORbUphgCMhnyn4K^BWWOA7;ZdUMx23r*Um$Y}<5rc*qAMU!_>2C{?Y~L%12es{y06b?I;vie4Wvh2&5C z%FlhixAlwA9&R`i%=uT!MgQ(s*!;|A>3#4010KBT2DX)!siL3=spMLOWNR1x5 zSNHt&`+49)e}nA4i`i_kBO++^Z#)PH2t&s_pyg>%yQYVwb4yyHyW->2nN^9E9Nz zu@*|>25J*>iCR+1P;cGFnQ%toE5vN2`AG$u-koi_(Aj^DK`zXwv+qVPGa2pdXZk@) z6T@S}ZxUTNB7&49V)_)l9%iyff9HA3KKa=qTX)6!7H5*hK7#E22*#Kz^q1a`v=z!E zHCqQu)cYQ!s_v(TW-+kPbs5{(kw8k=Cf+xl??Ib~7N-UTZA>~gAj^aqXkoWKj5))WYoZz#t&Y&GuH46(% zEY-@=maR-b>seg=9pA^c7rt0z<8~|sh@qVYVdeYfbUy1%Eu_1GhCF}GIF==zk)*niME(T3nn$e>Q z_Ku6ja+H=J)J9mEo?zp1zEQ9Esb9qGdZJisy-zN=ea={O#cK5CjBQ_UPz2#!NkxIu z>w#htH5=Q}8>Cs$F9|$y;~J&?g}XrFGT}IE+-9tm6O2(!YsZI5T?ecZzB35IOradS z|32n!x`8d1JSpwDnS|kVUx~92X)p?%U2~vw6`)azSkEl_NrAy#T~#n;-xU?qFyJ;P zArg(+&O~V-yM4vq^VWjdb)}s9&whb(zVT&xCa*Ni(kg%HP05d>;!d!VG$GWzwzO&4d=)igC={Oxy)9q=pEeWwW~C3 zl=_|<^wu}NmZP8gG@AyCY??AqyUCe`mF|#&wfM&#qxofu&~*=T7#R zzjrsc{`#-7^M>p6(w}-Y=G-TU^-*1kC(~Ng`+(9H-Aa}rO&WydID4WRMm#8 zanrz+PO;&f932*vW+Fr#9XLk4S#kFe*0UF`bNUq*Tp{`^9CT-)M${3))L;r1OA*v*NPuYn^Wh%Gr>@RUGGzR(s`Ri-*NZG4+zYmjmVFvf z&!jHv>_!@_Tpkotn9OA=*|oM9e_c(l??RGWiJ~NXuEh_0db%E)GZ4X50K~diP6%62 z%LK)xPnE5gUdxu3e=oMeb3O@-5wXVYSKr%xF zNqMhAIBPIuLSL!zbJpug>h;jtO&g5(e%fNB@Zsi`;jm&b5QZw~>;}xG=P~(&t2po3 z&t>~Fo`>wXKDgcz7N0k(Eg^XPQP%Uvb>ZIK%=Qe%z%Yb0NgDje#bP8|V9d-_T1#td zi5TxIarwq@*M1@xX9lzfq%ljxO*M(Q;Hm&;i@R_uOHnA3)iQ^^b|V)YJ%pTdffshU z^+L)UPs*Nl8$Lf{QJ*DNNdDfe4u|-0C0{Qj@QLdB@T)fWAg!I|wRD>1YGzc#Yd$6m zM${#>J=>vW!(q5(d}R(N%Ct4QxDZ`=()NXSc=QOqaa{36@p(|xT3mt-H`8_60)N#l zE_B@|02EffE}d~Q9?|fZG*qi8s8q2QdYdj{(@QRpP0#yA^vFZHc>C=}L3JUL<3Q4Pt zABb0WkK_k?HXx<4NayL86PAy0-ygkQA9&YW*|dCu4HE{j6_xkTxab~}ddNIdE(^8I z%*=Q%h52Q(v{@KzHAaUKCV19Tpls_Kj(+v4T=%R0kthGl|Ddxke7RVQd$u*Od|w|9 ztE>l$*rjJfgH>n=pN+>An<5z;;Wb$f$?MbBTfqv( zP2*FxuCzWkDK2+|xZ-xHTvnpJYE^nx>7gvo%~LNeXxXZ6mF9wH#)Hua^s6hP&V!~w z5?UaSXbQ}fX4lPKi`?R0;RehKoi(Z7ruCjSky7p_+osu)dGe@qPf75_#jOg~4V0SG z*{AzTtPQlUc5umH3QXB+d@m~QFXRsrD0)3obd@wWoKHwrar3TkxK15r8sY!OrLiRm%ivbx%l`2miFJT^Y`7!{2jM4f8RZv*t?Ip<42I=Cy=ED%<>}T zu%b|<6rm6(jMEpmEYvn;8ug)Bu73$xNqv#fA<6)#q@?aoQEk{jwQ-hW+jcfwd@0ix zUCPYG7c+J4#q`g+P-JE+6q9aUtEhEe3K2tD7O@~;U}}tShNNp*3-FYopY*$EEDH-q zxc&97(Sz@M7dwiINu$(N?Ir`p_~%NpuFy0M6p!f6$*63VY}bCNW<~O3FsZk2tN^u} z1a9dS6nh`!=Ku98JaMq3n_u!hqPCBQz?rYpys)IUNL`_Ig{>Qv@TV3=|2QeM+SMZWvFD12rLfi& z?jnS_Nu1d$npN^Voya?RmtoM|IwsnHnv3Sd&4%L|63m%o90Uf^wUdmDG3i3B#J}bO z$fg0(u;O!LBqsVw4uP~&@B7RW1;uT4;@HO=jcb)XdzgZfQFqE#`b0F+AAxm3hZY`%c0%@@e@mCs`P3+J#4 z$EfE{Xno=+OGgf~cwi4p`}QzAc!28gVTLD;Q=d4243^R1(DnV*z(suE7#ECj`zlJL z*T+mvQp{{XHg3Rd-O9|i^O)LoF%vt^XL856lpD87F}u;#xiSS-1uG^_K%+iFoY57k zD7$m^J!?!7N`pv08_kpvhQZq?RxO1nEFNX|YyPJm`HMf}oYGR(18QFlT*Y|Ty`Ow$ zxyeRl24fiNQOepshT0fMLsP4DLS`o|@@$r^*<^(B-~n#=-@nGymFnhi`Cd^iQA09j zA7_+C1Y@fdEMu+L;biWC=++>O^06VZjx2uN%sLuacvWpLnzdKH zJwdy`->5!a)3ef~VVXx3?upoL@rs5GH<-1=IF6Yyg;_^y2jbK8pRJwOS%FA~DyAsu zO-?b;+&BzXZvB%acmrjeK46?dvQkg5p5gnJOv4lE&wyuseN#Ht!;4=<7}^@4fQd~o zu?g9NObAS>vPEmO8luAiy0k#GIH!a8c|uikTMwzKA!v;t6h#42L^8~nlA`R>o1BEn zX)#mNn8_KWobc`ks1=sgxypm{yYT&HK5q}Yvml~w)&b3R7Nh;_nQYePebJbCxW85w z(HiW4J-_!_J@Eebux+xSr)ZbRH0DI1|!I^qulko|C=Xm-lUVyd8t?xw2_A6@w+mms-y41Y_(y%hP)uO2ZEf+ z&*rtk%c5BmX5(8PytabvH+q_y@5#R(wTZ~aMY5!Pt*hcMG-;4_#*I*`Gt5-o`{sD5 zlW(t1`%6?f}PbhiY*W9SHM^uuT@jvB4>cyX0b27M)e zN}J&d24PN-#hH0z*_{3-XGTKuFGCQ4DMlSFY{cbCF;i2FQe&s}QmhHYX3QS=m=nPi znFG;XAu+!1FoG39Ci*m!Jju!%?yxFy=XywR7Abkn3cva1s5B-{t|HqQXnP7Uedy1i zGn)~urLRy%VQM}>X-C&JwGYl9G8qG#a;F%?Ck6{ltdOSyu$ovm>%Xu4B*a9iwM3!t zC_j7V-9L@QrdjK|zASZ9wOX(>Ml6Tl_lJ7#AN>(KN};e7wZSuw5+`d$vI(e2Hh$(+ zt8-*&=p~D!9CJT~ZzRqR-Di_A?jg)ut!1_ls)Gl)?X9oj$>*Q1#jb0_S~Dj6__0T{ zor1zbM0|F`7KOfoKd{MCNMtACuZ>1Xjr7RN-8XO2MxYV7BOra>{G0~pVBg2ktnfg) z(J-FXsChQ}-Pa$k({k6UWao2UNPY3899$f-WNYf$qFP~Xg=$4g!k7NSZ*e(WVYQ;x zirTvGY(2!*71q{R?+R4wUa@ujQ(J+p(Co9?AzBZ?FRNAMv@*8x-&Gh}W3)zWjnwY5 zXpP$1Wk*y;1J`PquWF7@^qIW)s^2zrjuasJq|#CblzV zqQpmQN%F0OkBkb}Iy-eCGL~8TvFx`M4ovc#Wi$rKJROI`eTI8M&;pQ}jeUXJZsqZ92!#TjpE>?p-q!Oam|BWbb}ZM8hoM@MhI5t3sBV`M=(W}Vy{ z;n|X;vXHS`3J;ADh;1&0aq97mc+J;-Trbe-5TA#_(7XJ}a`i9&ir)GAZ{x(h_hJSW zWv@m}h4E7#H`}p((04Rl9+wn-a*dg9=1a9fpjsz)85zsw>W(JYj%3$c-F@a#>D!o0 z&#_q=4M$6XF@h-sYYLPSwL(=GhMPBW;kUnn&Chzl>9!8nB${lbqLPWK8|4ICcHfk% z!spy5dN8hU7)g(Px}Te~2P1=>cb(?*v~Ud!62ptbk0#m&u0QX2je&ns6H^xUlYNe& zH2+OQqt?(^XkS)GwIP!SI27B(UCJNLS<_TYB;`+ zyWa8!W{w_asxMUjd}2C}aBW*7V(zI(qi~X7G$!oQ8ciF`mHZaX`g+nFQhnHX@bT5w zVsywRDLMR+53}_t&(e)A{}FK-)W?@aDP8=#2J_rEs!hQY1nXED{Yo61vI~_rL;h1E z_$l|rwANhBP+5)K8}BFz{t9V)+g?hu3u=XwI@+W(|0m;)P7fqgybDUn^fO;7PrBqP zjz9Q-+Qk8738qkQpcvGI*^MGTIwNI|O>Ncm zVQD|jJvI99))MWXLT;Or!?P`8+NZ7YJ3G*9Qw_`p?XkvXZ$0?Y4{3eV4Q%UKCnKOl zO$uCSPKrm}TuEqThPI_)>|~22jl%ew zVB~VdWk=1M%$u5Kfbk{@>nE!*EaNl|p79A~9m)66Jmk}j{5Y&n314ixh|OCsl&Y>{ zSeXP$NoWX_Y2U=TJ8s1CZnkbLOh)d%CSLpeH)-7$!OyChC6vzzB5fUo%HAL<``8fX zmYCyFnl-zdLn>)9s@$!2vK$K|jj;ZpsU_7LqfG-voCXD=%V%7dy1o#YrPa?pn1Z(T zU3{{eSh--CaYzX3TsM|5HjTp%NesA zvckJ`Ke+uj{RP>Q(wb<8{#g?PmoqfsS+{xEyR}Rj!?D|M<@o15#OCjNHFaHMiX!z3 zSp*trWXw!sf4OnKI-6RKb6_QxNwc$Y8WSIi)-W7GBcvmU&7m3HpEp1zdCD(t;{de% zWqb70%)Q_Zl6GiuPX2_#cVlR@2C|vpxT(RJ^6-z&kOb?iGD{&*)EaNsXdwEl(4y&q zI8#m`f7;kb3R_NOVK6TOMVzOq&Ta|6-E{C7BigtJY-4`_MZ9lJOPSuj7`bs8E?+yz ztd3c)gN}a8JI-;`eSR-wy0!5`^IA1l5??6USEn{_935<|x_l(Yck)44I=V)O(+$nW zuv8Kx^@T1J9~ZUr;lHB3^EReRu(t5|MB45c&72FNOhMe)*o)u3Gi2n4@r+ekE#<;e z@DZ???;uR#MP1=nBlK;>-oO7Sn_luyFq_Y7)$YpMCpJ#%m$Tzay~WBX$vT#ih8Ss_ zV5GQ>=Bduy$z_+WG4NzFQ*H74NEwE|_S^Y7|2y_fFQ%jqfuwm=Q>N938Vh**3Jf ze&cx3O_y#LjXrIS-)Eiv80sW9frC+!W(dR?Mw4yBvl=l+#!-8%b@PfcF;b`Tc_r6LQ;6B&bV2g8Eg?UtORDFepAs1E{+b^6 z=m*(k2Ve(iQTk{oTH|w=R5cYQJlyeU)JkdmlubD^=GuMM5vC>GZ5BY4iNbK`wp*F| z!Y6dY%YQ_ywOQD4Gn;MhjB3ywr^H|f1|`p;)g~6ALEvdiRZNi-+@g_*CukBvo2KbA zKJl0MDo5!73o~-fX8NI%l>9YhtqiWFaG<6w;Z5{&vY0eZqeFS><_+L)te90Vr>a-7 z@Xpj3p)?qbaIEotV3ZmL$vFC|{W-twT5cPGRO`FFz8SlnD6VRC2GX}4uO~$6HpUsK%HWDPzGuz z&VW`JpIlNVu%%F0WjLr1TqR90E(@zMPV}gf)Y|Ih5Ai%>^~+{T=U@lL%dP1{@QIl) z1x9gsKEm(#-~Gl4ag{N#AW-&ul!Y7ca&>ZYFJxaOf6-ZpQG+8iSIl9Ou?md~yyEsw z@?KV2{^Z-ti(hH`4wu};lbx2V-fi2Ub5V7*E~0kOu}D^3Z)aK++DC`+#_hv#?zrwyG2$kGs( zjtC5+gpv=Qf!E-wQ+fjW&^>zWA3x2MUB;RkYl_H@MU9sMHZJygIWRdMy(QCvLonWpth&4$b&a*w-ze&$L8<}E58Y31{wNy}3O`Fpmpu0=`J+5b z#D1x>8Z{om_l=(=|Lyc6+_)MUo#w1OJXOM}4{fd#fiG3Ccz{D+_>3O9={mMu`6RZz z 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�|!%2XD}Xi?%j3D8cj< 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 Date: Thu, 8 Jan 2026 08:21:10 +0700 Subject: [PATCH 02/11] chore: add logo to docs.rs and README --- README.md | 1 + .../src/entity/parse/entity.rs | 2 + .../src/entity/parse/entity/index.rs | 238 ++++++++++ .../src/entity/parse/field.rs | 156 ++++++- .../src/entity/parse/field/column.rs | 427 ++++++++++++++++++ .../src/entity/parse/field/storage.rs | 48 +- crates/entity-derive/src/lib.rs | 4 +- 7 files changed, 861 insertions(+), 15 deletions(-) create mode 100644 crates/entity-derive-impl/src/entity/parse/entity/index.rs create mode 100644 crates/entity-derive-impl/src/entity/parse/field/column.rs diff --git a/README.md b/README.md index cd83d0e..5feb447 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@

+ entity-derive logo

entity-derive

One macro to rule them all diff --git a/crates/entity-derive-impl/src/entity/parse/entity.rs b/crates/entity-derive-impl/src/entity/parse/entity.rs index 5d100d0..6de7068 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity.rs @@ -142,10 +142,12 @@ mod attrs; mod constructor; mod def; mod helpers; +mod index; mod projection; pub use attrs::EntityAttrs; pub use def::EntityDef; +pub use index::{CompositeIndexDef, parse_index_meta}; pub use projection::{ProjectionDef, parse_projection_attrs}; #[cfg(test)] diff --git a/crates/entity-derive-impl/src/entity/parse/entity/index.rs b/crates/entity-derive-impl/src/entity/parse/entity/index.rs new file mode 100644 index 0000000..72e9867 --- /dev/null +++ b/crates/entity-derive-impl/src/entity/parse/entity/index.rs @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! Composite index definitions for entity-level indexes. +//! +//! Parsed from `#[entity(index(...))]` and `#[entity(unique_index(...))]` attributes. +//! +//! # Examples +//! +//! ```rust,ignore +//! #[entity( +//! table = "users", +//! index(name, email), // Default btree composite +//! index(type = "gin", tags), // GIN index +//! unique_index(tenant_id, email), // Unique composite +//! index(name = "idx_custom", status), // Named index +//! index(status, where = "active = true") // Partial index +//! )] +//! pub struct User { ... } +//! ``` + +use crate::entity::parse::field::IndexType; + +/// Composite index definition from entity-level attributes. +/// +/// Represents an index spanning one or more columns. +#[derive(Debug, Clone)] +pub struct CompositeIndexDef { + /// Index name. Auto-generated if not specified. + /// + /// Format: `idx_{table}_{col1}_{col2}_...` + pub name: Option, + + /// Column names included in the index. + pub columns: Vec, + + /// Index type (btree, hash, gin, gist, brin). + pub index_type: IndexType, + + /// Whether this is a unique index. + pub unique: bool, + + /// WHERE clause for partial index (raw SQL). + /// + /// Example: `"active = true"`, `"deleted_at IS NULL"` + pub where_clause: Option +} + +impl CompositeIndexDef { + /// Create a new non-unique btree index. + #[must_use] + pub fn new(columns: Vec) -> Self { + Self { + name: None, + columns, + index_type: IndexType::default(), + unique: false, + where_clause: None + } + } + + /// Create a new unique btree index. + #[must_use] + pub fn unique(columns: Vec) -> Self { + Self { + name: None, + columns, + index_type: IndexType::default(), + unique: true, + where_clause: None + } + } + + /// Set the index name. + #[must_use] + pub fn with_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Set the index type. + #[must_use] + pub fn with_type(mut self, index_type: IndexType) -> Self { + self.index_type = index_type; + self + } + + /// Set the WHERE clause for partial index. + #[must_use] + pub fn with_where(mut self, where_clause: String) -> Self { + self.where_clause = Some(where_clause); + self + } + + /// Generate the default index name. + /// + /// Format: `idx_{table}_{col1}_{col2}_...` + #[must_use] + pub fn default_name(&self, table: &str) -> String { + format!("idx_{}_{}", table, self.columns.join("_")) + } + + /// Get the index name, using default if not set. + #[must_use] + pub fn name_or_default(&self, table: &str) -> String { + self.name + .clone() + .unwrap_or_else(|| self.default_name(table)) + } + + /// Check if this is a partial index. + #[must_use] + pub fn is_partial(&self) -> bool { + self.where_clause.is_some() + } +} + +/// Parse index attributes from entity-level meta list. +/// +/// Handles both `index(...)` and `unique_index(...)` forms. +/// +/// # Syntax +/// +/// ```text +/// index(col1, col2, ...) +/// index(type = "gin", col1, col2) +/// index(name = "idx_name", col1, col2) +/// index(col1, where = "condition") +/// unique_index(col1, col2) +/// ``` +pub fn parse_index_meta( + meta: syn::meta::ParseNestedMeta<'_>, + unique: bool +) -> syn::Result { + let mut columns = Vec::new(); + let mut name = None; + let mut index_type = IndexType::default(); + let mut where_clause = None; + + meta.parse_nested_meta(|nested| { + if nested.path.is_ident("type") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + index_type = IndexType::from_str(&value.value()).unwrap_or_default(); + } else if nested.path.is_ident("name") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + name = Some(value.value()); + } else if nested.path.is_ident("where") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + where_clause = Some(value.value()); + } else { + // Assume it's a column name + let col = nested + .path + .get_ident() + .map(|i| i.to_string()) + .ok_or_else(|| nested.error("expected column name"))?; + columns.push(col); + } + Ok(()) + })?; + + if columns.is_empty() { + return Err(meta.error("index must have at least one column")); + } + + Ok(CompositeIndexDef { + name, + columns, + index_type, + unique, + where_clause + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_creates_btree_index() { + let idx = CompositeIndexDef::new(vec!["name".to_string(), "email".to_string()]); + assert_eq!(idx.columns, vec!["name", "email"]); + assert_eq!(idx.index_type, IndexType::BTree); + assert!(!idx.unique); + assert!(idx.name.is_none()); + assert!(idx.where_clause.is_none()); + } + + #[test] + fn unique_creates_unique_index() { + let idx = CompositeIndexDef::unique(vec!["tenant_id".to_string(), "email".to_string()]); + assert!(idx.unique); + assert_eq!(idx.index_type, IndexType::BTree); + } + + #[test] + fn with_name_sets_name() { + let idx = + CompositeIndexDef::new(vec!["status".to_string()]).with_name("idx_custom".to_string()); + assert_eq!(idx.name, Some("idx_custom".to_string())); + } + + #[test] + fn with_type_sets_type() { + let idx = CompositeIndexDef::new(vec!["tags".to_string()]).with_type(IndexType::Gin); + assert_eq!(idx.index_type, IndexType::Gin); + } + + #[test] + fn with_where_sets_partial() { + let idx = CompositeIndexDef::new(vec!["status".to_string()]) + .with_where("active = true".to_string()); + assert!(idx.is_partial()); + assert_eq!(idx.where_clause, Some("active = true".to_string())); + } + + #[test] + fn default_name_format() { + let idx = CompositeIndexDef::new(vec!["name".to_string(), "email".to_string()]); + assert_eq!(idx.default_name("users"), "idx_users_name_email"); + } + + #[test] + fn name_or_default_uses_custom() { + let idx = + CompositeIndexDef::new(vec!["status".to_string()]).with_name("my_idx".to_string()); + assert_eq!(idx.name_or_default("users"), "my_idx"); + } + + #[test] + fn name_or_default_uses_generated() { + let idx = CompositeIndexDef::new(vec!["status".to_string()]); + assert_eq!(idx.name_or_default("users"), "idx_users_status"); + } +} diff --git a/crates/entity-derive-impl/src/entity/parse/field.rs b/crates/entity-derive-impl/src/entity/parse/field.rs index 73de35d..40c75f2 100644 --- a/crates/entity-derive-impl/src/entity/parse/field.rs +++ b/crates/entity-derive-impl/src/entity/parse/field.rs @@ -26,12 +26,14 @@ //! pub user_id: Uuid, //! ``` +mod column; mod example; mod expose; mod filter; mod storage; mod validation; +pub use column::{ColumnConfig, IndexType, ReferentialAction}; pub use example::ExampleValue; pub use expose::ExposeConfig; pub use filter::{FilterConfig, FilterType}; @@ -41,11 +43,31 @@ pub use validation::ValidationConfig; use crate::utils::docs::extract_doc_comments; -/// Parse `#[belongs_to(EntityName)]` attribute. +/// Parse `#[belongs_to(EntityName)]` or `#[belongs_to(EntityName, on_delete = "cascade")]`. /// -/// Extracts the entity identifier from the attribute. -fn parse_belongs_to(attr: &Attribute) -> Option { - attr.parse_args::().ok() +/// Returns the entity identifier and optional ON DELETE action. +fn parse_belongs_to(attr: &Attribute) -> (Option, Option) { + // Try simple case: #[belongs_to(Entity)] + if let Ok(ident) = attr.parse_args::() { + return (Some(ident), None); + } + + // Try extended case: #[belongs_to(Entity, on_delete = "cascade")] + let mut entity = None; + let mut on_delete = None; + + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("on_delete") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + on_delete = ReferentialAction::from_str(&value.value()); + } else if let Some(ident) = meta.path.get_ident() { + entity = Some(ident.clone()); + } + Ok(()) + }); + + (entity, on_delete) } /// Field definition with all parsed attributes. @@ -65,6 +87,9 @@ fn parse_belongs_to(attr: &Attribute) -> Option { /// #[auto] // StorageConfig::is_auto = true /// #[field(response)] /// pub created_at: DateTime, +/// +/// #[column(unique, index)] // ColumnConfig +/// pub email: String, /// ``` #[derive(Debug)] pub struct FieldDef { @@ -83,6 +108,11 @@ pub struct FieldDef { /// Query filter configuration. pub filter: FilterConfig, + /// Column configuration for migrations. + /// + /// Parsed from `#[column(...)]` attributes for constraints and indexes. + pub column: ColumnConfig, + /// Documentation comment from the field. /// /// Extracted from `///` comments for use in OpenAPI descriptions. @@ -123,6 +153,7 @@ impl FieldDef { let mut expose = ExposeConfig::default(); let mut storage = StorageConfig::default(); let mut filter = FilterConfig::default(); + let mut column = ColumnConfig::default(); for attr in &field.attrs { if attr.path().is_ident("id") { @@ -132,9 +163,13 @@ impl FieldDef { } else if attr.path().is_ident("field") { expose = ExposeConfig::from_attr(attr); } else if attr.path().is_ident("belongs_to") { - storage.belongs_to = parse_belongs_to(attr); + let (entity, on_del) = parse_belongs_to(attr); + storage.belongs_to = entity; + storage.on_delete = on_del; } else if attr.path().is_ident("filter") { filter = FilterConfig::from_attr(attr); + } else if attr.path().is_ident("column") { + column = ColumnConfig::from_attr(attr); } } @@ -144,6 +179,7 @@ impl FieldDef { expose, storage, filter, + column, doc, validation, example @@ -281,6 +317,34 @@ impl FieldDef { pub fn has_example(&self) -> bool { self.example.is_some() } + + /// Get the column configuration. + /// + /// Returns parsed column constraints and index settings. + #[must_use] + pub fn column(&self) -> &ColumnConfig { + &self.column + } + + /// Check if this column has a UNIQUE constraint. + #[must_use] + pub fn is_unique(&self) -> bool { + self.column.unique + } + + /// Check if this column should be indexed. + #[must_use] + pub fn has_index(&self) -> bool { + self.column.has_index() + } + + /// Get the database column name. + /// + /// Returns custom name if set, otherwise the field name. + #[must_use] + pub fn column_name(&self) -> String { + self.column.column_name(&self.name_str()).to_string() + } } #[cfg(test)] @@ -425,4 +489,86 @@ mod tests { let field = parse_field(quote::quote! { pub email: String }); assert_eq!(field.name().to_string(), "email"); } + + #[test] + fn field_column_unique() { + let field = parse_field(quote::quote! { + #[column(unique)] + pub email: String + }); + assert!(field.is_unique()); + } + + #[test] + fn field_column_index() { + let field = parse_field(quote::quote! { + #[column(index)] + pub status: String + }); + assert!(field.has_index()); + assert_eq!(field.column().index, Some(IndexType::BTree)); + } + + #[test] + fn field_column_index_gin() { + let field = parse_field(quote::quote! { + #[column(index = "gin")] + pub tags: Vec + }); + assert!(field.has_index()); + assert_eq!(field.column().index, Some(IndexType::Gin)); + } + + #[test] + fn field_column_default() { + let field = parse_field(quote::quote! { + #[column(default = "true")] + pub is_active: bool + }); + assert_eq!(field.column().default, Some("true".to_string())); + } + + #[test] + fn field_column_check() { + let field = parse_field(quote::quote! { + #[column(check = "age >= 0")] + pub age: i32 + }); + assert_eq!(field.column().check, Some("age >= 0".to_string())); + } + + #[test] + fn field_column_varchar() { + let field = parse_field(quote::quote! { + #[column(varchar = 100)] + pub name: String + }); + assert_eq!(field.column().varchar, Some(100)); + } + + #[test] + fn field_column_custom_name() { + let field = parse_field(quote::quote! { + #[column(name = "user_email")] + pub email: String + }); + assert_eq!(field.column_name(), "user_email"); + } + + #[test] + fn field_column_default_name() { + let field = parse_field(quote::quote! { pub email: String }); + assert_eq!(field.column_name(), "email"); + } + + #[test] + fn field_column_multiple_attrs() { + let field = parse_field(quote::quote! { + #[column(unique, index, default = "NOW()")] + pub created_at: DateTime + }); + assert!(field.is_unique()); + assert!(field.has_index()); + assert_eq!(field.column().default, Some("NOW()".to_string())); + } } diff --git a/crates/entity-derive-impl/src/entity/parse/field/column.rs b/crates/entity-derive-impl/src/entity/parse/field/column.rs new file mode 100644 index 0000000..b866a7c --- /dev/null +++ b/crates/entity-derive-impl/src/entity/parse/field/column.rs @@ -0,0 +1,427 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! Column-level database configuration for migrations. +//! +//! Controls database-specific constraints, indexes, and type mappings. +//! +//! # Supported Attributes +//! +//! | Attribute | Example | SQL | +//! |-----------|---------|-----| +//! | `unique` | `#[column(unique)]` | `UNIQUE` | +//! | `index` | `#[column(index)]` | `CREATE INDEX` (btree) | +//! | `index` | `#[column(index = "gin")]` | `CREATE INDEX USING gin` | +//! | `default` | `#[column(default = "true")]` | `DEFAULT true` | +//! | `check` | `#[column(check = "age >= 0")]` | `CHECK (age >= 0)` | +//! | `varchar` | `#[column(varchar = 255)]` | `VARCHAR(255)` | +//! | `sql_type` | `#[column(sql_type = "JSONB")]` | Explicit type | +//! | `nullable` | `#[column(nullable)]` | Allow NULL | +//! | `name` | `#[column(name = "user_name")]` | Custom column name | + +use syn::{Attribute, Meta}; + +/// Index type for database indexes. +/// +/// PostgreSQL supports multiple index types optimized for different use cases. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum IndexType { + /// B-tree index (default). Best for equality and range queries. + #[default] + BTree, + + /// Hash index. Only for equality comparisons. + Hash, + + /// GIN (Generalized Inverted Index). For array/JSONB containment. + Gin, + + /// GiST (Generalized Search Tree). For geometric/full-text search. + Gist, + + /// BRIN (Block Range Index). For large sequential data. + Brin +} + +impl IndexType { + /// Parse index type from string. + /// + /// Returns `None` for unrecognized values. + #[must_use] + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "btree" | "b-tree" => Some(Self::BTree), + "hash" => Some(Self::Hash), + "gin" => Some(Self::Gin), + "gist" => Some(Self::Gist), + "brin" => Some(Self::Brin), + _ => None + } + } + + /// Get SQL USING clause for this index type. + /// + /// Returns empty string for btree (default). + #[must_use] + pub fn as_sql_using(&self) -> &'static str { + match self { + Self::BTree => "", + Self::Hash => " USING hash", + Self::Gin => " USING gin", + Self::Gist => " USING gist", + Self::Brin => " USING brin" + } + } +} + +/// Referential action for foreign key ON DELETE/ON UPDATE. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReferentialAction { + /// Delete/update child rows when parent is deleted/updated. + Cascade, + + /// Set foreign key to NULL. + SetNull, + + /// Set foreign key to default value. + SetDefault, + + /// Prevent deletion/update if children exist (deferred check). + Restrict, + + /// Prevent deletion/update if children exist (immediate check). + NoAction +} + +impl ReferentialAction { + /// Parse referential action from string. + /// + /// Returns `None` for unrecognized values. + #[must_use] + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().replace([' ', '_'], "").as_str() { + "cascade" => Some(Self::Cascade), + "setnull" => Some(Self::SetNull), + "setdefault" => Some(Self::SetDefault), + "restrict" => Some(Self::Restrict), + "noaction" => Some(Self::NoAction), + _ => None + } + } + + /// Get SQL representation of this action. + #[must_use] + pub fn as_sql(&self) -> &'static str { + match self { + Self::Cascade => "CASCADE", + Self::SetNull => "SET NULL", + Self::SetDefault => "SET DEFAULT", + Self::Restrict => "RESTRICT", + Self::NoAction => "NO ACTION" + } + } +} + +/// Column-level database configuration. +/// +/// Parsed from `#[column(...)]` attributes on entity fields. +/// +/// # Example +/// +/// ```rust,ignore +/// #[column(unique, index = "btree", default = "true")] +/// pub is_active: bool, +/// +/// #[column(varchar = 100, check = "length(name) > 0")] +/// pub name: String, +/// +/// #[column(sql_type = "JSONB")] +/// pub metadata: serde_json::Value, +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ColumnConfig { + /// UNIQUE constraint on this column. + pub unique: bool, + + /// Index type if indexed. `None` means no index. + pub index: Option, + + /// DEFAULT value expression (raw SQL). + /// + /// Examples: `"true"`, `"NOW()"`, `"'pending'"`. + pub default: Option, + + /// CHECK constraint expression (raw SQL). + /// + /// Example: `"age >= 0"`, `"length(name) > 0"`. + pub check: Option, + + /// VARCHAR length. Converts `String` to `VARCHAR(n)`. + pub varchar: Option, + + /// Explicit SQL type override. + /// + /// Bypasses automatic type mapping. + pub sql_type: Option, + + /// Explicitly allow NULL even for non-Option types. + pub nullable: bool, + + /// Custom column name. Defaults to field name. + pub name: Option +} + +impl ColumnConfig { + /// Parse column config from `#[column(...)]` attribute. + /// + /// # Recognized Options + /// + /// - `unique` — Add UNIQUE constraint + /// - `index` — Create btree index + /// - `index = "type"` — Create index of specified type + /// - `default = "expr"` — Set DEFAULT value + /// - `check = "expr"` — Add CHECK constraint + /// - `varchar = N` — Use VARCHAR(N) instead of TEXT + /// - `sql_type = "TYPE"` — Override SQL type + /// - `nullable` — Allow NULL + /// - `name = "col"` — Custom column name + pub fn from_attr(attr: &Attribute) -> Self { + let mut config = Self::default(); + + if let Meta::List(meta_list) = &attr.meta { + let _ = meta_list.parse_nested_meta(|meta| { + if meta.path.is_ident("unique") { + config.unique = true; + } else if meta.path.is_ident("index") { + if meta.input.peek(syn::Token![=]) { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + config.index = + Some(IndexType::from_str(&value.value()).unwrap_or_default()); + } else { + config.index = Some(IndexType::default()); + } + } else if meta.path.is_ident("default") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + config.default = Some(value.value()); + } else if meta.path.is_ident("check") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + config.check = Some(value.value()); + } else if meta.path.is_ident("varchar") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitInt = meta.input.parse()?; + config.varchar = value.base10_parse().ok(); + } else if meta.path.is_ident("sql_type") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + config.sql_type = Some(value.value()); + } else if meta.path.is_ident("nullable") { + config.nullable = true; + } else if meta.path.is_ident("name") { + let _: syn::Token![=] = meta.input.parse()?; + let value: syn::LitStr = meta.input.parse()?; + config.name = Some(value.value()); + } + Ok(()) + }); + } + + config + } + + /// Check if this column has any constraints. + #[must_use] + pub fn has_constraints(&self) -> bool { + self.unique || self.check.is_some() + } + + /// Check if this column should be indexed. + #[must_use] + pub fn has_index(&self) -> bool { + self.index.is_some() + } + + /// Get the column name, using custom name if set. + #[must_use] + pub fn column_name<'a>(&'a self, field_name: &'a str) -> &'a str { + self.name.as_deref().unwrap_or(field_name) + } +} + +#[cfg(test)] +mod tests { + use quote::quote; + use syn::parse_quote; + + use super::*; + + fn parse_column_attr(tokens: proc_macro2::TokenStream) -> ColumnConfig { + let attr: Attribute = parse_quote!(#[column(#tokens)]); + ColumnConfig::from_attr(&attr) + } + + #[test] + fn default_is_empty() { + let config = ColumnConfig::default(); + assert!(!config.unique); + assert!(config.index.is_none()); + assert!(config.default.is_none()); + assert!(config.check.is_none()); + assert!(config.varchar.is_none()); + assert!(config.sql_type.is_none()); + assert!(!config.nullable); + assert!(config.name.is_none()); + } + + #[test] + fn parse_unique() { + let config = parse_column_attr(quote! { unique }); + assert!(config.unique); + } + + #[test] + fn parse_index_default() { + let config = parse_column_attr(quote! { index }); + assert_eq!(config.index, Some(IndexType::BTree)); + } + + #[test] + fn parse_index_gin() { + let config = parse_column_attr(quote! { index = "gin" }); + assert_eq!(config.index, Some(IndexType::Gin)); + } + + #[test] + fn parse_index_hash() { + let config = parse_column_attr(quote! { index = "hash" }); + assert_eq!(config.index, Some(IndexType::Hash)); + } + + #[test] + fn parse_default_value() { + let config = parse_column_attr(quote! { default = "true" }); + assert_eq!(config.default, Some("true".to_string())); + } + + #[test] + fn parse_default_now() { + let config = parse_column_attr(quote! { default = "NOW()" }); + assert_eq!(config.default, Some("NOW()".to_string())); + } + + #[test] + fn parse_check_constraint() { + let config = parse_column_attr(quote! { check = "age >= 0" }); + assert_eq!(config.check, Some("age >= 0".to_string())); + } + + #[test] + fn parse_varchar() { + let config = parse_column_attr(quote! { varchar = 255 }); + assert_eq!(config.varchar, Some(255)); + } + + #[test] + fn parse_sql_type() { + let config = parse_column_attr(quote! { sql_type = "JSONB" }); + assert_eq!(config.sql_type, Some("JSONB".to_string())); + } + + #[test] + fn parse_nullable() { + let config = parse_column_attr(quote! { nullable }); + assert!(config.nullable); + } + + #[test] + fn parse_custom_name() { + let config = parse_column_attr(quote! { name = "user_name" }); + assert_eq!(config.name, Some("user_name".to_string())); + } + + #[test] + fn parse_multiple_attrs() { + let config = parse_column_attr(quote! { unique, index = "btree", default = "true" }); + assert!(config.unique); + assert_eq!(config.index, Some(IndexType::BTree)); + assert_eq!(config.default, Some("true".to_string())); + } + + #[test] + fn has_constraints_check() { + let config = parse_column_attr(quote! { unique }); + assert!(config.has_constraints()); + + let config2 = parse_column_attr(quote! { check = "x > 0" }); + assert!(config2.has_constraints()); + + let config3 = ColumnConfig::default(); + assert!(!config3.has_constraints()); + } + + #[test] + fn has_index_check() { + let config = parse_column_attr(quote! { index }); + assert!(config.has_index()); + + let config2 = ColumnConfig::default(); + assert!(!config2.has_index()); + } + + #[test] + fn column_name_default() { + let config = ColumnConfig::default(); + assert_eq!(config.column_name("email"), "email"); + } + + #[test] + fn column_name_custom() { + let config = parse_column_attr(quote! { name = "user_email" }); + assert_eq!(config.column_name("email"), "user_email"); + } + + #[test] + fn index_type_as_sql() { + assert_eq!(IndexType::BTree.as_sql_using(), ""); + assert_eq!(IndexType::Hash.as_sql_using(), " USING hash"); + assert_eq!(IndexType::Gin.as_sql_using(), " USING gin"); + assert_eq!(IndexType::Gist.as_sql_using(), " USING gist"); + assert_eq!(IndexType::Brin.as_sql_using(), " USING brin"); + } + + #[test] + fn referential_action_from_str() { + assert_eq!( + ReferentialAction::from_str("cascade"), + Some(ReferentialAction::Cascade) + ); + assert_eq!( + ReferentialAction::from_str("SET NULL"), + Some(ReferentialAction::SetNull) + ); + assert_eq!( + ReferentialAction::from_str("set_default"), + Some(ReferentialAction::SetDefault) + ); + assert_eq!( + ReferentialAction::from_str("RESTRICT"), + Some(ReferentialAction::Restrict) + ); + assert_eq!( + ReferentialAction::from_str("no action"), + Some(ReferentialAction::NoAction) + ); + assert_eq!(ReferentialAction::from_str("invalid"), None); + } + + #[test] + fn referential_action_as_sql() { + assert_eq!(ReferentialAction::Cascade.as_sql(), "CASCADE"); + assert_eq!(ReferentialAction::SetNull.as_sql(), "SET NULL"); + assert_eq!(ReferentialAction::SetDefault.as_sql(), "SET DEFAULT"); + assert_eq!(ReferentialAction::Restrict.as_sql(), "RESTRICT"); + assert_eq!(ReferentialAction::NoAction.as_sql(), "NO ACTION"); + } +} diff --git a/crates/entity-derive-impl/src/entity/parse/field/storage.rs b/crates/entity-derive-impl/src/entity/parse/field/storage.rs index 71c77bc..1f55511 100644 --- a/crates/entity-derive-impl/src/entity/parse/field/storage.rs +++ b/crates/entity-derive-impl/src/entity/parse/field/storage.rs @@ -16,9 +16,18 @@ //! ``` //! //! This generates a `find_user` method in the repository. +//! +//! ## With ON DELETE action +//! +//! ```rust,ignore +//! #[belongs_to(User, on_delete = "cascade")] +//! pub user_id: Uuid, +//! ``` use syn::Ident; +use super::ReferentialAction; + /// Database storage configuration. /// /// Determines how the field is stored and managed in the database. @@ -28,12 +37,7 @@ use syn::Ident; /// - `#[id]` — Primary key with auto-generated UUID /// - `#[auto]` — Auto-generated value (timestamps) /// - `#[belongs_to(Entity)]` — Foreign key relation -/// -/// # Future attributes (planned) -/// -/// - `#[column(name = "...")]` — Custom column name -/// - `#[column(index)]` — Create index -/// - `#[column(unique)]` — Unique constraint +/// - `#[belongs_to(Entity, on_delete = "cascade")]` — FK with ON DELETE #[derive(Debug, Default, Clone)] pub struct StorageConfig { /// Primary key field (`#[id]`). @@ -56,6 +60,7 @@ pub struct StorageConfig { /// /// Stores the related entity name. When set, generates: /// - `find_{entity}(&self, id) -> Result>` method + /// - REFERENCES clause in migration (if migrations enabled) /// /// # Example /// @@ -64,7 +69,20 @@ pub struct StorageConfig { /// pub user_id: Uuid, /// // Generates: async fn find_user(&self, post_id: Uuid) -> Result> /// ``` - pub belongs_to: Option + pub belongs_to: Option, + + /// ON DELETE action for foreign key. + /// + /// Only applies when `belongs_to` is set. + /// + /// # Example + /// + /// ```rust,ignore + /// #[belongs_to(User, on_delete = "cascade")] + /// pub user_id: Uuid, + /// // Generates: REFERENCES users(id) ON DELETE CASCADE + /// ``` + pub on_delete: Option } impl StorageConfig { @@ -87,6 +105,7 @@ mod tests { assert!(!config.is_id); assert!(!config.is_auto); assert!(!config.is_relation()); + assert!(config.on_delete.is_none()); } #[test] @@ -94,8 +113,21 @@ mod tests { let config = StorageConfig { is_id: false, is_auto: false, - belongs_to: Some(Ident::new("User", Span::call_site())) + belongs_to: Some(Ident::new("User", Span::call_site())), + on_delete: None + }; + assert!(config.is_relation()); + } + + #[test] + fn belongs_to_with_on_delete() { + let config = StorageConfig { + is_id: false, + is_auto: false, + belongs_to: Some(Ident::new("User", Span::call_site())), + on_delete: Some(ReferentialAction::Cascade) }; assert!(config.is_relation()); + assert_eq!(config.on_delete, Some(ReferentialAction::Cascade)); } } diff --git a/crates/entity-derive/src/lib.rs b/crates/entity-derive/src/lib.rs index 4615074..d9d90ee 100644 --- a/crates/entity-derive/src/lib.rs +++ b/crates/entity-derive/src/lib.rs @@ -3,8 +3,8 @@ #![doc = include_str!("../README.md")] #![doc( - html_logo_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/assets/logo.svg", - html_favicon_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/assets/favicon.ico" + html_logo_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/logo.png", + html_favicon_url = "https://raw.githubusercontent.com/RAprogramm/entity-derive/main/logo.png" )] #![cfg_attr(docsrs, feature(doc_cfg))] #![warn(missing_docs)] From d01e86a9048b4d9f66b7d32e553a92df3e6234c8 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:25:59 +0700 Subject: [PATCH 03/11] feat(migrations): add migration generation infrastructure - Add ColumnConfig for column-level constraints (#[column(...)]) - unique, index, default, check, varchar, sql_type, nullable, name - Add CompositeIndexDef for entity-level indexes - #[entity(index(col1, col2), unique_index(...))] - Add ReferentialAction for ON DELETE/UPDATE actions - Extend StorageConfig with on_delete for #[belongs_to] - Add migrations flag to EntityAttrs and EntityDef - Add parse_index_attrs helper for composite indexes - Add PostgresTypeMapper for Rust -> PostgreSQL type mapping - Create migrations module structure (types/, postgres/) --- .../src/entity/migrations/postgres/mod.rs | 53 +++ .../src/entity/migrations/types/mod.rs | 141 ++++++++ .../src/entity/migrations/types/postgres.rs | 318 ++++++++++++++++++ .../src/entity/parse/entity/attrs.rs | 26 +- .../src/entity/parse/entity/constructor.rs | 7 +- .../src/entity/parse/entity/def.rs | 15 +- .../src/entity/parse/entity/helpers.rs | 88 +++++ .../src/entity/parse/field.rs | 23 ++ 8 files changed, 666 insertions(+), 5 deletions(-) create mode 100644 crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs create mode 100644 crates/entity-derive-impl/src/entity/migrations/types/mod.rs create mode 100644 crates/entity-derive-impl/src/entity/migrations/types/postgres.rs diff --git a/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs b/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs new file mode 100644 index 0000000..0746954 --- /dev/null +++ b/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! PostgreSQL migration generation. +//! +//! Generates `MIGRATION_UP` and `MIGRATION_DOWN` constants for PostgreSQL. + +mod ddl; + +use proc_macro2::TokenStream; +use quote::quote; + +use crate::entity::parse::EntityDef; +use crate::utils::marker; + +/// Generate migration constants for PostgreSQL. +/// +/// # Generated Code +/// +/// ```rust,ignore +/// impl User { +/// pub const MIGRATION_UP: &'static str = "CREATE TABLE..."; +/// pub const MIGRATION_DOWN: &'static str = "DROP TABLE..."; +/// } +/// ``` +pub fn generate(entity: &EntityDef) -> TokenStream { + let entity_name = entity.name(); + let vis = &entity.vis; + + let up_sql = ddl::generate_up(entity); + let down_sql = ddl::generate_down(entity); + + let marker = marker::generated(); + + quote! { + #marker + impl #entity_name { + /// SQL migration to create this entity's table, indexes, and constraints. + /// + /// # Usage + /// + /// ```rust,ignore + /// sqlx::query(User::MIGRATION_UP).execute(&pool).await?; + /// ``` + #vis const MIGRATION_UP: &'static str = #up_sql; + + /// SQL migration to drop this entity's table. + /// + /// Uses CASCADE to drop dependent objects. + #vis const MIGRATION_DOWN: &'static str = #down_sql; + } + } +} diff --git a/crates/entity-derive-impl/src/entity/migrations/types/mod.rs b/crates/entity-derive-impl/src/entity/migrations/types/mod.rs new file mode 100644 index 0000000..b3d5404 --- /dev/null +++ b/crates/entity-derive-impl/src/entity/migrations/types/mod.rs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! Type mapping from Rust to database-specific SQL types. +//! +//! This module provides traits and implementations for mapping Rust types +//! to their SQL equivalents during migration generation. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────────────────────────────┐ +//! │ Type Mapping System │ +//! ├─────────────────────────────────────────────────────────────────────┤ +//! │ │ +//! │ Rust Type TypeMapper SQL Type │ +//! │ │ +//! │ Uuid ──► PostgresMapper ──► UUID │ +//! │ String ──► ──► TEXT / VARCHAR(n) │ +//! │ i32 ──► ──► INTEGER │ +//! │ DateTime ──► ──► TIMESTAMPTZ │ +//! │ Option ──► ──► T (nullable) │ +//! │ Vec ──► ──► T[] │ +//! │ │ +//! └─────────────────────────────────────────────────────────────────────┘ +//! ``` + +mod postgres; + +pub use postgres::PostgresTypeMapper; + +use syn::Type; + +use crate::entity::parse::field::ColumnConfig; + +/// Mapped SQL type representation. +#[derive(Debug, Clone)] +pub struct SqlType { + /// SQL type name (e.g., "UUID", "TEXT", "INTEGER"). + pub name: String, + + /// Whether this type allows NULL values. + pub nullable: bool, + + /// Array dimension (0 = scalar, 1 = T[], 2 = T[][], etc.). + pub array_dim: usize +} + +impl SqlType { + /// Create a non-nullable SQL type. + #[must_use] + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + nullable: false, + array_dim: 0 + } + } + + /// Create a nullable SQL type. + #[must_use] + pub fn nullable(name: impl Into) -> Self { + Self { + name: name.into(), + nullable: true, + array_dim: 0 + } + } + + /// Get the full SQL type string with array suffix. + #[must_use] + pub fn to_sql_string(&self) -> String { + if self.array_dim > 0 { + format!("{}{}", self.name, "[]".repeat(self.array_dim)) + } else { + self.name.clone() + } + } +} + +/// Trait for mapping Rust types to SQL types. +/// +/// Implement this trait for each database dialect. +pub trait TypeMapper { + /// Map a Rust type to its SQL representation. + /// + /// # Arguments + /// + /// * `ty` - The Rust type from syn + /// * `column` - Column configuration with overrides + /// + /// # Returns + /// + /// `SqlType` with name, nullable flag, and array dimension. + fn map_type(&self, ty: &Type, column: &ColumnConfig) -> SqlType; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sql_type_new() { + let ty = SqlType::new("INTEGER"); + assert_eq!(ty.name, "INTEGER"); + assert!(!ty.nullable); + assert_eq!(ty.array_dim, 0); + } + + #[test] + fn sql_type_nullable() { + let ty = SqlType::nullable("TEXT"); + assert!(ty.nullable); + } + + #[test] + fn sql_type_to_sql_string_scalar() { + let ty = SqlType::new("UUID"); + assert_eq!(ty.to_sql_string(), "UUID"); + } + + #[test] + fn sql_type_to_sql_string_array() { + let ty = SqlType { + name: "TEXT".to_string(), + nullable: false, + array_dim: 1 + }; + assert_eq!(ty.to_sql_string(), "TEXT[]"); + } + + #[test] + fn sql_type_to_sql_string_2d_array() { + let ty = SqlType { + name: "INTEGER".to_string(), + nullable: false, + array_dim: 2 + }; + assert_eq!(ty.to_sql_string(), "INTEGER[][]"); + } +} diff --git a/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs new file mode 100644 index 0000000..2348b59 --- /dev/null +++ b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! PostgreSQL type mapping. +//! +//! Maps Rust types to PostgreSQL types for migration generation. +//! +//! # Type Mapping Table +//! +//! | Rust Type | PostgreSQL Type | Notes | +//! |-----------|-----------------|-------| +//! | `Uuid` | `UUID` | | +//! | `String` | `TEXT` | Or `VARCHAR(n)` with `#[column(varchar = n)]` | +//! | `i16` | `SMALLINT` | | +//! | `i32` | `INTEGER` | | +//! | `i64` | `BIGINT` | | +//! | `f32` | `REAL` | | +//! | `f64` | `DOUBLE PRECISION` | | +//! | `bool` | `BOOLEAN` | | +//! | `DateTime` | `TIMESTAMPTZ` | | +//! | `NaiveDate` | `DATE` | | +//! | `NaiveTime` | `TIME` | | +//! | `NaiveDateTime` | `TIMESTAMP` | | +//! | `Option` | `T` | Nullable | +//! | `Vec` | `T[]` | PostgreSQL array | +//! | `serde_json::Value` | `JSONB` | | +//! | `Decimal` | `DECIMAL` | | +//! | `IpAddr` | `INET` | | + +use syn::Type; + +use super::{SqlType, TypeMapper}; +use crate::entity::parse::field::ColumnConfig; + +/// PostgreSQL type mapper. +/// +/// Converts Rust types to PostgreSQL SQL types with full support for: +/// - Primitive types (integers, floats, booleans) +/// - String types with optional VARCHAR length +/// - Date/time types from chrono +/// - UUID from uuid crate +/// - JSON from serde_json +/// - Arrays via Vec +/// - Nullable types via Option +pub struct PostgresTypeMapper; + +impl TypeMapper for PostgresTypeMapper { + fn map_type(&self, ty: &Type, column: &ColumnConfig) -> SqlType { + // Handle explicit SQL type override + if let Some(ref explicit) = column.sql_type { + return SqlType { + name: explicit.clone(), + nullable: is_option(ty) || column.nullable, + array_dim: 0 + }; + } + + // Handle Option + if let Some(inner) = extract_option_inner(ty) { + let mut result = self.map_type(inner, column); + result.nullable = true; + return result; + } + + // Handle Vec (PostgreSQL arrays) + if let Some(inner) = extract_vec_inner(ty) { + let mut result = self.map_type(inner, column); + result.array_dim += 1; + return result; + } + + // Map core types + let name = map_type_name(ty, column); + + SqlType { + name, + nullable: column.nullable, + array_dim: 0 + } + } +} + +/// Map a Rust type path to PostgreSQL type name. +fn map_type_name(ty: &Type, column: &ColumnConfig) -> String { + let type_str = type_path_string(ty); + + match type_str.as_str() { + // UUIDs + "Uuid" | "uuid::Uuid" => "UUID".to_string(), + + // Strings + "String" | "str" => { + if let Some(len) = column.varchar { + format!("VARCHAR({})", len) + } else { + "TEXT".to_string() + } + } + + // Integers + "i8" => "SMALLINT".to_string(), // PostgreSQL has no TINYINT + "i16" => "SMALLINT".to_string(), + "i32" => "INTEGER".to_string(), + "i64" => "BIGINT".to_string(), + "u8" => "SMALLINT".to_string(), + "u16" => "INTEGER".to_string(), + "u32" => "BIGINT".to_string(), + "u64" => "BIGINT".to_string(), // May overflow + + // Floats + "f32" => "REAL".to_string(), + "f64" => "DOUBLE PRECISION".to_string(), + + // Boolean + "bool" => "BOOLEAN".to_string(), + + // Date/Time (chrono) + "DateTime" | "chrono::DateTime" => "TIMESTAMPTZ".to_string(), + "NaiveDate" | "chrono::NaiveDate" => "DATE".to_string(), + "NaiveTime" | "chrono::NaiveTime" => "TIME".to_string(), + "NaiveDateTime" | "chrono::NaiveDateTime" => "TIMESTAMP".to_string(), + + // JSON + "Value" | "serde_json::Value" | "Json" | "sqlx::types::Json" => "JSONB".to_string(), + + // Decimal + "Decimal" | "rust_decimal::Decimal" | "BigDecimal" | "bigdecimal::BigDecimal" => { + "DECIMAL".to_string() + } + + // Network + "IpAddr" | "std::net::IpAddr" | "Ipv4Addr" | "Ipv6Addr" => "INET".to_string(), + "MacAddr" => "MACADDR".to_string(), + + // Binary + "Vec" | "bytes::Bytes" => "BYTEA".to_string(), + + // Fallback to TEXT for unknown types + _ => "TEXT".to_string() + } +} + +/// Extract the type path as a string. +fn type_path_string(ty: &Type) -> String { + if let Type::Path(type_path) = ty { + type_path + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::") + } else { + String::new() + } +} + +/// Check if a type is Option. +fn is_option(ty: &Type) -> bool { + extract_option_inner(ty).is_some() +} + +/// Extract the inner type from Option. +fn extract_option_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + && segment.ident == "Option" + { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + return Some(inner); + } + } + } + None +} + +/// Extract the inner type from Vec. +fn extract_vec_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + && segment.ident == "Vec" + { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + return Some(inner); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + fn map_type(ty_tokens: proc_macro2::TokenStream) -> SqlType { + let ty: Type = parse_quote!(#ty_tokens); + let column = ColumnConfig::default(); + PostgresTypeMapper.map_type(&ty, &column) + } + + fn map_type_with_column(ty_tokens: proc_macro2::TokenStream, column: ColumnConfig) -> SqlType { + let ty: Type = parse_quote!(#ty_tokens); + PostgresTypeMapper.map_type(&ty, &column) + } + + #[test] + fn map_uuid() { + let ty = map_type(quote::quote! { Uuid }); + assert_eq!(ty.name, "UUID"); + assert!(!ty.nullable); + } + + #[test] + fn map_string() { + let ty = map_type(quote::quote! { String }); + assert_eq!(ty.name, "TEXT"); + } + + #[test] + fn map_string_varchar() { + let mut column = ColumnConfig::default(); + column.varchar = Some(255); + let ty = map_type_with_column(quote::quote! { String }, column); + assert_eq!(ty.name, "VARCHAR(255)"); + } + + #[test] + fn map_integers() { + assert_eq!(map_type(quote::quote! { i16 }).name, "SMALLINT"); + assert_eq!(map_type(quote::quote! { i32 }).name, "INTEGER"); + assert_eq!(map_type(quote::quote! { i64 }).name, "BIGINT"); + } + + #[test] + fn map_floats() { + assert_eq!(map_type(quote::quote! { f32 }).name, "REAL"); + assert_eq!(map_type(quote::quote! { f64 }).name, "DOUBLE PRECISION"); + } + + #[test] + fn map_bool() { + assert_eq!(map_type(quote::quote! { bool }).name, "BOOLEAN"); + } + + #[test] + fn map_datetime() { + let ty = map_type(quote::quote! { DateTime }); + assert_eq!(ty.name, "TIMESTAMPTZ"); + } + + #[test] + fn map_naive_date() { + assert_eq!(map_type(quote::quote! { NaiveDate }).name, "DATE"); + } + + #[test] + fn map_option_nullable() { + let ty = map_type(quote::quote! { Option }); + assert_eq!(ty.name, "TEXT"); + assert!(ty.nullable); + } + + #[test] + fn map_vec_to_array() { + let ty = map_type(quote::quote! { Vec }); + assert_eq!(ty.name, "TEXT"); + assert_eq!(ty.array_dim, 1); + assert_eq!(ty.to_sql_string(), "TEXT[]"); + } + + #[test] + fn map_vec_option() { + let ty = map_type(quote::quote! { Vec> }); + assert_eq!(ty.name, "INTEGER"); + assert!(ty.nullable); + assert_eq!(ty.array_dim, 1); + } + + #[test] + fn map_option_vec() { + let ty = map_type(quote::quote! { Option> }); + assert_eq!(ty.name, "INTEGER"); + assert!(ty.nullable); + assert_eq!(ty.array_dim, 1); + } + + #[test] + fn map_json() { + assert_eq!(map_type(quote::quote! { serde_json::Value }).name, "JSONB"); + } + + #[test] + fn map_explicit_sql_type() { + let mut column = ColumnConfig::default(); + column.sql_type = Some("CITEXT".to_string()); + let ty = map_type_with_column(quote::quote! { String }, column); + assert_eq!(ty.name, "CITEXT"); + } + + #[test] + fn map_decimal() { + assert_eq!(map_type(quote::quote! { Decimal }).name, "DECIMAL"); + } + + #[test] + fn map_ip_addr() { + assert_eq!(map_type(quote::quote! { IpAddr }).name, "INET"); + } + + #[test] + fn map_unknown_to_text() { + assert_eq!(map_type(quote::quote! { MyCustomType }).name, "TEXT"); + } +} diff --git a/crates/entity-derive-impl/src/entity/parse/entity/attrs.rs b/crates/entity-derive-impl/src/entity/parse/entity/attrs.rs index eb8b08f..1aa02ef 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/attrs.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/attrs.rs @@ -245,5 +245,29 @@ pub struct EntityAttrs { /// .await?; /// ``` #[darling(default)] - pub transactions: bool + pub transactions: bool, + + /// Enable migration generation. + /// + /// When enabled, generates: + /// - `{Entity}::MIGRATION_UP` — SQL to create the table + /// - `{Entity}::MIGRATION_DOWN` — SQL to drop the table + /// + /// # Example + /// + /// ```rust,ignore + /// #[entity(table = "users", migrations)] + /// pub struct User { + /// #[id] + /// pub id: Uuid, + /// #[column(unique, index)] + /// pub email: String, + /// } + /// + /// // Generated: + /// // User::MIGRATION_UP → CREATE TABLE core.users (...) + /// // User::MIGRATION_DOWN → DROP TABLE core.users CASCADE + /// ``` + #[darling(default)] + pub migrations: bool } diff --git a/crates/entity-derive-impl/src/entity/parse/entity/constructor.rs b/crates/entity-derive-impl/src/entity/parse/entity/constructor.rs index 37d5ddc..740e8fe 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/constructor.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/constructor.rs @@ -61,7 +61,7 @@ use syn::DeriveInput; use super::{ super::{command::parse_command_attrs, field::FieldDef}, EntityAttrs, EntityDef, - helpers::{parse_api_attr, parse_has_many_attrs}, + helpers::{parse_api_attr, parse_has_many_attrs, parse_index_attrs}, parse_projection_attrs }; use crate::utils::docs::extract_doc_comments; @@ -130,6 +130,7 @@ impl EntityDef { let projections = parse_projection_attrs(&input.attrs); let command_defs = parse_command_attrs(&input.attrs); let api_config = parse_api_attr(&input.attrs); + let indexes = parse_index_attrs(&input.attrs); let doc = extract_doc_comments(&input.attrs); let id_field_index = fields.iter().position(|f| f.is_id()).ok_or_else(|| { @@ -160,7 +161,9 @@ impl EntityDef { streams: attrs.streams, transactions: attrs.transactions, api_config, - doc + doc, + migrations: attrs.migrations, + indexes }) } } diff --git a/crates/entity-derive-impl/src/entity/parse/entity/def.rs b/crates/entity-derive-impl/src/entity/parse/entity/def.rs index f1c9ddb..31a18f8 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/def.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/def.rs @@ -63,7 +63,7 @@ use super::{ api::ApiConfig, command::CommandDef, dialect::DatabaseDialect, field::FieldDef, returning::ReturningMode, sql_level::SqlLevel, uuid_version::UuidVersion }, - ProjectionDef + CompositeIndexDef, ProjectionDef }; /// Complete parsed entity definition. @@ -204,5 +204,16 @@ pub struct EntityDef { /// Documentation comment from the entity struct. /// /// Extracted from `///` comments for use in OpenAPI tag descriptions. - pub doc: Option + pub doc: Option, + + /// Whether to generate database migrations. + /// + /// When `true`, generates `MIGRATION_UP` and `MIGRATION_DOWN` constants + /// with SQL DDL statements for creating/dropping the table. + pub migrations: bool, + + /// Composite index definitions from `#[entity(index(...))]`. + /// + /// Each entry defines an index spanning multiple columns. + pub indexes: Vec } diff --git a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs index c20382b..074f2d6 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs @@ -59,6 +59,8 @@ use syn::{Attribute, Ident}; use super::super::api::{ApiConfig, parse_api_config}; +use super::super::field::IndexType; +use super::CompositeIndexDef; /// Parse `#[has_many(Entity)]` attributes from struct attributes. /// @@ -152,3 +154,89 @@ pub fn parse_api_attr(attrs: &[Attribute]) -> ApiConfig { ApiConfig::default() } + +/// Parse `index(...)` and `unique_index(...)` from `#[entity(...)]` attribute. +/// +/// Extracts composite index definitions from the entity attribute. +/// +/// # Syntax +/// +/// ```text +/// #[entity( +/// table = "users", +/// index(name, email), // Btree composite index +/// index(type = "gin", tags), // GIN index +/// unique_index(tenant_id, email), // Unique composite +/// index(name = "idx_custom", status), // Named index +/// )] +/// ``` +/// +/// # Returns +/// +/// Vector of `CompositeIndexDef` with parsed configurations. +pub fn parse_index_attrs(attrs: &[Attribute]) -> Vec { + let mut indexes = Vec::new(); + + for attr in attrs { + if !attr.path().is_ident("entity") { + continue; + } + + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("index") { + if let Ok(idx) = parse_index_content(&meta, false) { + indexes.push(idx); + } + } else if meta.path.is_ident("unique_index") { + if let Ok(idx) = parse_index_content(&meta, true) { + indexes.push(idx); + } + } + Ok(()) + }); + } + + indexes +} + +/// Parse the content of an index(...) or unique_index(...) attribute. +fn parse_index_content( + meta: &syn::meta::ParseNestedMeta<'_>, + unique: bool +) -> syn::Result { + let mut columns = Vec::new(); + let mut name = None; + let mut index_type = IndexType::default(); + let mut where_clause = None; + + meta.parse_nested_meta(|nested| { + if nested.path.is_ident("type") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + index_type = IndexType::from_str(&value.value()).unwrap_or_default(); + } else if nested.path.is_ident("name") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + name = Some(value.value()); + } else if nested.path.is_ident("where") { + let _: syn::Token![=] = nested.input.parse()?; + let value: syn::LitStr = nested.input.parse()?; + where_clause = Some(value.value()); + } else if let Some(ident) = nested.path.get_ident() { + columns.push(ident.to_string()); + } + Ok(()) + })?; + + if columns.is_empty() { + return Err(meta.error("index must have at least one column")); + } + + Ok(CompositeIndexDef { + name, + columns, + index_type, + unique, + where_clause + }) +} diff --git a/crates/entity-derive-impl/src/entity/parse/field.rs b/crates/entity-derive-impl/src/entity/parse/field.rs index 40c75f2..909fd00 100644 --- a/crates/entity-derive-impl/src/entity/parse/field.rs +++ b/crates/entity-derive-impl/src/entity/parse/field.rs @@ -416,6 +416,29 @@ mod tests { assert!(field.is_relation()); assert!(field.belongs_to().is_some()); assert_eq!(field.belongs_to().unwrap().to_string(), "User"); + assert!(field.storage.on_delete.is_none()); + } + + #[test] + fn field_belongs_to_with_on_delete() { + let field = parse_field(quote::quote! { + #[belongs_to(User, on_delete = "cascade")] + pub user_id: uuid::Uuid + }); + assert!(field.is_relation()); + assert_eq!(field.belongs_to().unwrap().to_string(), "User"); + assert_eq!(field.storage.on_delete, Some(ReferentialAction::Cascade)); + } + + #[test] + fn field_belongs_to_with_on_delete_set_null() { + let field = parse_field(quote::quote! { + #[belongs_to(Organization, on_delete = "set null")] + pub org_id: uuid::Uuid + }); + assert!(field.is_relation()); + assert_eq!(field.belongs_to().unwrap().to_string(), "Organization"); + assert_eq!(field.storage.on_delete, Some(ReferentialAction::SetNull)); } #[test] From 1d353824a7279918b49d1858f53c63acc1fb725c Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:35:36 +0700 Subject: [PATCH 04/11] feat(migrations): add compile-time migration generation - Add ColumnConfig parsing for column constraints (unique, index, default, check, varchar, sql_type, nullable) - Add IndexType enum (BTree, Hash, Gin, Gist, Brin) for index types - Add ReferentialAction enum for ON DELETE actions - Add CompositeIndexDef for entity-level composite indexes - Add PostgresTypeMapper for Rust to PostgreSQL type mapping - Generate MIGRATION_UP and MIGRATION_DOWN constants - Support full DDL: CREATE TABLE, CREATE INDEX, DROP TABLE CASCADE - Add column attribute to derive macro --- crates/entity-derive-impl/src/entity.rs | 3 + .../src/entity/migrations/mod.rs | 54 +++++ .../src/entity/migrations/postgres/ddl.rs | 201 ++++++++++++++++++ .../src/entity/migrations/postgres/mod.rs | 3 +- .../src/entity/migrations/types/mod.rs | 5 +- .../src/entity/migrations/types/postgres.rs | 18 +- crates/entity-derive-impl/src/entity/parse.rs | 4 +- .../src/entity/parse/entity.rs | 2 +- .../src/entity/parse/entity/helpers.rs | 26 ++- .../src/entity/parse/entity/index.rs | 22 +- .../src/entity/parse/field.rs | 4 +- .../src/entity/parse/field/column.rs | 1 + crates/entity-derive-impl/src/lib.rs | 2 +- 13 files changed, 307 insertions(+), 38 deletions(-) create mode 100644 crates/entity-derive-impl/src/entity/migrations/mod.rs create mode 100644 crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs diff --git a/crates/entity-derive-impl/src/entity.rs b/crates/entity-derive-impl/src/entity.rs index e2974ba..52bb772 100644 --- a/crates/entity-derive-impl/src/entity.rs +++ b/crates/entity-derive-impl/src/entity.rs @@ -68,6 +68,7 @@ mod events; mod hooks; mod insertable; mod mappers; +mod migrations; pub mod parse; mod policy; mod projection; @@ -110,6 +111,7 @@ fn generate(entity: EntityDef) -> TokenStream { let insertable = insertable::generate(&entity); let mappers = mappers::generate(&entity); let sql = sql::generate(&entity); + let migrations = migrations::generate(&entity); let expanded = quote! { #dto @@ -127,6 +129,7 @@ fn generate(entity: EntityDef) -> TokenStream { #insertable #mappers #sql + #migrations }; expanded.into() diff --git a/crates/entity-derive-impl/src/entity/migrations/mod.rs b/crates/entity-derive-impl/src/entity/migrations/mod.rs new file mode 100644 index 0000000..377b96e --- /dev/null +++ b/crates/entity-derive-impl/src/entity/migrations/mod.rs @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! Migration generation for entity-derive. +//! +//! Generates `MIGRATION_UP` and `MIGRATION_DOWN` constants containing +//! SQL DDL statements for creating/dropping tables. +//! +//! # Features +//! +//! - Full type mapping (Rust → PostgreSQL) +//! - Column constraints (UNIQUE, CHECK, DEFAULT) +//! - Indexes (btree, hash, gin, gist, brin) +//! - Foreign keys with ON DELETE actions +//! - Composite indexes +//! +//! # Usage +//! +//! ```rust,ignore +//! #[derive(Entity)] +//! #[entity(table = "users", migrations)] +//! pub struct User { +//! #[id] +//! pub id: Uuid, +//! +//! #[column(unique, index)] +//! pub email: String, +//! } +//! +//! // Apply migration: +//! sqlx::query(User::MIGRATION_UP).execute(&pool).await?; +//! ``` + +mod postgres; +pub mod types; + +use proc_macro2::TokenStream; + +use super::parse::{DatabaseDialect, EntityDef}; + +/// Generate migration constants based on entity configuration. +/// +/// Returns empty `TokenStream` if migrations are not enabled. +pub fn generate(entity: &EntityDef) -> TokenStream { + if !entity.migrations { + return TokenStream::new(); + } + + match entity.dialect { + DatabaseDialect::Postgres => postgres::generate(entity), + DatabaseDialect::ClickHouse => TokenStream::new(), // TODO: future + DatabaseDialect::MongoDB => TokenStream::new() // N/A for document DB + } +} diff --git a/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs b/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs new file mode 100644 index 0000000..fc9d175 --- /dev/null +++ b/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +//! DDL (Data Definition Language) generation for PostgreSQL. +//! +//! Generates CREATE TABLE, CREATE INDEX, and DROP TABLE statements. + +use convert_case::{Case, Casing}; + +use crate::entity::{ + migrations::types::{PostgresTypeMapper, TypeMapper}, + parse::{CompositeIndexDef, EntityDef, FieldDef} +}; + +/// Generate the complete UP migration SQL. +/// +/// Includes: +/// - CREATE TABLE with columns and constraints +/// - CREATE INDEX for single-column indexes +/// - CREATE INDEX for composite indexes +pub fn generate_up(entity: &EntityDef) -> String { + let mut sql = String::new(); + + // CREATE TABLE + sql.push_str(&generate_create_table(entity)); + + // Single-column indexes + for field in entity.all_fields() { + if field.column().has_index() { + sql.push_str(&generate_single_index(entity, field)); + } + } + + // Composite indexes + for idx in &entity.indexes { + sql.push_str(&generate_composite_index(entity, idx)); + } + + sql +} + +/// Generate the DOWN migration SQL. +pub fn generate_down(entity: &EntityDef) -> String { + format!( + "DROP TABLE IF EXISTS {} CASCADE;\n", + entity.full_table_name() + ) +} + +/// Generate CREATE TABLE statement. +fn generate_create_table(entity: &EntityDef) -> String { + let mapper = PostgresTypeMapper; + let full_table = entity.full_table_name(); + + let columns: Vec = entity + .all_fields() + .iter() + .map(|f| generate_column_def(f, &mapper, entity)) + .collect(); + + format!( + "CREATE TABLE IF NOT EXISTS {} (\n{}\n);\n", + full_table, + columns.join(",\n") + ) +} + +/// Generate a single column definition. +fn generate_column_def( + field: &FieldDef, + mapper: &PostgresTypeMapper, + entity: &EntityDef +) -> String { + let column_name = field.column_name(); + let sql_type = mapper.map_type(field.ty(), field.column()); + + let mut parts = vec![format!(" {}", column_name)]; + + // Type with array suffix + parts.push(sql_type.to_sql_string()); + + // PRIMARY KEY for #[id] fields + if field.is_id() { + parts.push("PRIMARY KEY".to_string()); + } else if !sql_type.nullable { + // NOT NULL unless nullable + parts.push("NOT NULL".to_string()); + } + + // UNIQUE constraint + if field.is_unique() { + parts.push("UNIQUE".to_string()); + } + + // DEFAULT value + if let Some(ref default) = field.column().default { + parts.push(format!("DEFAULT {}", default)); + } + + // CHECK constraint + if let Some(ref check) = field.column().check { + parts.push(format!("CHECK ({})", check)); + } + + // Foreign key REFERENCES from #[belongs_to] + if field.is_relation() + && let Some(parent) = field.belongs_to() + { + let parent_table = parent.to_string().to_case(Case::Snake); + // Use same schema as current entity for the reference + let ref_table = format!("{}.{}", entity.schema, pluralize(&parent_table)); + let mut fk_str = format!("REFERENCES {}(id)", ref_table); + + if let Some(action) = &field.storage.on_delete { + fk_str.push_str(&format!(" ON DELETE {}", action.as_sql())); + } + + parts.push(fk_str); + } + + parts.join(" ") +} + +/// Generate CREATE INDEX for a single column. +fn generate_single_index(entity: &EntityDef, field: &FieldDef) -> String { + let table = &entity.table; + let schema = &entity.schema; + let column = field.column_name(); + + let index_type = field.column().index.unwrap_or_default(); + let index_name = format!("idx_{}_{}", table, column); + let using = index_type.as_sql_using(); + + format!( + "CREATE INDEX IF NOT EXISTS {} ON {}.{}{} ({});\n", + index_name, schema, table, using, column + ) +} + +/// Generate CREATE INDEX for a composite index. +fn generate_composite_index(entity: &EntityDef, idx: &CompositeIndexDef) -> String { + let table = &entity.table; + let schema = &entity.schema; + + let index_name = idx.name_or_default(table); + let using = idx.index_type.as_sql_using(); + let unique_str = if idx.unique { "UNIQUE " } else { "" }; + let columns = idx.columns.join(", "); + + let mut sql = format!( + "CREATE {}INDEX IF NOT EXISTS {} ON {}.{}{} ({})", + unique_str, index_name, schema, table, using, columns + ); + + if let Some(ref where_clause) = idx.where_clause { + sql.push_str(&format!(" WHERE {}", where_clause)); + } + + sql.push_str(";\n"); + sql +} + +/// Simple pluralization for table names. +fn pluralize(s: &str) -> String { + if s.ends_with('s') || s.ends_with("sh") || s.ends_with("ch") || s.ends_with('x') { + format!("{}es", s) + } else if s.ends_with('y') && !s.ends_with("ay") && !s.ends_with("ey") && !s.ends_with("oy") { + format!("{}ies", &s[..s.len() - 1]) + } else { + format!("{}s", s) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pluralize_regular() { + assert_eq!(pluralize("user"), "users"); + assert_eq!(pluralize("post"), "posts"); + } + + #[test] + fn pluralize_es() { + assert_eq!(pluralize("status"), "statuses"); + assert_eq!(pluralize("match"), "matches"); + } + + #[test] + fn pluralize_ies() { + assert_eq!(pluralize("category"), "categories"); + assert_eq!(pluralize("company"), "companies"); + } + + #[test] + fn pluralize_ey_oy() { + assert_eq!(pluralize("key"), "keys"); + assert_eq!(pluralize("toy"), "toys"); + } +} diff --git a/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs b/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs index 0746954..1ba3a99 100644 --- a/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs +++ b/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs @@ -10,8 +10,7 @@ mod ddl; use proc_macro2::TokenStream; use quote::quote; -use crate::entity::parse::EntityDef; -use crate::utils::marker; +use crate::{entity::parse::EntityDef, utils::marker}; /// Generate migration constants for PostgreSQL. /// diff --git a/crates/entity-derive-impl/src/entity/migrations/types/mod.rs b/crates/entity-derive-impl/src/entity/migrations/types/mod.rs index b3d5404..9e50c00 100644 --- a/crates/entity-derive-impl/src/entity/migrations/types/mod.rs +++ b/crates/entity-derive-impl/src/entity/migrations/types/mod.rs @@ -28,10 +28,9 @@ mod postgres; pub use postgres::PostgresTypeMapper; - use syn::Type; -use crate::entity::parse::field::ColumnConfig; +use crate::entity::parse::ColumnConfig; /// Mapped SQL type representation. #[derive(Debug, Clone)] @@ -48,6 +47,7 @@ pub struct SqlType { impl SqlType { /// Create a non-nullable SQL type. + #[cfg(test)] #[must_use] pub fn new(name: impl Into) -> Self { Self { @@ -58,6 +58,7 @@ impl SqlType { } /// Create a nullable SQL type. + #[cfg(test)] #[must_use] pub fn nullable(name: impl Into) -> Self { Self { diff --git a/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs index 2348b59..5d9dd81 100644 --- a/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs +++ b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs @@ -30,7 +30,7 @@ use syn::Type; use super::{SqlType, TypeMapper}; -use crate::entity::parse::field::ColumnConfig; +use crate::entity::parse::ColumnConfig; /// PostgreSQL type mapper. /// @@ -165,12 +165,10 @@ fn extract_option_inner(ty: &Type) -> Option<&Type> { if let Type::Path(type_path) = ty && let Some(segment) = type_path.path.segments.last() && segment.ident == "Option" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return Some(inner); - } - } + return Some(inner); } None } @@ -180,12 +178,10 @@ fn extract_vec_inner(ty: &Type) -> Option<&Type> { if let Type::Path(type_path) = ty && let Some(segment) = type_path.path.segments.last() && segment.ident == "Vec" + && let syn::PathArguments::AngleBracketed(args) = &segment.arguments + && let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return Some(inner); - } - } + return Some(inner); } None } diff --git a/crates/entity-derive-impl/src/entity/parse.rs b/crates/entity-derive-impl/src/entity/parse.rs index 5489b00..e3ca52e 100644 --- a/crates/entity-derive-impl/src/entity/parse.rs +++ b/crates/entity-derive-impl/src/entity/parse.rs @@ -120,10 +120,10 @@ mod uuid_version; pub use api::ApiConfig; pub use command::{CommandDef, CommandKindHint, CommandSource}; pub use dialect::DatabaseDialect; -pub use entity::{EntityDef, ProjectionDef}; +pub use entity::{CompositeIndexDef, EntityDef, ProjectionDef}; #[allow(unused_imports)] // Will be used for OpenAPI schema examples (#80) pub use field::ExampleValue; -pub use field::{FieldDef, FilterType}; +pub use field::{ColumnConfig, FieldDef, FilterType}; pub use returning::ReturningMode; pub use sql_level::SqlLevel; pub use uuid_version::UuidVersion; diff --git a/crates/entity-derive-impl/src/entity/parse/entity.rs b/crates/entity-derive-impl/src/entity/parse/entity.rs index 6de7068..c360ce5 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity.rs @@ -147,7 +147,7 @@ mod projection; pub use attrs::EntityAttrs; pub use def::EntityDef; -pub use index::{CompositeIndexDef, parse_index_meta}; +pub use index::CompositeIndexDef; pub use projection::{ProjectionDef, parse_projection_attrs}; #[cfg(test)] diff --git a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs index 074f2d6..10fca36 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs @@ -58,9 +58,13 @@ use syn::{Attribute, Ident}; -use super::super::api::{ApiConfig, parse_api_config}; -use super::super::field::IndexType; -use super::CompositeIndexDef; +use super::{ + super::{ + api::{ApiConfig, parse_api_config}, + field::IndexType + }, + CompositeIndexDef +}; /// Parse `#[has_many(Entity)]` attributes from struct attributes. /// @@ -183,14 +187,14 @@ pub fn parse_index_attrs(attrs: &[Attribute]) -> Vec { } let _ = attr.parse_nested_meta(|meta| { - if meta.path.is_ident("index") { - if let Ok(idx) = parse_index_content(&meta, false) { - indexes.push(idx); - } - } else if meta.path.is_ident("unique_index") { - if let Ok(idx) = parse_index_content(&meta, true) { - indexes.push(idx); - } + if meta.path.is_ident("index") + && let Ok(idx) = parse_index_content(&meta, false) + { + indexes.push(idx); + } else if meta.path.is_ident("unique_index") + && let Ok(idx) = parse_index_content(&meta, true) + { + indexes.push(idx); } Ok(()) }); diff --git a/crates/entity-derive-impl/src/entity/parse/entity/index.rs b/crates/entity-derive-impl/src/entity/parse/entity/index.rs index 72e9867..a6fe2d6 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/index.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/index.rs @@ -3,7 +3,8 @@ //! Composite index definitions for entity-level indexes. //! -//! Parsed from `#[entity(index(...))]` and `#[entity(unique_index(...))]` attributes. +//! Parsed from `#[entity(index(...))]` and `#[entity(unique_index(...))]` +//! attributes. //! //! # Examples //! @@ -48,30 +49,33 @@ pub struct CompositeIndexDef { impl CompositeIndexDef { /// Create a new non-unique btree index. + #[cfg(test)] #[must_use] pub fn new(columns: Vec) -> Self { Self { - name: None, + name: None, columns, - index_type: IndexType::default(), - unique: false, + index_type: IndexType::default(), + unique: false, where_clause: None } } /// Create a new unique btree index. + #[cfg(test)] #[must_use] pub fn unique(columns: Vec) -> Self { Self { - name: None, + name: None, columns, - index_type: IndexType::default(), - unique: true, + index_type: IndexType::default(), + unique: true, where_clause: None } } /// Set the index name. + #[cfg(test)] #[must_use] pub fn with_name(mut self, name: String) -> Self { self.name = Some(name); @@ -79,6 +83,7 @@ impl CompositeIndexDef { } /// Set the index type. + #[cfg(test)] #[must_use] pub fn with_type(mut self, index_type: IndexType) -> Self { self.index_type = index_type; @@ -86,6 +91,7 @@ impl CompositeIndexDef { } /// Set the WHERE clause for partial index. + #[cfg(test)] #[must_use] pub fn with_where(mut self, where_clause: String) -> Self { self.where_clause = Some(where_clause); @@ -109,6 +115,7 @@ impl CompositeIndexDef { } /// Check if this is a partial index. + #[cfg(test)] #[must_use] pub fn is_partial(&self) -> bool { self.where_clause.is_some() @@ -128,6 +135,7 @@ impl CompositeIndexDef { /// index(col1, where = "condition") /// unique_index(col1, col2) /// ``` +#[allow(dead_code)] // Will be used when full index parsing is integrated pub fn parse_index_meta( meta: syn::meta::ParseNestedMeta<'_>, unique: bool diff --git a/crates/entity-derive-impl/src/entity/parse/field.rs b/crates/entity-derive-impl/src/entity/parse/field.rs index 909fd00..6596bb8 100644 --- a/crates/entity-derive-impl/src/entity/parse/field.rs +++ b/crates/entity-derive-impl/src/entity/parse/field.rs @@ -43,7 +43,8 @@ pub use validation::ValidationConfig; use crate::utils::docs::extract_doc_comments; -/// Parse `#[belongs_to(EntityName)]` or `#[belongs_to(EntityName, on_delete = "cascade")]`. +/// Parse `#[belongs_to(EntityName)]` or `#[belongs_to(EntityName, on_delete = +/// "cascade")]`. /// /// Returns the entity identifier and optional ON DELETE action. fn parse_belongs_to(attr: &Attribute) -> (Option, Option) { @@ -334,6 +335,7 @@ impl FieldDef { /// Check if this column should be indexed. #[must_use] + #[allow(dead_code)] // Public API for future use pub fn has_index(&self) -> bool { self.column.has_index() } diff --git a/crates/entity-derive-impl/src/entity/parse/field/column.rs b/crates/entity-derive-impl/src/entity/parse/field/column.rs index b866a7c..a94c98a 100644 --- a/crates/entity-derive-impl/src/entity/parse/field/column.rs +++ b/crates/entity-derive-impl/src/entity/parse/field/column.rs @@ -233,6 +233,7 @@ impl ColumnConfig { /// Check if this column has any constraints. #[must_use] + #[allow(dead_code)] // Public API for future use pub fn has_constraints(&self) -> bool { self.unique || self.check.is_some() } diff --git a/crates/entity-derive-impl/src/lib.rs b/crates/entity-derive-impl/src/lib.rs index 083d65f..e5bd696 100644 --- a/crates/entity-derive-impl/src/lib.rs +++ b/crates/entity-derive-impl/src/lib.rs @@ -399,7 +399,7 @@ use proc_macro::TokenStream; Entity, attributes( entity, field, id, auto, validate, belongs_to, has_many, projection, filter, command, - example + example, column ) )] pub fn derive_entity(input: TokenStream) -> TokenStream { From c047e292717f5d4fc0588ec555cec0a04d6a7da3 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:40:41 +0700 Subject: [PATCH 05/11] test(migrations): add integration tests for migration generation --- .../tests/cases/pass/migrations_basic.rs | 37 ++++++++++++ .../cases/pass/migrations_constraints.rs | 56 +++++++++++++++++++ .../tests/cases/pass/migrations_relations.rs | 40 +++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 crates/entity-derive/tests/cases/pass/migrations_basic.rs create mode 100644 crates/entity-derive/tests/cases/pass/migrations_constraints.rs create mode 100644 crates/entity-derive/tests/cases/pass/migrations_relations.rs diff --git a/crates/entity-derive/tests/cases/pass/migrations_basic.rs b/crates/entity-derive/tests/cases/pass/migrations_basic.rs new file mode 100644 index 0000000..709f08f --- /dev/null +++ b/crates/entity-derive/tests/cases/pass/migrations_basic.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +use chrono::{DateTime, Utc}; +use entity_derive::Entity; +use uuid::Uuid; + +#[derive(Entity)] +#[entity(table = "users", schema = "core", migrations)] +pub struct User { + #[id] + pub id: Uuid, + + #[field(create, update, response)] + pub name: String, + + #[field(create, response)] + pub email: String, + + #[field(response)] + #[auto] + pub created_at: DateTime, +} + +fn main() { + // Verify MIGRATION_UP is generated and contains expected SQL + let up = User::MIGRATION_UP; + assert!(up.contains("CREATE TABLE IF NOT EXISTS core.users")); + assert!(up.contains("id UUID PRIMARY KEY")); + assert!(up.contains("name TEXT NOT NULL")); + assert!(up.contains("email TEXT NOT NULL")); + assert!(up.contains("created_at TIMESTAMPTZ NOT NULL")); + + // Verify MIGRATION_DOWN is generated + let down = User::MIGRATION_DOWN; + assert!(down.contains("DROP TABLE IF EXISTS core.users CASCADE")); +} diff --git a/crates/entity-derive/tests/cases/pass/migrations_constraints.rs b/crates/entity-derive/tests/cases/pass/migrations_constraints.rs new file mode 100644 index 0000000..184deee --- /dev/null +++ b/crates/entity-derive/tests/cases/pass/migrations_constraints.rs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +use entity_derive::Entity; +use uuid::Uuid; + +#[derive(Entity)] +#[entity(table = "products", migrations)] +pub struct Product { + #[id] + pub id: Uuid, + + #[field(create, update, response)] + #[column(unique)] + pub sku: String, + + #[field(create, update, response)] + #[column(varchar = 200)] + pub name: String, + + #[field(create, update, response)] + #[column(default = "0")] + pub quantity: i32, + + #[field(create, update, response)] + #[column(check = "price >= 0")] + pub price: f64, + + #[field(create, update, response)] + #[column(index)] + pub category: String, + + #[field(create, update, response)] + #[column(index = "gin")] + pub tags: Vec, +} + +fn main() { + let up = Product::MIGRATION_UP; + + // Check UNIQUE constraint + assert!(up.contains("sku TEXT NOT NULL UNIQUE")); + + // Check VARCHAR + assert!(up.contains("name VARCHAR(200) NOT NULL")); + + // Check DEFAULT + assert!(up.contains("quantity INTEGER NOT NULL DEFAULT 0")); + + // Check CHECK constraint + assert!(up.contains("price DOUBLE PRECISION NOT NULL CHECK (price >= 0)")); + + // Check indexes are generated + assert!(up.contains("CREATE INDEX IF NOT EXISTS idx_products_category")); + assert!(up.contains("USING gin")); +} diff --git a/crates/entity-derive/tests/cases/pass/migrations_relations.rs b/crates/entity-derive/tests/cases/pass/migrations_relations.rs new file mode 100644 index 0000000..2d59d3a --- /dev/null +++ b/crates/entity-derive/tests/cases/pass/migrations_relations.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025-2026 RAprogramm +// SPDX-License-Identifier: MIT + +use entity_derive::Entity; +use uuid::Uuid; + +// Stub types for belongs_to references +pub struct User; +pub struct Category; + +#[derive(Entity)] +#[entity(table = "posts", schema = "blog", migrations, sql = "none")] +pub struct Post { + #[id] + pub id: Uuid, + + #[field(create, update, response)] + pub title: String, + + #[field(create, response)] + #[belongs_to(User, on_delete = "cascade")] + pub author_id: Uuid, + + #[field(create, response)] + #[belongs_to(Category, on_delete = "set null")] + pub category_id: Option, +} + +fn main() { + let up = Post::MIGRATION_UP; + + // Check table creation + assert!(up.contains("CREATE TABLE IF NOT EXISTS blog.posts")); + + // Check foreign key with CASCADE + assert!(up.contains("author_id UUID NOT NULL REFERENCES blog.users(id) ON DELETE CASCADE")); + + // Check foreign key with SET NULL (nullable field) + assert!(up.contains("category_id UUID REFERENCES blog.categories(id) ON DELETE SET NULL")); +} From b230b35a5a7227f4a787660e8aa74db3f28b626c Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:42:26 +0700 Subject: [PATCH 06/11] docs: document migrations and column attribute --- crates/entity-derive-impl/src/lib.rs | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/entity-derive-impl/src/lib.rs b/crates/entity-derive-impl/src/lib.rs index e5bd696..e63a57d 100644 --- a/crates/entity-derive-impl/src/lib.rs +++ b/crates/entity-derive-impl/src/lib.rs @@ -256,6 +256,7 @@ use proc_macro::TokenStream; /// | `sql` | No | `"full"` | SQL generation: `"full"`, `"trait"`, or `"none"` | /// | `dialect` | No | `"postgres"` | Database dialect: `"postgres"`, `"clickhouse"`, `"mongodb"` | /// | `uuid` | No | `"v7"` | UUID version for ID: `"v7"` (time-ordered) or `"v4"` (random) | +/// | `migrations` | No | `false` | Generate `MIGRATION_UP` and `MIGRATION_DOWN` constants | /// /// # Field Attributes /// @@ -268,11 +269,18 @@ use proc_macro::TokenStream; /// | `#[field(response)]` | Include in `Response`. | /// | `#[field(skip)]` | Exclude from ALL DTOs. Use for sensitive data. | /// | `#[belongs_to(Entity)]` | Foreign key relation. Generates `find_{entity}` method in repository. | +/// | `#[belongs_to(Entity, on_delete = "...")]` | Foreign key with ON DELETE action (`cascade`, `set null`, `restrict`). | /// | `#[has_many(Entity)]` | One-to-many relation (entity-level). Generates `find_{entities}` method. | /// | `#[projection(Name: f1, f2)]` | Entity-level. Defines a projection struct with specified fields. | /// | `#[filter]` | Exact match filter. Generates field in Query struct with `=` comparison. | /// | `#[filter(like)]` | ILIKE pattern filter. Generates field for text pattern matching. | /// | `#[filter(range)]` | Range filter. Generates `field_from` and `field_to` fields. | +/// | `#[column(unique)]` | Add UNIQUE constraint in migrations. | +/// | `#[column(index)]` | Add btree index in migrations. | +/// | `#[column(index = "gin")]` | Add index with specific type (btree, hash, gin, gist, brin). | +/// | `#[column(default = "...")]` | Set DEFAULT value in migrations. | +/// | `#[column(check = "...")]` | Add CHECK constraint in migrations. | +/// | `#[column(varchar = N)]` | Use VARCHAR(N) instead of TEXT in migrations. | /// /// Multiple attributes can be combined: `#[field(create, update, response)]` /// @@ -357,6 +365,41 @@ use proc_macro::TokenStream; /// // No repository trait or SQL implementation /// ``` /// +/// ## Migration Generation +/// +/// Generate compile-time SQL migrations with `migrations`: +/// +/// ```rust,ignore +/// #[derive(Entity)] +/// #[entity(table = "products", migrations)] +/// pub struct Product { +/// #[id] +/// pub id: Uuid, +/// +/// #[field(create, update, response)] +/// #[column(unique, index)] +/// pub sku: String, +/// +/// #[field(create, update, response)] +/// #[column(varchar = 200)] +/// pub name: String, +/// +/// #[field(create, update, response)] +/// #[column(check = "price >= 0")] +/// pub price: f64, +/// +/// #[belongs_to(Category, on_delete = "cascade")] +/// pub category_id: Uuid, +/// } +/// +/// // Generated constants: +/// // Product::MIGRATION_UP - CREATE TABLE, indexes, constraints +/// // Product::MIGRATION_DOWN - DROP TABLE CASCADE +/// +/// // Apply migration: +/// sqlx::query(Product::MIGRATION_UP).execute(&pool).await?; +/// ``` +/// /// # Security /// /// Use `#[field(skip)]` to prevent sensitive data from leaking: From 426498eedda879c6989e976ba90d944e25533f90 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 08:47:47 +0700 Subject: [PATCH 07/11] fix: clippy warnings and REUSE compliance --- .../src/entity/migrations/types/postgres.rs | 12 ++++++++---- logo.png.license | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 logo.png.license diff --git a/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs index 5d9dd81..8e0582a 100644 --- a/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs +++ b/crates/entity-derive-impl/src/entity/migrations/types/postgres.rs @@ -218,8 +218,10 @@ mod tests { #[test] fn map_string_varchar() { - let mut column = ColumnConfig::default(); - column.varchar = Some(255); + let column = ColumnConfig { + varchar: Some(255), + ..Default::default() + }; let ty = map_type_with_column(quote::quote! { String }, column); assert_eq!(ty.name, "VARCHAR(255)"); } @@ -291,8 +293,10 @@ mod tests { #[test] fn map_explicit_sql_type() { - let mut column = ColumnConfig::default(); - column.sql_type = Some("CITEXT".to_string()); + let column = ColumnConfig { + sql_type: Some("CITEXT".to_string()), + ..Default::default() + }; let ty = map_type_with_column(quote::quote! { String }, column); assert_eq!(ty.name, "CITEXT"); } diff --git a/logo.png.license b/logo.png.license new file mode 100644 index 0000000..f4247d6 --- /dev/null +++ b/logo.png.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2025-2026 RAprogramm +SPDX-License-Identifier: MIT From ea6fe2e45134ad256617f51b6130d5f07f7487a5 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 09:24:18 +0700 Subject: [PATCH 08/11] test: add comprehensive tests for migration and SQL helper modules - Add unit tests for sql/postgres/helpers.rs (100% coverage) - Add tests for migrations/mod.rs (100% coverage) - Add tests for migrations/postgres/ddl.rs (100% coverage) - Add tests for parse/field/column.rs IndexType::from_str - Add tests for parse/entity/index.rs builder methods - Export IndexType from parse module for test usage --- .../src/entity/migrations/mod.rs | 64 ++++ .../src/entity/migrations/postgres/ddl.rs | 311 ++++++++++++++++++ crates/entity-derive-impl/src/entity/parse.rs | 3 +- .../src/entity/parse/entity/index.rs | 87 +++++ .../src/entity/parse/field/column.rs | 35 ++ .../src/entity/sql/postgres/helpers.rs | 234 +++++++++++++ 6 files changed, 733 insertions(+), 1 deletion(-) diff --git a/crates/entity-derive-impl/src/entity/migrations/mod.rs b/crates/entity-derive-impl/src/entity/migrations/mod.rs index 377b96e..7c0a777 100644 --- a/crates/entity-derive-impl/src/entity/migrations/mod.rs +++ b/crates/entity-derive-impl/src/entity/migrations/mod.rs @@ -52,3 +52,67 @@ pub fn generate(entity: &EntityDef) -> TokenStream { DatabaseDialect::MongoDB => TokenStream::new() // N/A for document DB } } + +#[cfg(test)] +mod tests { + use syn::DeriveInput; + + use super::*; + + fn parse_entity(tokens: proc_macro2::TokenStream) -> EntityDef { + let input: DeriveInput = syn::parse_quote!(#tokens); + EntityDef::from_derive_input(&input).unwrap() + } + + #[test] + fn generate_returns_empty_when_migrations_disabled() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users")] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let result = generate(&entity); + assert!(result.is_empty()); + } + + #[test] + fn generate_returns_tokens_when_migrations_enabled() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let result = generate(&entity); + assert!(!result.is_empty()); + } + + #[test] + fn generate_returns_empty_for_clickhouse() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", dialect = "clickhouse", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let result = generate(&entity); + assert!(result.is_empty()); + } + + #[test] + fn generate_returns_empty_for_mongodb() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", dialect = "mongodb", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let result = generate(&entity); + assert!(result.is_empty()); + } +} diff --git a/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs b/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs index fc9d175..b6cf425 100644 --- a/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs +++ b/crates/entity-derive-impl/src/entity/migrations/postgres/ddl.rs @@ -173,7 +173,15 @@ fn pluralize(s: &str) -> String { #[cfg(test)] mod tests { + use syn::DeriveInput; + use super::*; + use crate::entity::parse::EntityDef; + + fn parse_entity(tokens: proc_macro2::TokenStream) -> EntityDef { + let input: DeriveInput = syn::parse_quote!(#tokens); + EntityDef::from_derive_input(&input).unwrap() + } #[test] fn pluralize_regular() { @@ -198,4 +206,307 @@ mod tests { assert_eq!(pluralize("key"), "keys"); assert_eq!(pluralize("toy"), "toys"); } + + #[test] + fn pluralize_sh() { + assert_eq!(pluralize("wish"), "wishes"); + assert_eq!(pluralize("bush"), "bushes"); + } + + #[test] + fn pluralize_x() { + assert_eq!(pluralize("box"), "boxes"); + assert_eq!(pluralize("fox"), "foxes"); + } + + #[test] + fn pluralize_ay() { + assert_eq!(pluralize("day"), "days"); + assert_eq!(pluralize("way"), "ways"); + } + + #[test] + fn generate_up_basic() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + pub name: String, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("CREATE TABLE IF NOT EXISTS public.users")); + assert!(sql.contains("id UUID PRIMARY KEY")); + assert!(sql.contains("name TEXT NOT NULL")); + } + + #[test] + fn generate_down_basic() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", schema = "core", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let sql = generate_down(&entity); + assert_eq!(sql, "DROP TABLE IF EXISTS core.users CASCADE;\n"); + } + + #[test] + fn generate_up_with_unique() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(unique)] + pub email: String, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("email TEXT NOT NULL UNIQUE")); + } + + #[test] + fn generate_up_with_default() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(default = "true")] + pub active: bool, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("DEFAULT true")); + } + + #[test] + fn generate_up_with_check() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(check = "age >= 0")] + pub age: i32, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("CHECK (age >= 0)")); + } + + #[test] + fn generate_up_with_index() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(index)] + pub status: String, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("CREATE INDEX IF NOT EXISTS idx_users_status")); + } + + #[test] + fn generate_up_with_gin_index() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(index = "gin")] + pub tags: Vec, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("USING gin")); + } + + #[test] + fn generate_up_with_nullable() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + pub bio: Option, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("bio TEXT")); + assert!(!sql.contains("bio TEXT NOT NULL")); + } + + #[test] + fn generate_up_with_varchar() { + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[column(varchar = 100)] + pub name: String, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("VARCHAR(100)")); + } + + #[test] + fn generate_up_with_belongs_to() { + let entity = parse_entity(quote::quote! { + #[entity(table = "posts", migrations)] + pub struct Post { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[belongs_to(User)] + pub user_id: uuid::Uuid, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("REFERENCES public.users(id)")); + } + + #[test] + fn generate_up_with_belongs_to_on_delete_cascade() { + let entity = parse_entity(quote::quote! { + #[entity(table = "posts", migrations)] + pub struct Post { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + #[belongs_to(User, on_delete = "cascade")] + pub user_id: uuid::Uuid, + } + }); + let sql = generate_up(&entity); + assert!(sql.contains("REFERENCES public.users(id) ON DELETE CASCADE")); + } + + #[test] + fn generate_composite_index_basic() { + let idx = CompositeIndexDef { + name: None, + columns: vec!["name".to_string(), "email".to_string()], + index_type: crate::entity::parse::IndexType::BTree, + unique: false, + where_clause: None + }; + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + pub name: String, + #[field(create, response)] + pub email: String, + } + }); + let sql = generate_composite_index(&entity, &idx); + assert!(sql.contains("CREATE INDEX IF NOT EXISTS idx_users_name_email")); + assert!(sql.contains("(name, email)")); + } + + #[test] + fn generate_composite_index_unique() { + let idx = CompositeIndexDef { + name: None, + columns: vec!["tenant_id".to_string(), "email".to_string()], + index_type: crate::entity::parse::IndexType::BTree, + unique: true, + where_clause: None + }; + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let sql = generate_composite_index(&entity, &idx); + assert!(sql.contains("CREATE UNIQUE INDEX")); + assert!(sql.contains("(tenant_id, email)")); + } + + #[test] + fn generate_composite_index_with_where() { + let idx = CompositeIndexDef { + name: Some("idx_active_users".to_string()), + columns: vec!["email".to_string()], + index_type: crate::entity::parse::IndexType::BTree, + unique: false, + where_clause: Some("active = true".to_string()) + }; + let entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + } + }); + let sql = generate_composite_index(&entity, &idx); + assert!(sql.contains("idx_active_users")); + assert!(sql.contains("WHERE active = true")); + } + + #[test] + fn generate_composite_index_gin() { + let idx = CompositeIndexDef { + name: None, + columns: vec!["tags".to_string()], + index_type: crate::entity::parse::IndexType::Gin, + unique: false, + where_clause: None + }; + let entity = parse_entity(quote::quote! { + #[entity(table = "posts", migrations)] + pub struct Post { + #[id] + pub id: uuid::Uuid, + } + }); + let sql = generate_composite_index(&entity, &idx); + assert!(sql.contains("USING gin")); + } + + #[test] + fn generate_up_with_composite_indexes() { + let mut entity = parse_entity(quote::quote! { + #[entity(table = "users", migrations)] + pub struct User { + #[id] + pub id: uuid::Uuid, + #[field(create, response)] + pub name: String, + #[field(create, response)] + pub email: String, + } + }); + entity.indexes.push(CompositeIndexDef { + name: None, + columns: vec!["name".to_string(), "email".to_string()], + index_type: crate::entity::parse::IndexType::BTree, + unique: false, + where_clause: None + }); + let sql = generate_up(&entity); + assert!(sql.contains("CREATE INDEX IF NOT EXISTS idx_users_name_email")); + } } diff --git a/crates/entity-derive-impl/src/entity/parse.rs b/crates/entity-derive-impl/src/entity/parse.rs index e3ca52e..31112f4 100644 --- a/crates/entity-derive-impl/src/entity/parse.rs +++ b/crates/entity-derive-impl/src/entity/parse.rs @@ -123,7 +123,8 @@ pub use dialect::DatabaseDialect; pub use entity::{CompositeIndexDef, EntityDef, ProjectionDef}; #[allow(unused_imports)] // Will be used for OpenAPI schema examples (#80) pub use field::ExampleValue; -pub use field::{ColumnConfig, FieldDef, FilterType}; +#[allow(unused_imports)] // Re-exported for migration generation tests +pub use field::{ColumnConfig, FieldDef, FilterType, IndexType}; pub use returning::ReturningMode; pub use sql_level::SqlLevel; pub use uuid_version::UuidVersion; diff --git a/crates/entity-derive-impl/src/entity/parse/entity/index.rs b/crates/entity-derive-impl/src/entity/parse/entity/index.rs index a6fe2d6..36dd8cc 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/index.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/index.rs @@ -243,4 +243,91 @@ mod tests { let idx = CompositeIndexDef::new(vec!["status".to_string()]); assert_eq!(idx.name_or_default("users"), "idx_users_status"); } + + #[test] + fn is_partial_false_without_where() { + let idx = CompositeIndexDef::new(vec!["status".to_string()]); + assert!(!idx.is_partial()); + } + + #[test] + fn with_type_gist() { + let idx = CompositeIndexDef::new(vec!["location".to_string()]).with_type(IndexType::Gist); + assert_eq!(idx.index_type, IndexType::Gist); + } + + #[test] + fn with_type_brin() { + let idx = + CompositeIndexDef::new(vec!["created_at".to_string()]).with_type(IndexType::Brin); + assert_eq!(idx.index_type, IndexType::Brin); + } + + #[test] + fn with_type_hash() { + let idx = CompositeIndexDef::new(vec!["key".to_string()]).with_type(IndexType::Hash); + assert_eq!(idx.index_type, IndexType::Hash); + } + + #[test] + fn single_column_index() { + let idx = CompositeIndexDef::new(vec!["email".to_string()]); + assert_eq!(idx.columns.len(), 1); + assert_eq!(idx.default_name("users"), "idx_users_email"); + } + + #[test] + fn multiple_columns_index() { + let idx = CompositeIndexDef::new(vec![ + "tenant_id".to_string(), + "user_id".to_string(), + "email".to_string(), + ]); + assert_eq!(idx.columns.len(), 3); + assert_eq!( + idx.default_name("users"), + "idx_users_tenant_id_user_id_email" + ); + } + + #[test] + fn unique_with_custom_name() { + let idx = CompositeIndexDef::unique(vec!["email".to_string()]) + .with_name("unique_email_idx".to_string()); + assert!(idx.unique); + assert_eq!(idx.name_or_default("users"), "unique_email_idx"); + } + + #[test] + fn unique_partial_index() { + let idx = CompositeIndexDef::unique(vec!["email".to_string()]) + .with_where("deleted_at IS NULL".to_string()); + assert!(idx.unique); + assert!(idx.is_partial()); + assert_eq!(idx.where_clause, Some("deleted_at IS NULL".to_string())); + } + + #[test] + fn composite_index_all_options() { + let idx = CompositeIndexDef::unique(vec!["tenant_id".to_string(), "email".to_string()]) + .with_name("idx_tenant_email".to_string()) + .with_type(IndexType::BTree) + .with_where("active = true".to_string()); + assert!(idx.unique); + assert!(idx.is_partial()); + assert_eq!(idx.name, Some("idx_tenant_email".to_string())); + assert_eq!(idx.index_type, IndexType::BTree); + assert_eq!(idx.where_clause, Some("active = true".to_string())); + } + + #[test] + fn chained_builder_pattern() { + let idx = CompositeIndexDef::new(vec!["col".to_string()]) + .with_name("my_idx".to_string()) + .with_type(IndexType::Gin) + .with_where("x > 0".to_string()); + assert_eq!(idx.name, Some("my_idx".to_string())); + assert_eq!(idx.index_type, IndexType::Gin); + assert_eq!(idx.where_clause, Some("x > 0".to_string())); + } } diff --git a/crates/entity-derive-impl/src/entity/parse/field/column.rs b/crates/entity-derive-impl/src/entity/parse/field/column.rs index a94c98a..99a50c2 100644 --- a/crates/entity-derive-impl/src/entity/parse/field/column.rs +++ b/crates/entity-derive-impl/src/entity/parse/field/column.rs @@ -392,6 +392,41 @@ mod tests { assert_eq!(IndexType::Brin.as_sql_using(), " USING brin"); } + #[test] + fn index_type_from_str_all() { + assert_eq!(IndexType::from_str("btree"), Some(IndexType::BTree)); + assert_eq!(IndexType::from_str("b-tree"), Some(IndexType::BTree)); + assert_eq!(IndexType::from_str("BTREE"), Some(IndexType::BTree)); + assert_eq!(IndexType::from_str("hash"), Some(IndexType::Hash)); + assert_eq!(IndexType::from_str("HASH"), Some(IndexType::Hash)); + assert_eq!(IndexType::from_str("gin"), Some(IndexType::Gin)); + assert_eq!(IndexType::from_str("GIN"), Some(IndexType::Gin)); + assert_eq!(IndexType::from_str("gist"), Some(IndexType::Gist)); + assert_eq!(IndexType::from_str("GIST"), Some(IndexType::Gist)); + assert_eq!(IndexType::from_str("brin"), Some(IndexType::Brin)); + assert_eq!(IndexType::from_str("BRIN"), Some(IndexType::Brin)); + assert_eq!(IndexType::from_str("invalid"), None); + assert_eq!(IndexType::from_str("unknown"), None); + } + + #[test] + fn parse_index_gist() { + let config = parse_column_attr(quote! { index = "gist" }); + assert_eq!(config.index, Some(IndexType::Gist)); + } + + #[test] + fn parse_index_brin() { + let config = parse_column_attr(quote! { index = "brin" }); + assert_eq!(config.index, Some(IndexType::Brin)); + } + + #[test] + fn parse_index_unknown_defaults_to_btree() { + let config = parse_column_attr(quote! { index = "unknown" }); + assert_eq!(config.index, Some(IndexType::BTree)); + } + #[test] fn referential_action_from_str() { assert_eq!( diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs index 512ec4a..ca7aade 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs @@ -209,3 +209,237 @@ pub fn generate_query_bindings(fields: &[&FieldDef]) -> TokenStream { quote! { #(#bindings)* } } + +#[cfg(test)] +mod tests { + use syn::{Field, parse_quote}; + + use super::*; + use crate::entity::parse::FieldDef; + + fn parse_field(tokens: proc_macro2::TokenStream) -> FieldDef { + let field: Field = parse_quote!(#tokens); + FieldDef::from_field(&field).unwrap() + } + + #[test] + fn join_columns_single() { + let field = parse_field(quote! { pub name: String }); + let result = join_columns(&[field]); + assert_eq!(result, "name"); + } + + #[test] + fn join_columns_multiple() { + let fields = vec![ + parse_field(quote! { pub id: Uuid }), + parse_field(quote! { pub name: String }), + parse_field(quote! { pub email: String }), + ]; + let result = join_columns(&fields); + assert_eq!(result, "id, name, email"); + } + + #[test] + fn join_columns_empty() { + let result = join_columns(&[]); + assert_eq!(result, ""); + } + + #[test] + fn insert_bindings_generates_bind_calls() { + let fields = vec![ + parse_field(quote! { pub id: Uuid }), + parse_field(quote! { pub name: String }), + ]; + let bindings = insert_bindings(&fields); + assert_eq!(bindings.len(), 2); + + let first = bindings[0].to_string(); + assert!(first.contains("bind"), "Expected 'bind' in: {}", first); + assert!( + first.contains("insertable"), + "Expected 'insertable' in: {}", + first + ); + assert!(first.contains("id"), "Expected 'id' in: {}", first); + + let second = bindings[1].to_string(); + assert!(second.contains("bind"), "Expected 'bind' in: {}", second); + assert!( + second.contains("insertable"), + "Expected 'insertable' in: {}", + second + ); + assert!(second.contains("name"), "Expected 'name' in: {}", second); + } + + #[test] + fn insert_bindings_empty() { + let bindings = insert_bindings(&[]); + assert!(bindings.is_empty()); + } + + #[test] + fn update_bindings_generates_bind_calls() { + let fields = vec![ + parse_field(quote! { pub name: String }), + parse_field(quote! { pub email: String }), + ]; + let refs: Vec<&FieldDef> = fields.iter().collect(); + let bindings = update_bindings(&refs); + assert_eq!(bindings.len(), 2); + + let first = bindings[0].to_string(); + assert!(first.contains("bind"), "Expected 'bind' in: {}", first); + assert!(first.contains("dto"), "Expected 'dto' in: {}", first); + assert!(first.contains("name"), "Expected 'name' in: {}", first); + } + + #[test] + fn update_bindings_empty() { + let bindings = update_bindings(&[]); + assert!(bindings.is_empty()); + } + + #[test] + fn where_conditions_eq_filter() { + let field = parse_field(quote! { + #[filter(eq)] + pub status: String + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_where_conditions(&refs, false); + let code = result.to_string(); + assert!(code.contains("query . status . is_some")); + assert!(code.contains("= $")); + } + + #[test] + fn where_conditions_like_filter() { + let field = parse_field(quote! { + #[filter(like)] + pub name: String + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_where_conditions(&refs, false); + let code = result.to_string(); + assert!(code.contains("query . name . is_some")); + assert!(code.contains("ILIKE")); + } + + #[test] + fn where_conditions_range_filter() { + let field = parse_field(quote! { + #[filter(range)] + pub age: i32 + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_where_conditions(&refs, false); + let code = result.to_string(); + assert!(code.contains("age_from")); + assert!(code.contains("age_to")); + assert!(code.contains(">=")); + assert!(code.contains("<=")); + } + + #[test] + fn where_conditions_none_filter() { + let field = parse_field(quote! { pub name: String }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_where_conditions(&refs, false); + let code = result.to_string(); + // No conditions for None filter + assert!(!code.contains("query")); + } + + #[test] + fn where_conditions_with_soft_delete() { + let result = generate_where_conditions(&[], true); + let code = result.to_string(); + assert!(code.contains("deleted_at IS NULL")); + } + + #[test] + fn where_conditions_without_soft_delete() { + let result = generate_where_conditions(&[], false); + let code = result.to_string(); + assert!(!code.contains("deleted_at")); + } + + #[test] + fn query_bindings_eq_filter() { + let field = parse_field(quote! { + #[filter(eq)] + pub status: String + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_query_bindings(&refs); + let code = result.to_string(); + assert!(code.contains("if let Some (ref v) = query . status")); + assert!(code.contains("q = q . bind (v)")); + } + + #[test] + fn query_bindings_like_filter() { + let field = parse_field(quote! { + #[filter(like)] + pub name: String + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_query_bindings(&refs); + let code = result.to_string(); + assert!(code.contains("escaped")); + assert!(code.contains("format !")); + } + + #[test] + fn query_bindings_range_filter() { + let field = parse_field(quote! { + #[filter(range)] + pub age: i32 + }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_query_bindings(&refs); + let code = result.to_string(); + assert!(code.contains("age_from")); + assert!(code.contains("age_to")); + } + + #[test] + fn query_bindings_none_filter() { + let field = parse_field(quote! { pub name: String }); + let refs: Vec<&FieldDef> = vec![&field]; + let result = generate_query_bindings(&refs); + let code = result.to_string(); + // No bindings for None filter + assert!(!code.contains("bind")); + } + + #[test] + fn query_bindings_empty() { + let result = generate_query_bindings(&[]); + assert!(result.is_empty()); + } + + #[test] + fn where_conditions_multiple_filters() { + let fields = vec![ + parse_field(quote! { + #[filter(eq)] + pub status: String + }), + parse_field(quote! { + #[filter(like)] + pub name: String + }), + ]; + let refs: Vec<&FieldDef> = fields.iter().collect(); + let result = generate_where_conditions(&refs, false); + let code = result.to_string(); + assert!(code.contains("status")); + assert!(code.contains("name")); + assert!(code.contains("= $")); + assert!(code.contains("ILIKE")); + } +} From f8832d8b27c333617ea387fc5debc6efece8e4cb Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 09:43:28 +0700 Subject: [PATCH 09/11] refactor(parse): remove dead code and add comprehensive tests - Remove unused parse_index_meta function from index.rs (duplicate of parse_index_content in helpers.rs) - Fix parse_index_attrs to properly consume all nested meta items (was failing to parse indexes when other attrs like table = "..." came first) - Fix parse_index_content to distinguish between `name` column and `name = "..."` option by checking for `=` sign - Add 19 comprehensive tests for parse_index_attrs covering all index options - Coverage: helpers.rs 47.75% -> 96.01%, index.rs 77.45% -> 100% --- .../src/entity/parse/entity/helpers.rs | 292 +++++++++++++++++- .../src/entity/parse/entity/index.rs | 61 ---- 2 files changed, 281 insertions(+), 72 deletions(-) diff --git a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs index 10fca36..bbfb596 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/helpers.rs @@ -187,14 +187,22 @@ pub fn parse_index_attrs(attrs: &[Attribute]) -> Vec { } let _ = attr.parse_nested_meta(|meta| { - if meta.path.is_ident("index") - && let Ok(idx) = parse_index_content(&meta, false) - { - indexes.push(idx); - } else if meta.path.is_ident("unique_index") - && let Ok(idx) = parse_index_content(&meta, true) - { - indexes.push(idx); + let is_index = meta.path.is_ident("index"); + let is_unique_index = meta.path.is_ident("unique_index"); + + if is_index || is_unique_index { + if let Ok(idx) = parse_index_content(&meta, is_unique_index) { + indexes.push(idx); + } + } else if meta.input.peek(syn::Token![=]) { + // Consume `key = value` style attributes (e.g., table = "users") + let _: syn::Token![=] = meta.input.parse()?; + let _: syn::Expr = meta.input.parse()?; + } else if meta.input.peek(syn::token::Paren) { + // Consume `key(...)` style attributes we don't handle + let content; + syn::parenthesized!(content in meta.input); + let _: proc_macro2::TokenStream = content.parse()?; } Ok(()) }); @@ -214,19 +222,23 @@ fn parse_index_content( let mut where_clause = None; meta.parse_nested_meta(|nested| { - if nested.path.is_ident("type") { + // Check if this is a key = value option by peeking for `=` + let has_value = nested.input.peek(syn::Token![=]); + + if has_value && nested.path.is_ident("type") { let _: syn::Token![=] = nested.input.parse()?; let value: syn::LitStr = nested.input.parse()?; index_type = IndexType::from_str(&value.value()).unwrap_or_default(); - } else if nested.path.is_ident("name") { + } else if has_value && nested.path.is_ident("name") { let _: syn::Token![=] = nested.input.parse()?; let value: syn::LitStr = nested.input.parse()?; name = Some(value.value()); - } else if nested.path.is_ident("where") { + } else if has_value && nested.path.is_ident("where") { let _: syn::Token![=] = nested.input.parse()?; let value: syn::LitStr = nested.input.parse()?; where_clause = Some(value.value()); } else if let Some(ident) = nested.path.get_ident() { + // Treat any other identifier as a column name columns.push(ident.to_string()); } Ok(()) @@ -244,3 +256,261 @@ fn parse_index_content( where_clause }) } + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + // ========================================================================= + // parse_has_many_attrs tests + // ========================================================================= + + #[test] + fn has_many_empty() { + let attrs: Vec = vec![]; + let result = parse_has_many_attrs(&attrs); + assert!(result.is_empty()); + } + + #[test] + fn has_many_single() { + let attrs: Vec = vec![parse_quote!(#[has_many(Post)])]; + let result = parse_has_many_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].to_string(), "Post"); + } + + #[test] + fn has_many_multiple() { + let attrs: Vec = vec![ + parse_quote!(#[has_many(Post)]), + parse_quote!(#[has_many(Comment)]), + parse_quote!(#[has_many(Like)]), + ]; + let result = parse_has_many_attrs(&attrs); + assert_eq!(result.len(), 3); + assert_eq!(result[0].to_string(), "Post"); + assert_eq!(result[1].to_string(), "Comment"); + assert_eq!(result[2].to_string(), "Like"); + } + + #[test] + fn has_many_ignores_other_attrs() { + let attrs: Vec = vec![ + parse_quote!(#[derive(Debug)]), + parse_quote!(#[has_many(Post)]), + parse_quote!(#[entity(table = "users")]), + ]; + let result = parse_has_many_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].to_string(), "Post"); + } + + // ========================================================================= + // parse_api_attr tests + // ========================================================================= + + #[test] + fn api_attr_default_when_missing() { + let attrs: Vec = vec![]; + let result = parse_api_attr(&attrs); + assert!(result.tag.is_none()); + assert!(result.security.is_none()); + } + + #[test] + fn api_attr_ignores_non_entity() { + let attrs: Vec = vec![parse_quote!(#[derive(Debug)])]; + let result = parse_api_attr(&attrs); + assert!(result.tag.is_none()); + } + + #[test] + fn api_attr_with_tag() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", api(tag = "Users API"))])]; + let result = parse_api_attr(&attrs); + assert_eq!(result.tag, Some("Users API".to_string())); + } + + #[test] + fn api_attr_with_security() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", api(security = "bearer"))])]; + let result = parse_api_attr(&attrs); + assert!(result.security.is_some()); + } + + #[test] + fn api_attr_entity_without_api() { + let attrs: Vec = vec![parse_quote!(#[entity(table = "users")])]; + let result = parse_api_attr(&attrs); + assert!(result.tag.is_none()); + } + + // ========================================================================= + // parse_index_attrs tests + // ========================================================================= + + #[test] + fn index_attrs_empty() { + let attrs: Vec = vec![]; + let result = parse_index_attrs(&attrs); + assert!(result.is_empty()); + } + + #[test] + fn index_attrs_no_indexes() { + let attrs: Vec = vec![parse_quote!(#[entity(table = "users")])]; + let result = parse_index_attrs(&attrs); + assert!(result.is_empty()); + } + + #[test] + fn index_attrs_single_column() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", index(email))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].columns, vec!["email"]); + assert!(!result[0].unique); + assert_eq!(result[0].index_type, IndexType::BTree); + } + + #[test] + fn index_attrs_multiple_columns() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", index(name, email))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].columns, vec!["name", "email"]); + } + + #[test] + fn index_attrs_unique() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", unique_index(tenant_id, email))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert!(result[0].unique); + assert_eq!(result[0].columns, vec!["tenant_id", "email"]); + } + + #[test] + fn index_attrs_with_type_gin() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "posts", index(type = "gin", tags))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].index_type, IndexType::Gin); + assert_eq!(result[0].columns, vec!["tags"]); + } + + #[test] + fn index_attrs_with_type_gist() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "locations", index(type = "gist", coordinates))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].index_type, IndexType::Gist); + } + + #[test] + fn index_attrs_with_type_brin() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "logs", index(type = "brin", created_at))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].index_type, IndexType::Brin); + } + + #[test] + fn index_attrs_with_type_hash() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "cache", index(type = "hash", key))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].index_type, IndexType::Hash); + } + + #[test] + fn index_attrs_with_custom_name() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", index(name = "idx_custom", status))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].name, Some("idx_custom".to_string())); + assert_eq!(result[0].columns, vec!["status"]); + } + + #[test] + fn index_attrs_with_where_clause() { + let attrs: Vec = vec![ + parse_quote!(#[entity(table = "users", index(email, where = "deleted_at IS NULL"))]), + ]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!( + result[0].where_clause, + Some("deleted_at IS NULL".to_string()) + ); + } + + #[test] + fn index_attrs_multiple_indexes() { + let attrs: Vec = vec![parse_quote!( + #[entity( + table = "users", + index(email), + unique_index(tenant_id, email), + index(type = "gin", tags) + )] + )]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 3); + + assert_eq!(result[0].columns, vec!["email"]); + assert!(!result[0].unique); + + assert_eq!(result[1].columns, vec!["tenant_id", "email"]); + assert!(result[1].unique); + + assert_eq!(result[2].columns, vec!["tags"]); + assert_eq!(result[2].index_type, IndexType::Gin); + } + + #[test] + fn index_attrs_all_options() { + let attrs: Vec = vec![parse_quote!( + #[entity( + table = "users", + unique_index(name = "idx_active_users", type = "btree", email, where = "active = true") + )] + )]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert!(result[0].unique); + assert_eq!(result[0].name, Some("idx_active_users".to_string())); + assert_eq!(result[0].index_type, IndexType::BTree); + assert_eq!(result[0].columns, vec!["email"]); + assert_eq!(result[0].where_clause, Some("active = true".to_string())); + } + + #[test] + fn index_attrs_ignores_non_entity() { + let attrs: Vec = vec![parse_quote!(#[derive(Debug)])]; + let result = parse_index_attrs(&attrs); + assert!(result.is_empty()); + } + + #[test] + fn index_attrs_unknown_type_defaults_to_btree() { + let attrs: Vec = + vec![parse_quote!(#[entity(table = "users", index(type = "unknown", col))])]; + let result = parse_index_attrs(&attrs); + assert_eq!(result.len(), 1); + assert_eq!(result[0].index_type, IndexType::BTree); + } +} diff --git a/crates/entity-derive-impl/src/entity/parse/entity/index.rs b/crates/entity-derive-impl/src/entity/parse/entity/index.rs index 36dd8cc..4954654 100644 --- a/crates/entity-derive-impl/src/entity/parse/entity/index.rs +++ b/crates/entity-derive-impl/src/entity/parse/entity/index.rs @@ -122,67 +122,6 @@ impl CompositeIndexDef { } } -/// Parse index attributes from entity-level meta list. -/// -/// Handles both `index(...)` and `unique_index(...)` forms. -/// -/// # Syntax -/// -/// ```text -/// index(col1, col2, ...) -/// index(type = "gin", col1, col2) -/// index(name = "idx_name", col1, col2) -/// index(col1, where = "condition") -/// unique_index(col1, col2) -/// ``` -#[allow(dead_code)] // Will be used when full index parsing is integrated -pub fn parse_index_meta( - meta: syn::meta::ParseNestedMeta<'_>, - unique: bool -) -> syn::Result { - let mut columns = Vec::new(); - let mut name = None; - let mut index_type = IndexType::default(); - let mut where_clause = None; - - meta.parse_nested_meta(|nested| { - if nested.path.is_ident("type") { - let _: syn::Token![=] = nested.input.parse()?; - let value: syn::LitStr = nested.input.parse()?; - index_type = IndexType::from_str(&value.value()).unwrap_or_default(); - } else if nested.path.is_ident("name") { - let _: syn::Token![=] = nested.input.parse()?; - let value: syn::LitStr = nested.input.parse()?; - name = Some(value.value()); - } else if nested.path.is_ident("where") { - let _: syn::Token![=] = nested.input.parse()?; - let value: syn::LitStr = nested.input.parse()?; - where_clause = Some(value.value()); - } else { - // Assume it's a column name - let col = nested - .path - .get_ident() - .map(|i| i.to_string()) - .ok_or_else(|| nested.error("expected column name"))?; - columns.push(col); - } - Ok(()) - })?; - - if columns.is_empty() { - return Err(meta.error("index must have at least one column")); - } - - Ok(CompositeIndexDef { - name, - columns, - index_type, - unique, - where_clause - }) -} - #[cfg(test)] mod tests { use super::*; From e333c04dd26005acbbd346061da2747f6acaf6b5 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 09:52:36 +0700 Subject: [PATCH 10/11] fix(clippy): replace useless vec! with array in tests --- .../entity-derive-impl/src/entity/sql/postgres/helpers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs index ca7aade..503547e 100644 --- a/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs +++ b/crates/entity-derive-impl/src/entity/sql/postgres/helpers.rs @@ -282,9 +282,9 @@ mod tests { #[test] fn update_bindings_generates_bind_calls() { - let fields = vec![ + let fields = [ parse_field(quote! { pub name: String }), - parse_field(quote! { pub email: String }), + parse_field(quote! { pub email: String }) ]; let refs: Vec<&FieldDef> = fields.iter().collect(); let bindings = update_bindings(&refs); @@ -424,7 +424,7 @@ mod tests { #[test] fn where_conditions_multiple_filters() { - let fields = vec![ + let fields = [ parse_field(quote! { #[filter(eq)] pub status: String @@ -432,7 +432,7 @@ mod tests { parse_field(quote! { #[filter(like)] pub name: String - }), + }) ]; let refs: Vec<&FieldDef> = fields.iter().collect(); let result = generate_where_conditions(&refs, false); From bd279273bddb8037968ff064445e1cb6e0535bbc Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Thu, 8 Jan 2026 10:23:33 +0700 Subject: [PATCH 11/11] up deps and bump version --- Cargo.toml | 26 +++++++++---------- crates/entity-core/Cargo.toml | 6 +++-- crates/entity-derive-impl/Cargo.toml | 9 +++++-- .../{migrations/mod.rs => migrations.rs} | 0 .../{postgres/mod.rs => postgres.rs} | 0 .../migrations/{types/mod.rs => types.rs} | 0 crates/entity-derive/Cargo.toml | 13 +++++++--- 7 files changed, 33 insertions(+), 21 deletions(-) rename crates/entity-derive-impl/src/entity/{migrations/mod.rs => migrations.rs} (100%) rename crates/entity-derive-impl/src/entity/migrations/{postgres/mod.rs => postgres.rs} (100%) rename crates/entity-derive-impl/src/entity/migrations/{types/mod.rs => types.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index e209c51..40e0ace 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,16 @@ resolver = "3" members = ["crates/*"] exclude = [ - "examples/basic", - "examples/filtering", - "examples/relations", - "examples/events", - "examples/hooks", - "examples/commands", - "examples/transactions", - "examples/soft-delete", - "examples/streams", - "examples/full-app", + "examples/basic", + "examples/filtering", + "examples/relations", + "examples/events", + "examples/hooks", + "examples/commands", + "examples/transactions", + "examples/soft-delete", + "examples/streams", + "examples/full-app", ] [workspace.package] @@ -26,9 +26,9 @@ license = "MIT" repository = "https://github.com/RAprogramm/entity-derive" [workspace.dependencies] -entity-core = { path = "crates/entity-core", version = "0.2.0" } -entity-derive = { path = "crates/entity-derive", version = "0.4.0" } -entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.2.0" } +entity-core = { path = "crates/entity-core", version = "0.3.0" } +entity-derive = { path = "crates/entity-derive", version = "0.5.0" } +entity-derive-impl = { path = "crates/entity-derive-impl", version = "0.3.0" } syn = { version = "2", features = ["full", "extra-traits", "parsing"] } quote = "1" proc-macro2 = "1" diff --git a/crates/entity-core/Cargo.toml b/crates/entity-core/Cargo.toml index 67d8791..0a60cd1 100644 --- a/crates/entity-core/Cargo.toml +++ b/crates/entity-core/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "entity-core" -version = "0.2.0" +version = "0.3.0" edition = "2024" rust-version = "1.92" authors = ["RAprogramm "] @@ -24,7 +24,9 @@ streams = ["serde", "serde_json", "futures"] [dependencies] async-trait = "0.1" -sqlx = { version = "0.8", optional = true, default-features = false, features = ["postgres"] } +sqlx = { version = "0.8", optional = true, default-features = false, features = [ + "postgres", +] } serde = { version = "1", features = ["derive"], optional = true } serde_json = { version = "1", optional = true } futures = { version = "0.3", optional = true } diff --git a/crates/entity-derive-impl/Cargo.toml b/crates/entity-derive-impl/Cargo.toml index e3be362..7ffe173 100644 --- a/crates/entity-derive-impl/Cargo.toml +++ b/crates/entity-derive-impl/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "entity-derive-impl" -version = "0.2.0" +version = "0.3.0" edition = "2024" rust-version = "1.92" authors = ["RAprogramm "] @@ -34,7 +34,12 @@ uuid = { version = "1", features = ["v4", "v7", "serde"] } chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive"] } async-trait = "0.1" -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] } +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "postgres", + "uuid", + "chrono", +] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } utoipa = { version = "5", features = ["chrono", "uuid"] } validator = { version = "0.20", features = ["derive"] } diff --git a/crates/entity-derive-impl/src/entity/migrations/mod.rs b/crates/entity-derive-impl/src/entity/migrations.rs similarity index 100% rename from crates/entity-derive-impl/src/entity/migrations/mod.rs rename to crates/entity-derive-impl/src/entity/migrations.rs diff --git a/crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs b/crates/entity-derive-impl/src/entity/migrations/postgres.rs similarity index 100% rename from crates/entity-derive-impl/src/entity/migrations/postgres/mod.rs rename to crates/entity-derive-impl/src/entity/migrations/postgres.rs diff --git a/crates/entity-derive-impl/src/entity/migrations/types/mod.rs b/crates/entity-derive-impl/src/entity/migrations/types.rs similarity index 100% rename from crates/entity-derive-impl/src/entity/migrations/types/mod.rs rename to crates/entity-derive-impl/src/entity/migrations/types.rs diff --git a/crates/entity-derive/Cargo.toml b/crates/entity-derive/Cargo.toml index 2ada274..7810606 100644 --- a/crates/entity-derive/Cargo.toml +++ b/crates/entity-derive/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "entity-derive" -version = "0.4.0" +version = "0.5.0" edition = "2024" rust-version = "1.92" authors = ["RAprogramm "] @@ -25,8 +25,8 @@ api = [] validate = [] [dependencies] -entity-core = { path = "../entity-core", version = "0.2.0" } -entity-derive-impl = { path = "../entity-derive-impl", version = "0.2.0" } +entity-core = { path = "../entity-core", version = "0.3.0" } +entity-derive-impl = { path = "../entity-derive-impl", version = "0.3.0" } [dev-dependencies] trybuild = "1" @@ -35,7 +35,12 @@ chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" async-trait = "0.1" -sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] } +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "postgres", + "uuid", + "chrono", +] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } utoipa = { version = "5", features = ["chrono", "uuid"] } validator = { version = "0.20", features = ["derive"] }