From 4a60cad64565820fe8b71962d3edabc53c26a61b Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Fri, 8 Mar 2024 10:40:43 +0000 Subject: [PATCH] feat(hogql): Allow lazy joins on lazy tables with requested fields (#20731) * WIP * Clean up and tests * Updated mypy * Updated lazy tables to work without resolving types twice * Update UI snapshots for `chromium` (2) * Update query snapshots * Update UI snapshots for `chromium` (2) * Update query snapshots * Update UI snapshots for `chromium` (2) * Updated mypy * Fixed cohort people and joining multiple lazy tables * Fixed infinite recursion * Dont use an extra column for hogql expressions * Update query snapshots * Update UI snapshots for `chromium` (2) * Update UI snapshots for `webkit` (2) * Update UI snapshots for `webkit` (2) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- cypress/e2e/auth.cy.ts | 2 +- cypress/productAnalytics/index.ts | 6 +- ...sights--user-paths-edit--light--webkit.png | Bin 65928 -> 66154 bytes .../scenes/data-warehouse/ViewLinkModal.tsx | 110 ++++++++-- .../scenes/data-warehouse/viewLinkLogic.tsx | 77 ++++--- mypy-baseline.txt | 33 ++- .../api/test/__snapshots__/test_query.ambr | 10 +- posthog/hogql/database/argmax.py | 2 +- posthog/hogql/database/database.py | 19 +- posthog/hogql/database/models.py | 5 +- .../hogql/database/schema/cohort_people.py | 14 +- posthog/hogql/database/schema/events.py | 14 +- posthog/hogql/database/schema/groups.py | 4 +- posthog/hogql/database/schema/log_entries.py | 4 +- .../database/schema/person_distinct_ids.py | 6 +- .../hogql/database/schema/person_overrides.py | 4 +- posthog/hogql/database/schema/persons.py | 8 +- posthog/hogql/database/schema/persons_pdi.py | 6 +- .../database/schema/session_replay_events.py | 6 +- .../database/schema/static_cohort_people.py | 2 +- .../hogql/test/__snapshots__/test_query.ambr | 96 ++++++--- .../test/__snapshots__/test_resolver.ambr | 49 +++-- posthog/hogql/transforms/lazy_tables.py | 203 +++++++++++++++--- .../test/__snapshots__/test_lazy_tables.ambr | 83 +++++-- .../__snapshots__/test_property_types.ambr | 8 +- .../hogql/transforms/test/test_lazy_tables.py | 43 ++++ .../test/__snapshots__/test_funnel.ambr | 12 +- .../test_lifecycle_query_runner.ambr | 12 +- .../test_retention_query_runner.ambr | 6 +- .../test/__snapshots__/test_trends.ambr | 91 +++++--- posthog/warehouse/api/view_link.py | 7 +- posthog/warehouse/models/join.py | 22 +- 32 files changed, 710 insertions(+), 254 deletions(-) diff --git a/cypress/e2e/auth.cy.ts b/cypress/e2e/auth.cy.ts index dd514f1121c23..5355f3011b12e 100644 --- a/cypress/e2e/auth.cy.ts +++ b/cypress/e2e/auth.cy.ts @@ -84,7 +84,7 @@ describe('Auth', () => { cy.visit('/signup') cy.location('pathname').should('eq', '/project/1') }) - + it('Logout in another tab results in logout in the current tab too', () => { cy.window().then(async (win) => { // Hit /logout *in the background* by using fetch() diff --git a/cypress/productAnalytics/index.ts b/cypress/productAnalytics/index.ts index cf94691bf657b..b523a4e970efb 100644 --- a/cypress/productAnalytics/index.ts +++ b/cypress/productAnalytics/index.ts @@ -209,14 +209,14 @@ export const dashboard = { cy.get('[data-attr="prop-val-0"]').click({ force: true }) cy.get('.PropertyFilterButton').should('have.length', 1) }, - addPropertyFilter(type: string = "Browser", value: string = "Chrome"): void { + addPropertyFilter(type: string = 'Browser', value: string = 'Chrome'): void { cy.get('.PropertyFilterButton').should('have.length', 0) cy.get('[data-attr="property-filter-0"]').click() - cy.get('[data-attr="taxonomic-filter-searchfield"]').click().type("Browser").wait(1000) + cy.get('[data-attr="taxonomic-filter-searchfield"]').click().type('Browser').wait(1000) cy.get('[data-attr="prop-filter-event_properties-0"]').click({ force: true }) cy.get('.ant-select-selector').type(value) cy.get('.ant-select-item-option-content').click({ force: true }) - } + }, } export function createInsight(insightName: string): void { diff --git a/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png b/frontend/__snapshots__/scenes-app-insights--user-paths-edit--light--webkit.png index 93d52b7379a038a5a4c15ce0b27862f35aa85790..b7e136c7058b1a6a483ed99f823a238b1361d390 100644 GIT binary patch literal 66154 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 K?abFbaq(Z1Bid*H literal 65928 zcmb@uWmweh*EWiBE4P3mB3;r564H&rNQboKNOyORMTvmK(B0i#qte~o-3;9W?}h(o zKgT|f{d{=e9gINO!?9!@fOmm zv^}StKc-ZWsRb_eq@~SeF}^tFy2$&Ku&;q1KmKL;izhZV*8d3wRoZJq$*GBn z5?^!kO?}A_+`);`^aAUG?Fm&2i>bkqWgj1rS@AFV74xMLlJiZCli|bJeQoWcN$mCs znVIjgdU#x^-%O7l_M23NVbn6e zYk5r7LfE3$qr`&T5e+RT`b)#ocg$F=LYvJuPWnU=DgD(YT)DEw@B*%bk+ihPd?xpI zju@vx_Q%Ka33c_4KXfbxk(9WLE5m~QaSXU7^?I5o+PF@xGqa6}NJwJh;>NU&R;PLs zcF62YV*)U-f-o^LdwYA66cLXpGCfcl^p|^$Lqp0u@fFq8+vCO6=@EvFOIA;ug`(x; z<#%>=zhee*a3qmE7UXfBdd?c1f1z5a&WSxNhpSFey`H6sqdt3!x{vnB2D6r$q5f0z zA`vNR0QL!g0-=+1fJ`P6TRtOhv~XvK^>Evd>}=*hmfkwTdRS5AmE~{F8?MTuMIJ_3 zpL;KjSKkr4;{D-;6d{qXZ*aup`%bJ=m2zQT*UJ>b;^$sI@7}#LFfdpRbtrRW^@n!$ zc5<_`?;oBXpPc0CFg^>szV>cx5Ms9d)93Dgx|i$8qiWe57$~jrhi1jWz1rrjMzP-V z(S=I3{NP}KR$*U9hn5Ik3KPjBlqAy1DCPjJ|s3QkL-HZ)xRl~QD5Q)Gom6c#iV z>Ez~!eNTj$NwW6V!>zTa=Ndh6l_tUr!BUQm)ntO_ z9r9t53w+qNhh6hgEp@5+iVYTO|IDiFirB-)$B*Z=cC;&@6frP687}UssWdmtG|$Kq zSMg@gA0OdzIPIyesW~4sl$e;BGBGwL7qXk^?Ny_XFcPl(@g*~}qoZRz?Dn%9eLna3_4SwU znC8<}*3iaTIVq`?gBoE^U%T3bbIDL<*mFEQLko+gg^iz4Q9g@*GPSz571|l$0kR4S ze7WlR^)9<<@0t1&M6~tv`h1>~;#nFQnc3QEczeyv=rsQci4KsxM!$9wniIWE=;`X( z8mXnxF7%WtNlkqVC)%5>x3w~`GB*!PO0wTx+pSB+SI#|d35-zARXsXQhAE7$zA_j^ z=?vv?)D{zC6t*`)bihRM2SS!scAk@aE9So6Fw@-L)EKVW`6sfr#=Lm|I$^s?Uju>7u(Y zyhLy_Gc(s$S4oM9{Z98X%@H{{F^Sv-a+ci|m0G!~7ndIX#~ZAy1jw1%?6Cz-4vv5v zhoK9L9TNqYkPkXbTWi$M0(W+%va+()*9pyh^z0m5w(zG^a#d~St2nvo)N)nFRQ24c zx>~WK;WF=(4LEQUMbFr9OUtjE$>`9q)%;`D9Ew2HtJ zdHC-UrHFup1hQJ4z6gd`NMzW-x9(LwLBWFyuR;u2%WpbQq66H7l44^gJCvFOrOx_$ zohd;E87NE>GqKWf=GkpEHG1`oFnaVjmv7agnz+*RE1Tdr#Zi$hEWJsa+v2xkwgP#v zUZW2KM_x$@V}>MqeM>@sjBIy6M^Dd>0696i8XW~q^T~2C6>1J{?%nBXUg7SG!^6Ws zDWey83HIuUo64-H?yjzYjz2?R0)EK@Z>XNDx+7_FTmQcP@Vtb^Ztb|1GrPYunE-$Md^O4VM5w>~sme^#RtlBhck0-0MwcJ^lzvI+`U7oSAo^dCQh zEi|Wcc8~J|L&Q~#*_}sdsVPEDksp(jqF5t3IXF3;VfI5LsWO>|8ygh2j3T>8C)ViU z-;LtQBB=t@#p5yPXrG>dg=~~FKcqAg-}L^yj_R`Ra^xW#>f!9Iw^6gJQH>J@DoNGh4i#vjdo5tK#v zwzjrb+rAUv6ZrR^`_xOD2R(A~<>e0H&mwen5}l88Dvf#GzI^$TD(&#lp;m|W`IbQ7 zsKrUd54mcWB7v=jjQZH0KcygabeYvQb0)G&l&u&KoQzxYrQ9LT!5EX!}d@UeN0`lEF7_Bo_SJJ9<5hm8d>w9;HRkWqvqv< zfE+QVOw)^sEMKN(>0^0ff23Y-cfEker~3%BIVpXFXA^R3Y8KQRfUD2p{{mz z@0FAez;(p z1u4UQW`ZqEHCbH6Qr?u1!7nvOa1dL(k5#d_Y1E^nLLw$H@u)vgrK*aDwTM+YH&Q7#TS^SA0x-`~}8291i#Qm)zJ;u9)0H85U_bEQX$Mg++B%P#}$c^aSzqp?B7OU0Kb5JgWeVV_mPT4&k z7O-&^<*FQej_J$8!{bof5bR9qQur^2BnSpRDiG=i==eOVhh? z`<0&o(doV=yB8yQFeuT+Hf%hrRa$L|`|m#%m6xH96X$i>79wq`CXn^ z$GuE4$)q_XWv<7DhM^Lc2MhVfF7cEmqQ^c{&gPBN2W#ygePLu_nXYuK4hsv5t~Lc( zRV>xXu4G&ZHnE@D&r(INIzMZ*n$&OCn6a)r48Vw~Lb9y9{PIB!`OGx0t1FQs+Nbqg zh|b}(?KXfuAf0Wjtnm5#nK-~}LGM(NbAa;bd#uzeiR1>K_ot1)*Zz{Bk^OR zL?8WXY--A5G*9ipg9mGCYb-=+K0Zykx#p|MDv>hAdU{EfmCnO1$KcF&|CvD67`7A^% zD(en8H$~4EzGh_5@*ovuW#joBP5hsbCnO|fDJCZ+CnIcT1solZ+C!85oQ~Gy!Bf=L z8D5wiy>`jR&;I`Y5fQ^+-V6;4Nbp9%#>Sx(DR7_3Q_dCgI=B1q;e+AS25Y`Me+rweL(>CT14 zT-zQ-MnGuGTMn^Q*eFlqxMpQ0eN7a>#3{T(V=Pwmh(-XU1vioHuQ6=H%o= z#NmA7in5_Q%G1*mRqCT+Vv-dY*csL=i9qe;2S&8vDHZDp*Sl^zZ>_{iM=)>=`><$L zboBOGH~T$CHl9)VH@l-w#$o4+7~nw>T5UTo0`RD*sU(EgW(LflKUjuRQXL1&?O-*a zQw#W9pMScCj*X37R#tX%ebf&|z!FYMAX(U5>;<)qmm4JLApTK92oNJT} zA)`twC@AQRVBl>hq5Y3^Bfs|R1_9_!S#h>Pe8H7<_qPwc^z>~IT7n>FRX(4cWRTfF zj!j3-4Zl*SqSoLgJoR;s=YgoIs}tbiWdk1NEMrXrB zxN&vH=MXNFo)|E``lgq;vVMuC6k}Gy%wAQ!MUFmsA)S zC&VTe$k8ZHA{VHbKU?>a=l0XjtH`3~#?4h_DqLmOKowk6y@B*bo;(XvW$Nt*8cxa6 zfB#`+Wj$K&cXe{|y4tO_ookq^bzC19c<=3vDm83BMb0BsvK2tsqDuqut2bHnwj~hn z;^IO=LIP|~2I@*Jc?kG)cVO{0*~wgAf3Rb}r>AGFFHsiK>w2XMW~M(BLj973rM#@n zYV!@?>9s#dMe7c+W^JKHWKT>%uoraQxs$fmWabOCtB`Y*uIB-%VPQ&|nwpX!6jt+1 z#G^k!k@jKR(SWWwbYQo<78)0 zP7odwL-g!fnbYQw>zunk%N-b{78h>@Vv?zmkxs2creS*s#A$0}I6tDdr|0r?*Yo=P z;LDdUsp9^a0Mc&F)Nj}MY|M<${F$A9-9{vapq)ST_JjEuP5ar<5I?gSxi!UoJB^I#ixO671)kzALkME7I zT3ug5qKHaTqUL#+$qSBaZJ499--NVDYQTL63k z#L>~2=}Q!>ciLQ9UT$8S0cnuvwL#0t_Bivru4zxqYh=R_NWJG<`Nb2@JUy>o8@72J zv=TlGZ2A3Lf4oT7v_GlQX*0aA(Ax3a>^BlOf6C&%n?xZmo}1E4hsB!G2#^)L4hK!; zaVL&mM0A2zPKW#auBTpxQ~Hu%0qM%)fAH3A^hlfyi;NtJ`ktB@86dmXqNXbh6gNF7V3q$;ZxCg}t+be!m~K=5E6uu(U=jqYkk_a2@BAeENO=nlDgUXp zYF&DS*KR#kbFqxGZo0l=HQkh}D_FfYovT~#(i_LU-4n|>?|n84BFeniQ7_`=GDx3@ zjjfp5ax6i>+2Zne)85{G=MU-F!tbwuQNDWhN=#gw+t(6sV=!WQYWXI82_QZZ6A-K| zE)II>^TG)|qAEJAXUkC8l(5C43#CC`Z2b@8iB@s4R*?3LEZ7X`c;eFcLJW(Gf zW=U$|HU-Y3Ri87XfXDIr^~KE<5^;NTwM8i(%VFFV`7*A0-skGr zC6hXgktTf$-hTh{$H_9|?x;qiejcAk%CJf;U6cVX8bNgTY{N-tREQWM50Z_UZF@2w z!@rp-&1UvK{%vfv$q7=}!|%g~ij{J7pq!_unYT%LfLH{(I{7n@gz{>rYZ!A~IO~SJ z!i_c)5r*CTSqU7ze_i+B!Rh$|N)b@RNrztQgs&L4N!*4Z)uz8HEqR%EwQK)`w|0B_ z9Y1=s*JyBo!x8(V@I^H97^+Es|JS03p03B4$8J6w0_;`VwEi{N$#5vbOzZq~+c%B} zE;IUlRn!S#D&M(_v1fPeo+Jw0J4;+ zre(GtgBK=~sdaYcH1{AvW5CquuH_g zct%G@mp`W9vy*cDI$wOA8{w~b>+^M@Iv<7#p8ffln|g`liie*+b4*;t)oN}&)AAS4 zfSRBO!;PM2ldo$%&yrG8KTFYCy@>}&9_{h4PWcTJ++yqp|G_~hScp@VuCfX;K>ImM zmpss}vZhT#zryi3+XY#lgC|_t7@+1xmy@X`lxIR>A{Wbblgn3n?-2_W688N0^Y4&9 z-~l3!k@4zv-6aBYJX?(wN+!~^Xs4r-kSx?GbnQBkDCmlVYAlu~W$vx&W9?Ii&)G~> ztll(Y>};cBJh87y@C|AjZNwVgopyb}|1wses8S0x-h>Y5$2M=lC~oMR7iRlUQD^IF z(^Y#fsQvVxkPCV7GSy`%4OJMLnZ2jVt+88FhO|^#3-VlCUYc!PAI__FO1~7M&|uZr zxpG8&>c3rL-X*jeaqaOHTa1Ikrc3lA=tnTlC@mfKm7W&es`jYnPyoUXt$t#va&07%udvWeo9^K;)Z4!fk+fht1 zdN!Ey8=XmGc=qG?FSJvFH%hK)D-Q3Rj)h(FwX3NEfzA*Y@TDGudtty5{_|&z+u$Lq zF8bW2fO#2@7-8(bc`FKa|W> zCAr-==!7ZE*Bl%>=*{MHwZ;r+Ww1tpYI(2>r829^kO!E2^;iRzUNu#|)*Y(mf z+gk6d5J}0Uf3f^#G8BGJiDYTJ4JY4@vLeh=`V_ZZGX zl!60Zol&f;6A}>Gmdr&(=%E!jAd_b>-4JLXDY)ZBfo!3 z13c?bZ*S#ZAxIQl$;$S%zrNqek&5Qhv3r<-i`y)^^`cauYGnc5P$tpnM1@FCK1ke=?7ES9`KoO!BSM)^V{n29vFx&b6rG z_o{;3QpvG?n{x|8%cBb4Gz02O!{#*r8CaQ_X{ZhH3{PoJQ!Tk)b93rL%?Lh%(E*cC zi7QxNwS%RtO$w@rs!Tya&aJ}>RL?|7bE^7X8g|Qz@&_smr%%o0Jw5A9DOW9(Va=v& z6WOXI^~KyL#mE>!d;($e1XI+MAfGCZ;#ur35-zh}V=U94{N~_z#Hx$QkoNiOL)AxR zV-M?HahmXm@$sFi@D_6zHaKB#1a)_73cFI}_~j#prLBi$dK z3bjJdE4enQt8aqhNMbIvwX}>)Oy(NwE&FCBcP>kS#+Q*{L_JiHikz-?1#Aj17B{xb zn@tr{+nJhL^X0LWlsBB^*$4^UrbAmp8{6=xsJ^C;#oe^jcq$P9`j!kgLcRDAwg?fx z40X{-ZW4>LbpPE<&6JlH0z0~_TT4#PA}~Aa)EOs?zFI+6*44pz_sqi2krda_J z*?+tM@uJ>OPJt*)N*dqwmS;zE_=Mb1ygA#{$i;=H-b<80P;ywSvF{&1Z$d(z02yPw zFUi#O0OK*q`%Hzx$1d9hpziK`h7@M!;-XEn`TZ6UG%%jpJhn8{8XD0&m5Gg7INrt` zSNG5s=+d&=IXT#$M#DRqjQrZDMW9Gu-Lqo#d}EGxU^Ob|@^EmyV#&8(f|@NZnREt= z-g>{?Vj2HgYjWD8b0d4yD#4ps(7=vTFgW)BV|}_SgohbfX?uhK<9oJAT4#EejEg(o zbbcLQYA6{fE2EAoi)-YRL-=?HFdm*JX3DBRZ7#SxSqBlz1?vfkfEz6?{`2R@yW^5C zo=Rm70pbHYKDfeeF3?1Ix}D?WxSr8<6O0M_feibY^7Y76UGJ8f<>WIM3<1&*E|AO? zGdl53j@{ZrD2f{%T_x}UmXHXy*4LEX%Qqyf7!!3{7?`k6Cnwv{xH;317uxyaP~OIbtX=8%e4sU1x90H5b{hsoWEB)xT~#GQMkdQ6e0^)z zZiJjQ=3gTxa%udVm2y>q0+03Yz6KK<#E3z!Ey@LrVov>ql=0h%l8dwXv2G&{082$- z^Gx^_)&tY++O*H$(b3SR=QjHKbE)E2V4mt-_VklC31%9#3;iX|&R-w_*o_W)Py4gW z$izsg@izK07McYaoI7OcrAYcvMGAq zIbK+{hA9}dpFjDW_N533O;6M0n3=oh8pR)DV~bp_d__jz))*gEdBgVBk1l+DeF0aU zn|9p7|JwA1eg7nNXNUwqj7!U+BGy&oRfwvxvH*A&6pFz*^%8+bEPFUP-Ld1@?6YlK z1gGm>!=%8}cqx|FcD4Di$;LuhoSTR5>CQywSMNe@ka_Q>bme74mF=`Uat^g%(>P*L znV%mt_Vb(A;yTo;&hB@3K!D|2UC$gqm&ots{@el@i|VGk^Yg9dCqTJRuYC z-m(g{^p3>LG_l9U9X*pH;xqe2Ny-DtoOpwu?xFG(Wk;q`k|@|xf^Ju37cD9U_-aUT z0i0MpaNBu+z9R;k^_OU0cuqjTs#no)d3kfPee)UJc= zGc`R8D04UdA-AQ5dXoSrd&%~7zerVS>EQ74>e|Yd4fKpvmDhPHl9)3)J~oylo+y-@ zcP$z&x)qZBTKpK`DF6eD;PA}Uu*H%^agp1l;kZ+S>u;s`5IrqmNgzo<6kJw?BS3PPn#%DhlY@05=u$Y3ykobs#Vvn_`Z)f|rcm=Gtu=4j_ zf7WUJ0}R|S0PnL+O?fCzt5Q{O#rwYp1u>*;x!?AZRwFL<-2T9_n>dN;Vr}GbAudw+p)m4&u>HaB*b` z7k^v?yUy;J+sJ44=ajQ0+HWhL$I7j3&%y%;*mT)ltQry9Z_v5ideoRkK}Bn1q~a6n z4Y*!Ng>(05vd^_|^P*0p&t%RJ8(Q(Fe2rq;xf!q2_Pq6nkAA715}d z?DzmRsPQ-v2yM3;AT zzBJ!tyt~4bmj_w`Xl{!}rt4(F9&%b#yK}-5{qxnC&0tv3K4pC#p}diFaZ&g%p+snE z3M!qBrv3RNd7sd3qn_N^1<~T(q$}SxG&Cf`^g5fymJeRBuz;HG;mL{l_g@u34c_ZL zljTS2us@3<&bi9yJh{?VRx_Eh(LjZoJkN?_XHu7wv-&VG*e{w4Ta|$*DD34Z(U1)M z8-G?`R|C?izdjLbwCCjr@ssA?Gj}8Ti#bKf-PFJ!91dSyPX?T9J-IKDH{09a1=M+A z=j>UzU-A_|O{dzHa(1&!l?k<#Z+m!jwB01F3kWiqh?mjfO5kJ5yx6oMDLFYOXJ%bOb{?*GL9n-eb=m(|2t?{m0> zfta$Clq>ZRA!g9`@7wjYCW)bMpmsYG2SBHs5S+*#-5QBh{oM4af1TI5;_jUL*W9cp ziZT=&M(nD=Qo6eWEtLNg3O-`@qfvn2eGz0(Q(OBFOA{d7P`}Qs|-5ORi=ivxOjJ&7#fD8MP3F zA;5*OW2-1p_4af***nuy`%wYOs(2aHb4dwYMh1VU&RcpD`z;;-R76fG6L&8)D}PilisATm>4J2<>M+2+|{+U#*4KI z^WiLdI=a}Hn3&jDIyyRgqQ1rrPdopKk?;4E#IR5dY+_b+;m%W9SfuTQ!AN3Vyo|%!c#*rcKvQMwU?ImALd{Pnx z?xtB@ZMuxK^kkj0CjfzzKF2pL(s=Ls+{$6aF2>&J!EDTc?OjRVGkDWROq# zprhl3yg{d~vq#Zi0oTa}#WOh&PeL$oZdh1f9_-tl9UXCValK+?^*Lu>EAJN_tr!OI z)l^qV7ooRm4K^llaK>30;bUgLr!|0TXe#&HC^urUMmH1wJArBS5t5;v?^xz|d4|+5 zRs?hqkZKkdmOv@jyE{>3@+Q0G<7QzIMZDnTl>N0{ZEmP$oKjX+z5|>jWF;03j*g(p zx3#pRLx&V~Jr%0&!*Kr_&hj&Gb77UfH{-?g>^wX!j*g9_cZAYltbiCUiXVs_kRuz7 zhJoP^_Orxn?G^F@ixPIIZy*+4Sy>qgD~5ZH3- zfc!PeYGAkw=C=f2C)qkaF_9*s6IdqlA>m;M8HI%`S6bleNz`m?_QndQ$;ruPrfS_i zTtb3_yE{7z%LbBndATYGM<4#Z(l?gFzbbD=2`{s=GLrbcT~=}yv6~~K;KU?EF~aHZ z8CM*9-~3_&LjoKkRW`%bw#e$W9@c27@87?7)2+hUWaSkUe2y8DZd_3wtJTud(oCbb zBC@>_h1h`__;7vRgt%ABopt}iLszd>Qd^H|F($})g$ zo?ZKb z+7q}99_y*0M$eMQufp0aAU2kyl+@JJ9MV5IJ&o1Wv=Rd1m(`AMT)J5`iX=y7R z9rK&YF>7_8IsHvzgQ|{_YOe^QBO^iX%E&g|nW+`zfCr20gBo8Jdp@%Bh~SIG44NXP-z*3S_itNsnKmvssL5ZU}8a; zh);1ballk_w9fVo+NAt5S=6oL1sj_pby-zu6DVi_iE{94Pf$=Wn4H(IeHnzofB(6L z^+W$fcx3`kdAK6|sJv{}>?FtzgS$=+}Ta)?z z^z3jM?+b^TxDnv+8wUPVgMwlB`sJL$*X6}QPfP-@uxe}@0?9YLno|_yU?YcE`SG?Y6A_Vb6 z%faC=rZhc&1qBjDO%Wj&kOllUq3@wpmCgXFZGCTC6!w(7ULV^WOm9DS^FYUV?3S9A zXR|%FJ2$6y@7_&azOYY%-N6!FFPtdOSp<}<&C+)tO5Tm^zJ=MjH%c1Vj)??;ACnf8h$U)Bd}IqJ7es zkmcFN32t9?(7xvVi^1G(^vWv#)9o<8{(sm15Vsbl2Ra^jc_Z#zpKv$}ZtiGaK>;?k z0TBob_xRO{{^3 z4LBi)G;M7rLPocDU);~L?D&$r6i%I_H+-!#PAgm>;I=UI+hVpyQD9 z+2!zSJ-mDGlb^ZN7TZguIj>Pr3hL~fPbv?pvVc)V(7?V*Y6yu6_SHF5Fr<1sESyHm3ZBhbR4;p*pqPZ6z! zB|5Y{klL)0-L$y9?YJTplV#T)qI=`>S@-@Ig+!sk`kyC*{>egKL`RDllx(_X2?0C> zx9OJAHhM`uNW>X1n$V;*dfs5~AO_BLw0w9UsxGb(HEHS{M2Mx z3=ZZRuT*8|u1?!}wIQ_8ofFk$tgZk!OGv1(J?ZP|sq;a#DT;dA8d)8WH=zK)p-ro) zkpxw>$p+(e1OmC9?Am1Y({{ z+7ha<%VFIf-w_$w@%dC%KfVCw@E^@U%g44wZKSMB)n=BR1)-BHo>$REetYf%hkB9WSzP5}ojYaNu;QTk-)C(9 zPc+RL=c%oY1&l#}30UHS15En7%5gt)69bCU5mo}Ua*%8GCL3Kx^G11X^a^#TI5`WO zX#Nh;{*ZZx7VPtzoccdC_J$EbyuDn^`{YmfszTCI4l-ep*&uv{wixxIl&mT3;1SWid8!a|J{QW|l zuK3s`Ct2ICUpW;S6B8|w`h38@R>B?{5J2U~$y}wNuTSBL?gOyfX^6nP)(1PwKEd)m z?mDcvfwJH69f)y>iDQ8j5|~q<`U)-`*zxO^)QbTIYH9b_jI@M+Z(``F?;(&OOleK` zm$;9PeIHVbQ-^mNXVSGoGTPTkiQ}GziBXx_inlBu{*dnpkjhjhz{h`z>%SNirwKj+ z7VuU2HWRmuyF`Un@$kd;f3>%L{6l*8&i~Y@_CIm^yYK$L8s1W=LXYncyjRc3bjQ?8 zQl?VM&0wNl`j=0L%HOsV|HEO;rJO9rzjwb;q&o3`s4O#TEX~o0$Dey!-Jp~CN<;bx zpDr}xQe7zB#Vh2gDC^yhsSwJz8F)IR^X}j)W~H(z9k<#FW5xw$MBn9o<^=P*W3gZl zm%g`MH9t)AtXxI)=fG8+iqnMT=X&1W{r(NGKZ&EH9P#N6hM0o9EK;+=-Ez4!26bNl z{gnL0iN6Q9;vh=Z5jLc0ZtnS8U*)M!EO#HKn16i0Ga_ytUT`QtSz2@MpX@`M=3=vJ z(!ejSQl$J6cD~sTI*G!hj!&YY<-QhlaJtSP!vFO18r@x%8)ok9Q96i$G)S|#!wWA; zN+iLL?50Tkt?zQXvZr{@@BmT;qT^(B=n^G7DoVYW-kqA)Zf!Xv`R?Z|X`iutJWe+p z_jWEGo_VawwD@90lQg&&E-g`9xM4h1Qc$po{*)3E^WpTQ<&la&1@gRKlz};VJpXss z*0hv`#TIxs>Xl~Sjb{Dn*+Zu(DPhm!TK6OM<|_CalZDn+x_Ukmg1W7ao35s@%#j=t z+GW0(uHN35oYYWe&DC++S}~Wz7*`ZYg0}w)nx*Rg`G;K8CoSaUZe+CJQL!f=b!h3S z&(*sYukCgG`Ey0DU-e69T4(m$u<;+te&9v!kPQuXf%W3Ru4_ z^V-!EY-Y!9uJvAbK@Q9TTpu0X+Scm8rvsPi7XD;(ff?4AH0A>G!soC4Sqh11FYjn- zBQ+QBw{f`}0PSPD5+7X08|J~t=nFlBkKss1X;Py1*ZPx*26tv{B3ygtji0zpBD$9d zp8dAqcu0lU1&*cqZ50>*7~&V`MPLf%6R5G7u1dE}z5yxC^X&QpWEK>ZbOT9PW3g&c zB?h!1ZQI`Mna=ikHoML6RbVM$QB;&o=5UYmuQx>wjHjg|OMP`%&qR?;P-W%ANmJ;} z@i45px%i(4ei<1VW?g}dY;3lB39dqdS1wqXn)Pe0U_8=)cQNrUZ7g`~FU)&i9iN?S z9)k2Kn>n;~GORpImPkgo8eTENm8pDH_}X%-qHALB>Zn*>T+%{Bx5Dm`Ah(`(MSc+M zx#+^lZ;HL74+`U>;@5=a!QBFaDK21b|adV47 z#>T9xY$mhZNeRywsh9F9ok`_86FYM34H@d{U7uj?G?|>(LR8~uVyPGneYN%)B|)3Y<=i0$a%lN`qjE_jO3fn1IRk|{4Q+%%Fpgp zhWUyeEaQmw#|bare9b{!-Re|2ID1_i=Bn~6_iE0woYQaZ6&5$$I2>gaLXJl@k`ZS& z_2nkL0S%7gCw$o|Y9t!d{|I3%sh3fqYuj_%OY$cJ@ zv_G-I`RWvB{uTlOmYudAlc1eqZx&IGEEe{3K^6|Jg^CspIBo6h=v<`7a<2373%;AH zwLbr<^|BodyN$tjw%Z?L@wV{+vOY$`BW4kR#_BE_^w>KqpWA^9=Ho#VN$_m z00zJU;u~l7t!A0gAndkYIbxDh(B;~nGKvjaT96gR4Z1o^`s0S{6*p>+OilVcjzBXG zK~QU@%>yt6fHIVgB)2b<3muOSq>6WScguEnfeObn3I;nq?9l8pzMIkvouO>S4L?x} z&Dp*hHjv(Ns5zKi;p2Gkq$eH0eF5Frc zH^1iM@+yQO04d$Vr}LjDYP{c)0xL?;rzHw-4$Pco7odp?P#qGZX z&K+u_j1PeC7Kn=2&31eVp`tfG1NP>Ei*(*fW{j61w}am2o2 zmwacE5W5+)+L;=g;40m%%MRQTw<^0@9a}})TDjBl&xno5P&6%jCm>F?^=#fIA$}$# zG`RLBgH{0GJ%=9MIoC>;BtZZL+rCh%Hcp=}?wv|pSTP*5>(x11+DguCPL?a4EhO#a zQGp@)q@LEvden$VTxq`?1yi6fBs9gL;QGOGhO<9i=M36x1RO7Sw9D7$LK@~`+|?~f zw@2;*U*{U=T4o>u4`SlVQk#f`gtV8Ygo)$tNVj>wq~L0>X|UOT_$u(NY?k)Fy#W5u z(&}o%Xwp(BTAtAa=oxjk@;rNVXOE5u4_8PyHyz(2x9Y9?;i7EdaHog^fDJ~Sr;3NW zQvHEq2V=M8j35-6yvsL}DF46>g6Q;V$+iVFT8QIyA5d3~0)t5!j<&Yw&#W)*mVX8Z`SL=Ts z1T}g$h>6Fe96&!KV3ltpdN1=wE&RZCr-RF!Aw@xxxg^9q*PMzf71)i*o=7r28!Gn{ zCkem?qFTk=r7kbfN`Q~VSXKkrr$7#ShZQXs$Mv~+5m-UN7SybA0_mu~uTQf+OMy9P z;vuMNMzQEp!MkWz!`TqmYoNN0r}UL{WU&h#&@XzGmyrP}R4>*YXhgUj>AxlD8xzBwHbOOk)8)xIY3LE8u7C+np43TyI#W zT;R1F>j|TgaK+lPXS+;7ASiM9`2>0rF4h3xX=uELo!?~7!=T*?Vxh_G>V@i87cQN> zz4;p1*v(dxbuU?jXZx!yljl(0-rh~;hvnI;+B|J7Egl}1A(XtEv$qYpXAi4wrV|pv zpDj=V2RPG9HUK%q3`s^7(?dn6MctE#NQqNT>OJ45i9u;7(m)AJ_pcJF@UM7L74-@;?!CKvErdGv zf@$KCdv$jecA`}Ss#-_p@?pU zulO|X?}t|G+c+om4gl>fScfb z*sr<#Fmrd;`)R*`685}Egr;n~SpfJJz)qzkxW2oX|LZ^Y@xMW#{&)Un$p6V#y- z1$3>lz|U|pY=?DbRgLVGT^-qQl#ZL;eBtRG#X@^C%O1LvZf#AjC4l{jFh*Gc-&j>o zhRpAqUxuv|8Ea<-Ep9}?m*aCd4L-x zCKDUVPwB>az$>jK!K7yBsl=F*@kx_AQBx?U-0L6HO}n-B(w-(dITLC)zVL1ZW@Vfa zh%zZ@I(dv#n*8t;$EJT%J76Q>%E~WENtIOqp|J#82~@-(GjA8E1$j>#JO8>`NJKoxUR*92J*Q@Nk|GohO@Pbjw_>u0H~0%LxJoAKV=;rv zpq6B0Y%C2Sh>DUYo5lJF5JI+mVw$@pXxRDkTZnRp6;zTlR#t2gnvA(WUqPExB~iM9 zXJnV6r+b{N-}86T_l3)Yb^jM_?*Wc=|Gy7k?M2zEL_{Sil)Z9`hP{&!*y41?Ros*D| zLX*Vp)FCqYh&PJdWXHoDd7ER^kGf9`|G4#?*`^^{IM225kGN>MX3WSRzDRBtsm%{B z@1@adsF8_q)ef(2kXP===<%ke9675VaYaTwqK<~%p6Z%UzmQiD1Ka(jkwR9l`;Iz% z=6*sxw02u5bGH5xY98@zp|&T>iB`TyBDdPRaMd0w4U!hMGt|3e*=QnRtZ0+y?q8&3 z%1d>EQch&uAjwZC{^{IK&eI`5)-SKf71>)|vaF^vrZ&4ilUrCKPqq7^^*>uF_gs*X zaS=~!*@1&n`j3kI-fb5BVze`ih8Bj4Shof)0Nk8ckbL-^VnD%X)*or-g$C;@kY98h~_2L zE9X+{quvVBrR^iUbkkVwIvv`1XZ6m(-fy!h!+k0@5|3I%{b*{MyPkP}`l-8{JNnDu z;vYuxONL%NN|*A&q~nQ1Zbju{Z68^H;+5$8b1qdc*?+|bZM3KP`8HXQG0_g1IHb8H z7y44a38;_Fb22)rstADwj?@`OuUP6UHJXY7e9e}ZLRTG^o(3{^2nV;`f5`TWEa0OQ znf+~tEDs^!w!{QoKJ&VRV3=08tA zd8rNZACmJHer_MGGU!0X$R2WQL9ybEy6)#evrXpsm z1?C;ULgTaLWEaTy?8*A_atlG9qRox{`WKpv`TiLv1s9>REUzPsC2)nGvoDqS9aNq z>~$&g^st+j&NNR)+NRJgY!K&zh)I3pd}u*QNfmF}zDI`+9a7iLXsE4CcG$XM{ViHa zA`%!gy_-7|^g$9!zNAFoK3@7h=Wf2v#Bkhc>!!7&r$0GuOP3XE*J=0%TRBU^PShLP z#F=RY84t}F5`FvjXC;@&t4hRIFLrD;+>m7XTtCrh6H(E#6q>6VS-)%it<88}{`jx< z$i|gtvI-UAryd_MVxW3l^D%PZ&%n>i^II=wn>=S!Pci<($!9=pbmM&**&f$Dw+d{j>-|mXiHh|5Rc|weM1A}rV#RbMBu+tx z+&;SFhO54GJS)3+_tLw69xZge^OQ67oUCe8QojECbMOt&+%G>*Z5iqDuv)^8jv_H5 zL80pjq08%O`wT?bH@@~n(TF;Y&h4Y$n3}ive8A+yq;IWt@%rk4x8@tq`m(oJpuKly zc3w$Qk;m3mtx-&_?dgn!z@h4io_w=U71M4#jaD7G7pfJRFOz}Nb#F#}^AKyVO;4^} z6a6EPwfnw9^|8)4e)jfb2M%|>Rz9xp>ZENy-EzPG;J4~i+Dpsck_`fuG%J597MHwP zJkQSms=R{ss4qS4@2NHm?<~*I-pH{nnbu5{jW_cUFLL=(OFq`G7$>1N5iCjMJCIf~ zXv*|Ri+=yIie;J<>(e^sx_x&#=TiAi6%XvQdf~WQ^)%k=3yanW=_xnWN5(O44lWhX zTY0SY9?7iB(Gbo#>r(x~nE>D8A>R45whfgN)0mM6T<5L*)&5!ecIT*VPPE zFh)!!2Y>uPzIHF6C$WB)qxm_th&rcbNz0BW1M_6;#Dp;SrH)4tQD!Mum27@Hs@~yy zPp>+(jc1nf=E~Qz(to~nyuP)i?Nv^H6}SG{U~6hiWAw5{ViQAn)N<);TwMN`Cd%BSCXi@3OrKV z+D3nGz5ZDB;VO-irw19unsAKoYV{}gy?si7e!is=lPvodPi!CV;bfByKP~q2$dc0( zzvWg!ig=skcW3OBOr89r(6uO;B=&C1Hf^G)MA`JoJI;Ch>wIgCnz+mDV+m*xO}Fot zm7!UUtC{G$4bluJDqoA6lT#mm{$yp-$#qo8n|e)OC>(_7VeDIJI+(fX>2)<~$6l#M zSw1O|M^$EhrfzV}N=d1adXL-}Yu%NX2%#hE(61d?6|W<1oCr?LDZShbs{$Ur$ohKQ z7Fi>9I)4r-UX6|LEal_!&VsdHzI2yO5LXu_(OKFlt(OOVPh^)8FZ+h;Z# zmpu$+D!dn8%1)%{vtDA}IDD(o-uTR!$_u)(e6&NVicG6rhqDZC$^NPGOZxHIL|ymd z3!^O7sA-#uS8Enu(=M4-cL+a|x!e#N9mCH!M@D(>k4&HVB~!CE&Zh0ex>r4H^h8e? zAq)2Y(JL7*O;^|ae8~J#4SH?MeF9<2Pyu%QzF8_^(fnRDHBk9D_v~V9c=#eZ;5Y5A z+u~%&%vC;UEU;PE2WVi-=izmg->g@mtl&oVH$J(S8f*UFO63Zg|Fex0j#6n1oLo>H z(;PgfmmThUSY(Mkju$Mi<1SL3X;NxYn>_6)Fc53=8fC{mJ=f&{LA_1mebyrRvqHYR z?Hv#GGb%%)_P>0Ewk@Aay07-R$B%4h!^;~RVv0{} zc!g8vj(S@&`|&@8pb>A*)~c$Y3M$3poJK+iJR3T^a&nGkzSu6^oF*Ln(87Ysu*K$R zww%kUYDR`3#<2CP%9|M@Y?*@pE4sqG0w% z2itjc-zWGL9Q=a~7Lv+L%)phS0@bW9rGk0(HKaq@_?+BQi3#bdvf*3_m zY+I`-S6WjdKiBMy{+~akWGos zGV99c-4~)?R``ng`1Uagyq@|KkQnu`GKwbFcv7D13Zrv}6LrnE;91+6WdWn+KdKIF zH>^2=*c+sx?;A20`BFb-$RR)W&QOMGgTQc*vEB9TIRU?^`Ht3-1MI=#?$TGuLuy}b z+a(L^;MKKu2zy2H4A$?gvX72mvrx^;yY(r`+f&5-TiB#Pj`f9IC&L!+bI-17TpNzO zd}7u&OYgnDev8xS50%Vsx`k8KV2?tFtbb_ea-*dsKdrdIc&OU18~vyj^r=8-ZFY_M zgMnvh>Rp*UQ_3$5v7)_&m-6 zF2ts(bB+bXcW51Tpm=57c4U6nJ(1L%af^%$3?J;HHDAv!Ob>bf;XLLqGuUvPiheyy#pCE zjdli)HS-_3z1u-i1ekN=$VdNh?v;hE$tBUPU$)nu@iZF^T~7-C-i`LjuXE)u+blgL zJ>xI+Tdt1jpEz`q)$jR_gM2mgCqzTtpM6R?*3!S~II+E>R#?tC$mGNX&+usV+wrxC67lgi6DjC+&DV zFKB0P30abqRg+m^I`k(lcp58higtfNo)g^M##h5w($$70#f5}WdSxn7hVDhT2uBFhq4~!;v%3on=g4C?2Jn4;;TW^&#F1%}WeZ zZ|Qs8)S^;Tk`H{Xul6yFIM2IHD^oX9m6kfVG^RU6<;If@H}1ZQnuv(|ZM65ws_(iR zKQ8NHT);X4gD6dYg*&ng*`O%oJ`I-OVYo0?6 z==r}^;9=2IavE{>&=8p9F})t5QQ_fQ9R@Cb-GTcC9hR-KOQ{Zu4#J#03kgAQLRM2s z;;uPCHAQlx9Y>wcI=Y0mUa0@$J=di_w8-*3PT4m+j=@EgGIcAFv9V5?g zNAa=nWqAyId&=bWin>>=RzqN3V6~3(cB4`A80@WY-$OQ|?VE+3_Y8%DeYLlAd=@Ga ztr-jUI@$3(SX+AQk^WIR7v}C|FBQ`>&eV}z^u7#=+2l-{FREn@?}tB#ixwwpG$ZC_YbihOvJrYB}>LtwbimSJYkj+qLh7h zcD7*hd+plnX2`cpgBN@p3#FGfFDWRbUnDc}V69v8X!{jz1BHf(PA9$%U2tY|OBcG-%Erxl~5VX#UPQy$q) zaeI;EDV~tKKCdK{=ttX>JjqNN?JEZKIDFdX%i{eTzFjao$fo(`fb)j9?P-Ek%*P*> zi^u!=X2;35WH43-7e}ZUJ&*fBLr-D$^Py4Ix8OXrr`obE=lpk%&kH;j>IO4e}Gg8ygM4gFTkg$Cx78`^9Tc_C&zuE;D`9@jHfE}4wV-w_$u}? zDDb|n-4vB}-EnE+lCj^h{PUUpLRo6nHZ95SKi(^4cLpWBU|wJT{_0YWB=d3;v{5^* zZHCyzNz=#NbgQFD+8uPZ-{6+C>&*D;1J^#KupFePqCDpKRjy}sEKsqqSD}4yyRd*s zc7u__>5(pR_Yy*49rYSRhHXLAEt_llP6R^!)>KI;?-8e0ZO_hMsh})>CM7R@gqNE# zSn-O>sSzjK8`WC=Wbfcu&C6rIF0yjpT|8y* zTpzF$1Dg+r=EbdB{)5^vc*XK7O4sr!9V0u(;LA@@OJ<2*aeeHF-J~L{())apeLtTQz*mzkHb#STw2x7rJWTpIqB+|msL0YA`NK$CsGCAp2oAn5fNtU zHDLw24D!Uf?D)$<@!~~J?aoWVM{Ulj?;uE3B9b8xtW5f|Y#Ft>&JzgQ`>Xycw-L1e zRV)8jr2O8$+T_>&RW|>B@)GYfu`hzBCeG(qlWH*pLg=r)KDE?|^k*wt@BYJHO>ttF z`b)0-e|6peJ$LutS2?deC@vgyl8Z~7mVU&Mso{gh4f((KtGu3n9eQPg9;^0mE6P8P6eepT@ax$Ndt`GIqK^AC3HqvO_AcDd`dlblDl zP`M?48=*wI7VR+$omSMG@(hAD(Ev6S6wt^43P`>T+oda4(nDFd5MEkxKVLbxW7jTz zvU1|W>}-0^r)t;uU-$pj1qfiwyEWXv$ZskiE7no?wX%}q=+TG1P%w0HDVFD)ouB7m zv$dTZ&15G0?5&t{oIu|{1;tc@=p^9u-QFCVcF*tr*+IA`^0_&66C!gQRDK**hoE`o zOUdp_5=(@O)=$^WTo~`TQG@l?mq}7;zY@lsn)OLIh=0QXXMs*_^~O5-y)u-K_qa}q zZvg8*2t232rdyq>4P9Q)nsRe@XJ=>kT%8bPgLa$haT;jgeb2Yjf;`6S*TY@5#F+4K zITDqlzkV&HE$fW7XM2M6iu4%5H$ja2KH1*BwPXRl ziZLQ>ua#Jol$1O@HwAm{unP+p>2&QOycC!|CVSw(6RNr^ajsvYOAiX#lo*!pl$4a* zAnuM9*h1jWSWw%9S{YRnq}!GX*2ImQnwrpqJoMt6l`-E{u&B|HwTq0wZld#?M-)Dv z+fv^ngj+2mjtEWS1Z@vf=HKl4rfq6$Y+RWx0fEBJFi%Qm<}XC(XNDY8+D}eS>n8)I zRP-6ct;CZ04}5%lK4gpze|{9jio3qEXc+>F>5z!y@oM|Vb^!vxNpIv21NZHQ;vqx!I-1Vf#{Deb{cDlla#b0ca>hbJP(M015HG{xZbJeFnZVC|2lDsNA_DL`h3Wr+JFu zM%yvP=-uSxQ024z1`&vrdC*>bFLKj4k2HJxcII1|+1kd%#d)HG2~YhwqqdC9!`H9T zxngN&N7BB;qwRGWI|co=v>iJM%470M?M6A@ub)vj&PGEN*XH;sn!SNqCib1%32(%% zyxu@Y!sQ+{3E4#f+a9O~G(psN;}r2nWu*zWY}FU*n>Ux~x$9;;pT=(T@Rp4lcr0_U zvzNcMWaDEX+v#y{DuMK0!un3!&yvI6T42dJb$wjA{Ss>;e3;afXrcW?Qy zFRfza#Uyp=%bA9rt@ou^DW%xg} z`t#!|PdJN&+xL1}v%__{q;|TK*XAM5hFYdHXQI3BMLgXn%tCHUZ!RYpF>sza^QPnj z6*V;^CZW4`kcGt)n;eJb!Go=0f&@aw{gI-AUuL%d_0&o?^okvMZ?{2JH2Z3vkNnP_ zA0>DB+1L_5TJ!Mmh!Jsumb^KFO$c&A-47kXxUPAb9SaK!n@*0=1yxm5Y}pIHcs19t z42a{GgPujsld<+i_OHl$CmYVxgNgy&%d6;H z9QaK6A8EQN>X`^$$Y++huRgB)g@xC2d$IdQTQX^Rj?-l6W#OLyv|ArY-GxisOKFup<`|>s49kea4HM$*>`YQbICuUyP4WBpkG#DB zJa!k_C+=llI8>dK+?`ht;_9+=Dd`=FH)FZGE>|@*^Yx>+n3x-uo$f*X+$yO%Mp+Zk zAsDX=hMsxkD9(oN_3Ny4xlk+(?dBB}ymp*MRJiDyj5Zt+dgFLlAsq1`8mQX@y5Q~mCMJS3**(7xuDw1X>Gc{ICy463!K)X=0r z5ezOW?anpxvz1hrH2zRvzi@?Ie8r@YgIE4wRSZCv?wg}I+PdDQWIMN2LGMVLp)NNp zQjvVv`S0%eUj#l3o+Q4{=1!N=4e%#f697o<+y0<)|1N!Xn0_U`mRq+%@doMK;ry(gDqhXHRr=ek0787co7X;hyuRdj+ojC zDcvHMi3G*hw0;#3QrqjD-YP3A!=<5edm%(F^@L7lvT72CzF1eG3O{a&XuHn zk(eSXU!Jaau1V;0>0EOJ?~SyAdJ!HD4wD~+_NZLN!sj<9T|uYMK@zW%K?sxsjw@mH zF`|o8edYaCfmbeFN}aeEpQV=ic&Vhft#Um{lNq({&W=v-j{l1PPL6d=;g`C)T#Jr6 zF^aD9ofksWQhbcEUWhb}DalAkNI?5w&|qwS1=6Iilaj6q?JLO3>mqs*Ijpu5jyEE# zd475DjbZs^?%tBSd0=Pr8DGql(U69v#2wnWbtPQz=UY{#JFSmdD{JG{SHu!~+yZGw ze&x8mJvSlzB!x%tGxV8%{PX3Kr@s&p z6m=7XoFM>xn1*IxczE&KHCuGrPIrM58E{arp}k%3=uu-d&cnO#-F7lR`vSM)^YG!) z@-q2hu_b(Nj#`5}(?IF?LibgH%>@|Hz1~K{4Kc>eSwi`?BLE93QuK3W*phvqx2mAX&f`jAR$98PJ&pSEX@%{Vm z?OVM*PAyH}7+Alquc#QN8hMJ!&+7qGR4<=UO3p>~qSqWC86OXI`YkzJ9I-M>KLoW& z!0Ip0d-3R(6kh-MV6ZMcPSVo@vD%tIfAy2z_s+J~*5esh^WYaZppW1GCkHx=_V#MY z^2^9PpmxE=&JG21g*ukA<`i|g=w~13g;3d7F!ybH%aXgEU%iM{KJuOAkYe`xJx~*Z z#^O1Pp#G%-bPR`IJPNZAs->@z-q!26xemQ%#O}pV17@=3CM<))1YNa$HNRoyxtwN)OQ- z8v8w$X9k;{p!0x-0Wsw8tepBOn!S}cD7|j83Md6nO--ToKuqbaNto=Q^YzfwlEAu? zVv>@*-}htNc()BAE|IaZ6D!yiC^ZNJ(Rw)>L8Ha@V zBqU-z&!ovF;`3ZiYL?I2CRQVVjP3MkgZn=j%MG`0jSn|V+6-}NSxb0r?vlU1_ zVegrkn#Ky*8^P-YGM(E?MuuqtqPNEp^t0^Te@N*12gH9nqeYw^`$Dt?zkoEw!1wRp z5i6hL;b~aPR8&$zm-TA2@GV)~1j_Wt3$dBUy9yfL@;Ey$A{mTI-Y9jM=POMwERkk9 zXF1T<_wm@eTNeDyI^9;Of)DqH$4J?PfN`lV&s4T!43CsQvU6qkru_=EFizf=!4`NT^zzAO zujKYI$<4J{NaqF~6bzc2?9I1ohxc(sLYXM{1VQXXPiYjYnj^6D$Z!^*IiqD6Ci{k~ zsOU1AgJz+o@r?_W0;_V%cQvBfb2f0Ip*uEUfY=9Bi1szBBHc3dMqCN!fg0o_ z4)fXCw%n9BYm2?_yg@1yx6ZcrtPH&q@B_hZAaDN*;x3(Sf05~qBFeFNb zOV?gPM)kuz)M(?vyZV|>{ru>}g*gGJ0~`{vyXScdbx{lsa*0ChO3M9^^FX3Jp17ik zim5X#V$@1^oG%s^>%6WR3+_0 zXC`t1QcLiM)3(U${OL}m{n&sjtoi);4pKkx@Gi^XeI22gpnijCTJL(2f7mYr`$pn6VvzR*kFdw7R7ZD4YQueCMT zS<2UtR<~ZZbh1ynk;d!9F>m|*P2V$)E54uG+Mpit+>n8-wx$L`DtUo{Uw(e{B3&az zB@sEfkMC0Y9k$@lh3}P@=A@ST1W7hSS^24b<}}N;MD)2FkXZVO<{$X!g@px}k+XVM z$xpFEt-Fio;Tk}X8gEXj3q7fY00wz@Noi@|!RpFN$k0S#H;H5C<7S~sy$l3HLrrwc zy*9>kmd~F&IZ%EECpK+D`|>Ed!=Vw~NE~YHYBVnMs+!BNn!~AeI8=S?fy}_f1oNRo zZm=E0fjzH8HmZ6dQ8DcIuD-c;`~%mM$iOS~9*9c+N~IG^&U@|5%4+-*z&+`kmeZS7 zlEC#n;`e#4&hj?x8d?!?9a+6g>*eL;fy1X0k)|NE{|%dr07S-dY0A)}h)9&ATwmz2 zb*uKYv`m}kGDA$^cU({~y!S$qYGrjb`1*-5HHwOgc(S0sUcY_&Hqm4KD3#xc4!<|d z`UO^sdZgvYkAE&NPsFK#QU`#i!R$v=Djpsi*g~jb)vzeyZ*Gr&he#D6Rt}EXO7vlF zQE_pYgj1B($Nevw-#^z@9r|?nf;8=tbCN2ILuSeT48F+T!V+bLT{%e%sEseg^MN>gf7^s7+5#BO#nb29G4M z^1!Z)$J*A2Fw-7u+Yw*6k) z!9wKc+{7-Wx682N|Nv&(*I-TYBFy}sBRcg`wIo-+z6?ii^N=$ffp)QmY5 zIL7GU!tO)NHr)7xV{W1g^AsU#9M)lC^7oKMi#iAO)_wc-4o1GJKn89t2Rc>4w}whK zR>n~51Fp9A1Fg`S{*!Kq;uN7j2oz?AsCfWF1E2lOD}&(%ogE!_U0prr+q8^w?qHE+ zTH*O{l^xr+Lqx@C>|3Z-B1hsWO5xD9`}1oDse|!Yu_?7Ow2y?;U|<=#bug(e-AzVD zU7r{lS;d{VUN^Qi{d%u%lFL^h3ecL6Bt#fSeDvBi+rJuEVU!aX7?_xtxLmdUYYocP9;Vji2}XRXJmUlrfrIXK`| zM0yU(w{~x3O-0_s-n!9)v+`O#GS_X{0W-3$=9x|x)N3sJ){YhMwM3Y_BS!MPd3@Fs zOK@1i?f0KQ4Y?Wm+3n_MlNSxp;gq9YpPLaC)rB~`zTl{~@Ufyi-7ICUJlme)&Xj05 zmuVXgz0C317l)W*1{^A9cWB2%T)Px5F6PFSucXuI_2FJ}XeWyH`%D_dlue*{jN}?| zX+uK;&M`1Hf2dsVF9hC^)?6e=!aHL7wpd<4yJDezKN1U=;a1Cec>QHVN%$L`QWv1! z_Rh|p80LZKds_YUne0OC&^yzDnVMs01V7YT=-5kCeEmMuF< zT4de9w^3XHoD#>!T~4kVUhJ(^`~JIZ9jU3e1G&AH`)`?qrlifu3$&HEO6AC<9+jj~)(;UCLC2TGsd4rV2CrhkN{j+gi zJffO9O_nRv+dE&KD=$v$Qsb98m304;^AzU52ZJv%GY@9*%#XG%d8mb5StsAQtuB`? zeLvwtt;XnSx(~#AQmh|lUHX=51|N$=6-r9h4gLboN|z2y0v-95qQHt$u5pL29AqiP zujuaO@el~lugAB>nOx1+iHVK1`48j)3{Ex?gRerngH5h_eN?7CvIPJPx4@Ym=^N`% zW$}v9gOfpr=KJ^PR6bR;zKx(Rir4)OW8>Q_2ZMs{E-#0kI?w%1C0%3f`E5K4#>!E) zobOdzX|Eoi*E&wKV|jT$ALAZxcw?x%{MG;|^)`3is;Mo=Y)Mej4@ScJ@V$dnchJc&_658Jg6+sx5zLX@>5) zENS!w=uRPGt{4^>&CNwm@ ziKgF?oS;nof5OS?2G))HUN7^JfdV!(Mx=A#vJGs=EHP-Rac_kA__V>Na4o&#MMP z((zxryT!%Ceti43P!)Kv&}JJUnS?EU=mnbSEo}%*JAAB0DEI+hfVXMC%}P*y^!E;k zgTuqvTkW@Ll*M5j5J*~y?j@xB^PfMTb14P6?xx(9w(^>qJy_@uG^Du$OS=LlH`^)D zD9*f<(0b~>#GC%_*6Qb73+Gc`|LALXsi3^b|M%^OE0d1hvT%+gMt2q1o}}69>FF7x zXl9Dhh%OeG3KOc6)G{Km@;i|ff{cIXJ$drv*|Uz*zaUVee0&Lz-Zuhi)>bPm{4PfV zy1mieGG)!S%duL%MNySpX=&ir>_|szq*{nhC$m^$7vqJhikm6=bF_1XAmH3 zXg5+?_!y)y<1McnkxIVR$+X9$v<(<>7*oh5VCs==ad@PLEK&i|?6Hs6?VO#5A8iX| z80JS6ba{4owoY<+fSRq@{4CxX<94QJXAffT6QHZ*Q?z?4kuifOj6^dpFHgbh1g0s- z=EAq5u*dl5aOn3s?Lv^nfV*RGkM)mw2B^9}laBxS#MeOL_P92R761-xiRuYKdv+Scp^pJ^S7;*xH@Wohq+&v~xrzx{m+v6ZRN5BFcDem?sdosz`>tJz7 z=G9fvkA)AN(oY|nJD*hhBhTzWT`meP(=#*RE!v{8QS3a~kj;c_0Tn|$Zg!v+fNC%! ziD8j1uhty?2F4Y&DIv$-Pt((_M#wplwnJSF;V$sH>23FoZrni5ybACWCjrf z0&|=xny|>dcZXvDVRUwOzM$hnp#o?Ispg23y)OK;CqjR7^A!8c5q;#`J;o=DhJ@wt^+prvneay1>p!MFGtZ?wY)K~l$IdStr2)w=lQXoV6wWK6*ZVoYMK_fy* z1d7NbIw2!}{`^Pm)TKa_nMucrnfX-k#Q=_1Nl8!=yN-83VB&TZXwcie{iGcMU?&2@ z!`@Gp;iaX>J_2Q_p=(}d|3c7K0?#~%S(N8$?kklk zy&;0PysN9LsVS!-h>?*INq3X^S=2C5S4Pu#KQ4}|nw1DC>(_A9=E!?NkLl~>x!+cr zfgEhp-!(LNp0Z5Le%!@^9z{x(y0AXaIHiBN0H^x<`@?zkxTyS4R=p$xyD(H>{q^Pj zoZ+7Ov|;<@8Iz!X$Z*U6@0`9e=dunQZn+&5+$86s+Y?Nra(0;K*yQBo__%fPefwyFk66yRy#c(^2N`YHe*TE&U0Z z1&;4Ja5F^+J(3bCPWeK&dW%kz`1k?)UWoi(=y@irJCQ%oHmB z=9>@g>yXBKKYB!sadqb!+^Y?~p5fD9NBwQMF>d$(W{&1^w~oa{L@c!^NuD$Rdj0p; zc+9~hO*aF!dwXfB4?F?J(=IdEIUS|0&NvUWwC8Z{upfKM+`9@jS>gU4@TI@W%M*od zLIDL0UNI37cL37h=)$r?m_h56G5pLals$nH$Dw#>|9d~>g$oy$#qQW41M2BP!oR4W zV+5J(*||AgJXKwu;R4>gwA`iluEX__KsBl9=rCY#cDkAcBSu(Q;wF)>17=aYc=7o3 zae^8-ilykWLe?=cF)`MbUH}M`N^r|T6xYa^Mds>G6W`|L%`Pl#U_=|RxX;1tekp)C zaXRULHGJn8`?XdL$s`6I@(O5^hVsD*e%s2*dq_#;TZov-g6Ol$5n~hE68d@Gdk8 zz4y@vAbEVIP=}toS|c$giN-%}lODiThJz@vhn&2d=())++KJ$72_2L8%PX8As8?CxWrH`1 zkQE{@rah^N0*fH13pizKacQVU1%Up6=LVen1;1I)NXRbKVPeC=1WamaP&ma4$QaJN zw0yLOpczyY>ex|U{#teQRg(B_GIO0&y*U?mII-nae-tdC@uZ(!P*9+B@gh7zl%(ed z7R7|`Dz-Y%J7Jq2LP$%2o1@gvf-P~Hho=WR*yiTucn+W%jd*>KcLt_z2?XRElLAs| z)F|KrVMst6v5n>-ftV&?KQP;}#UWPs%T~e%;&&`7&bdmLO372)ioJbKP9CI-UO_N6 zS?NFu--g9D)J0N1l`VO1UGwMYE>rC|OG;CgDWl=&7N)0b>gsOU+5HAr9i`CU!^3hi zsIg$WOSU#Qkfd<{kH-{VRM_?+TmXI^dE%R6HR3vyj`HAx5UGKL;fUK>MBF2yF!S`t zKeZ=2-bObs>VOL+)`BX5zQ^+5($W%WASQs&fv>m;OF4CbZj=;H({B~=OCJXm z0THN``E~=0R!8iIcRhP&>Zj!i#?lRfax&N4$~AM6BHssM)RQNT2$-!B;IPME9H%2> zJRSVE&ad;3ROjD_*NJ_+!|DFA871K!tu#r(dlT80cGnT9c}#^$o1h@PwE3&azp31F z!@SSi%YcLM{NCR`t;)@iP2_cdu(#wTDjR2+0;O0}CZq^2_kSeAyH5$Gs*hf`%~t2y zw{IUP8YX;<4%PJj`q`%;3Htf-SIc~dU5`p0Jm?JCO&FyhC1z8C-qI>h<7`id@=5n$ z$uXqCKK-{pP)GDf{LSW+HFWVobh{|GNHIV7dk;gSqc1{3Io{>BS)Jv}(dMLUzb&Io z>k=r-N@bzWMNc2z8MFhJ4vLQS8N|O59WTo;$dV(?Vn5uc^=dgGAfhCpWG+^4J=>Kv%>5i4D%Fkr|{mN$s zPNjZKw}zxFMfg8Bg%2+5TKi9$w6^9JaA*f1<;cI32ZHnOtpuq($iA^a=+inuvlmf( zbWDu5^bb>9`^$gwBt%pHQ=UX{^7USM=qS>S-bI+Q@=u?Bbaw}%nBTfRUKvo$VLCbx zJDmHg$dN~5r3g~YM)9qZ92^&p(_pHlPO=DraFU*Z;d8FFgM-80bP!i0jSCP1>!Gd{ zA3#POR4_nnfANS>3Q!W@6#>%lIbo+#0va*=fc1&U{jqj{q4xmc`X!^B!gv3GZcw~K zjfj_kjPc*J(*M%x_+L_Z_REF{&g z`=kqgAQ~`VUdK%Ki!yKvC^P2D2g0EG1qSBV>zh#R^~O_x?r~V^U#d&SRlFE3!Rx^T z?3bm@Eg2Zw8;KentcR5-1tH}YiYv-PCQ#ZGL`PK1%rxm8r1b9Ae?yFn%`9?z{P*}c z2{t|Qj}Pjy9-%s!K~J6#9R?q3a|Z!K?XIimQiB2RBq<84E^<&^0`lzW_*RPbJHkms z!hXjE1l}P=-?o$Nb3=oK-A^f`ADZdp`}ZFfaZCZ$K|GH?Cxtw)i8!Mqz}WG0%VR|P zpr1v7#e%2{HWf97w+OEckSL;RG>1w8N9c;}AL6*xL4j zPY1}n`Ml;%B;1u6TB@4}(R~bG2;3YU z7?^{W9OU)AA3mH)e+Rl5LJHVq&?j&rQO98~k6`yADq^Tt zSfOBp`~~sODvWN}?%Wql2MPKFhR+1Y!a)m^zLqMYcSNoEA3~4`hg_k2>5ZemC%KV35_ z;J<&HZ#emtUWOC&4+uD#02~RB3JQma$WDgV?xW|K8}GDox(7wF4mx!qhFpVZYhE`UtD5ZccVrpW7Fr(KoAs!n; zV@exElz{B@wKY&E7Z@PXpaKDJc2zO5Q`}d-o4<>(VrMJ;$&=58HL?=HsSBCUF#v-5vU``ARCXZP zaXYtfr%-Wm5%Dy+w=wE>`c~)(AhFTWPy~mE)0dxtt^HL!aXhp(ARqwHibjcFEpXzD z^N7?=8Cclay~&aM)Y#ZKvt|4C?Ip7fqCg$!KCgmW0+sz%*uoi8H6#FQG1rmPt$^xr zW@Z;bUpYT+MH0+sadB~cW+BH%zMmios$0b8_*)rM1F%34a+K$$gV{keRC(vqxx89+ zd;4kp13oXTa|CFFsA3`ho=IP_KZjzuc~im;$qLCMM~;Yzi3tlw##OzRPj;Qon~SKe zum1tG%Fb>IjugdoNIJ5xJ=>IYb!Mi&uu{Ectt0sc?VdfsPoGgJpI4!vrQDt~CSTEz zr?}n`YgQd&7I$Co`2+j!na2&cCA?&N{orjEQL^gVqOZyM9gdQd^q%z%VvYR0?xu8Q z^%AoIK|w%pFccc|CuzC7KAHneVq0hT1jDZPGa^u-LyF8hKbYnLVA2y^ef;E!mYUkW zy?dYO-5qJ>UZtU2x1EC{^fnn(CpSE=R{8a2@(7;Pza^ds9 zZvKJ&4lD)33;2&6lUs0hXUxs7wY}MrOdg{s=`v>ygceU&Pggf(9+kuSnHdp~s^N6L zTQA^{oeX$aj6RN(bAU&5c?y92!KhG>W?v6FAZ}IMjalR|{AT;l=`>eY9|P+^zPq(m zohST?JLTowMR%sz!t0!zoRakQ;q`iZdjV&C`ecMD@2%~xP12ym)DSCvt=OOXnBwPm ztUrN1<1}e%XfT8OhE>D8TuE#KXN8%CB^!CViOB_pCrCA0-WdKfqP@x2zk#p> zB6vnNHW+mGG$aI_)|kAuZQC|L)0ptD0D9kzTHHW*=;fPer17b^csm~PYu z)a#(;hwa|nOqxp%E-{`Vu)FMxK|VoEjQ7db&M|6AxR8760??bJj2^Fuc;}#Kz}77}S|NB|FKPAR=o|TFH0Td-m-^SJ1Olw_uFmgVIw|V?dO^ zCg3-%XMOm2|1K^rE)mDyUqKN|-Q5@`nSH(P;ZsDrbp9HqL4^?LL`nlc_WVRfX66^8 z9OS<*f0Fi&772rw+JPb8+?yQkuU_KV0RKQ)0D%TvD2ZIK!CWvhFd)-S4nT*L!l1%@ zypSE`b!-JdMfkD4w|5gZ8z+D##P=8n2c z-|9xfCOwZE@qN%FL6}-kTkUf5JPJdbIR^ZuEC?1-{Z3T&*{(1ilWIjWW)?OOql zZ8;ZAbc4f{j(=01_k9bYjJ}ZcbRHKYB$Ag80OkvVItY3&OSnU~orpt_$+AC0gT?c@ zLB)tO=iepx`0PCLuyGhi(wD+v`qzb#WB{*l2<5GPthGnOvtIrH zK{+br?j6heuC8k?JGyIX;LR0KpT0r|;7a zWGZ6!DG6n2`QiRrOC_gJvynAc8)hQm!u_6@haOGg0CR${7AYXc#7Ig?0&|DGQ+6gy zcE1|<+SRk&1cSh7aIygwn z%&fP;%ZYP^J(}DyvS-g8SkqnUmg<@L@4wfpf9-+qdMy?@=|)XT@M1Nl61nPR$Lj=9U&9E#v62qMoYLRAO3MT9}V4AR!eM4EQK3E*2pbU)`LL%As)p zE`gQex49KP4c#{=NG&I_ZgD43<{aaN> z8wVZy_wLJYZQUKRjH3eC_Y2AQ(S&Ik-!g*tV;UXhk|)X57U|mP$<4*6XlQ_rN49%6 zDFCmlV}FQoB8aB?%Jd0Kyto{UEl!(gP;#CN0v0J-V!e@ z4jOzflh}l-oROv_c!=er7%Q82RYzxto^CG(6Zea6~ z^mzpYu)0pnlms_HyL{Y&1hs!9&RX-=uXc`(t?lh{Jq8DOBjr)_ffsdhn#UvPk76V! zANpJDg35+8-$FhT@+UL)_PoJdS-PenQw$hkfzYeeS z2><)Fiqjw`eu^DcIt)2-gXFN#H zRQ7dF$DvWdiLr5dD!*4*>{|)Q$xd3UzSL#gj3~tSJcA7LYJoE_QW?Z z0+EMlIJanh9CQG`NTXW^w48$c{LJ$g2=}tn!&E&bHrf5X_nyLBzWs7DM$ zD)9Xe(%u59$~9aUMO`YWgkpgLf(R-gii9)*3QD)2fKt*ZDXjux5F#ZYCEd~u7D`B$ zAfmzz2Bl#`xZ4To)5N&Bs@i9s@+9 zgaATs(6|2C#5q_}KV)WJ;^08OdXek4OUNF5i7l8{Eer1$PZOHy zBAd&gAM)}DHlzhj$r`ECMkpub<>f6cvvNKHX5K?iOMol}2EgGc%%}fDl=u&A2QFc2 z9y(?SGDd6A#vK$zdC#P&Yi!K-@XuG{T&jZyuO>93u_IVA@qH)|VdhW_gC7V450BN5 zG|B=nB2eT?npAZ#n!p%>Q-s=Mz_JiK5jin&fz5aj_#hY@R5P>$|8kRGQJ#Qg#o12Y z06>BQ$lSnya2P;d7#SH2ja45uhe|Sz0q4br2A?F*K!f^-+CfVK`F`5~I}SkIFn+uA zq@+j7YUn|GMX-E z^F^P7F2_2jw;zw(8bq-u0m5KGZOtk!M(Otpx4Inh?BZhF(oa0^Xa#YZA-Nc-;X6Xq zC;YdV%MC1iFAOd~v@-a|T`}*a5KyAfW025epTU--nQcJ&s={GzfP_94O@;CeD=RB> zfX3=|OzOC8P;USTY}!A=;5Sn7>X!}iBCG&Iokp<4F!^(D+O5)pZfSSr&e_{CC;hsNl3i9Uzv0f;#o@xG`wm*C&8Q-L{TdilklQ}%rXaIIQV5rUgHOgYi z6iLQI9ffBT=U(?aLKK#6kw7>@4=&a|-jK(H-V)ppN?X`DSE2W8ZD|1*fHc&^m>97@zw8@9IKik~@7IQ6#Fnv+4nc-N zK;m%F&z?O48yY0Zi2GJbQgQ@h5Zu{7fsnmMh+_r#a5;qV;DVqlabCBD+at@&!NGy{ z%7c$hO>e67%sa9?G}M)qVG=y0t=-$u&=Ab79UmJDxS_Jm7|RM6Q_x^3(a{*5N^oj( zhtW_|uLbfSCW<=rcl(m!GHM!-N>)-^Mo*@%wrBV5imzYqKq~|05}h6@&Q6?)6P$)0 zV4=bBNWH@1F%FK$uqCk(kMrEz9Gtq)e8S0udk9Dcd_HQLn%$^aq1u@RA~?IWH1yyK z*x!VqP1dt#VIk_sG4jV}H+6$cf|kp;1yB+i7=mvKP_XFp=;+^QrV0Of_ahOu zXghPI8I@~r;vAuU!bL+=55%VBvCBk6hb0_CLro1071Q`SI@DEzYEx9saIwV|If5Z% zS9a5GM=5$l_RX%vqhwT%o06|S_0GiNkJrur1mG1#{?2D5iaPwCTk&6veunO)aP^B! zGwjZ%h;E2qRo*xDZleDO?uUQ*p%|2d>0(6J0s{hCY>0_w1pg(22v);(`p54dd?1R_ z_*zk6|vkAi5$yw9i%^iBw&=8PW#G6-g8ad8p1(QEZK z9tOX}*jVI_{H-{RjvPMHY9mfG)BX2wBOexaz5gsKqoP9nx1V6%%*Tl8JlVEtx>Z_U0&6Xd+$BGyI-D8+- z?_4uHPP6ysRXI5Va(|MP^k8JhH-nT+6eywFgj8{epCB$`koh^;!-VP zH`^O8FCP^BS~vc1k}hD!tBS15OmNBFSkEr?CD6X#vw=DpOCQ7Z-p9tc8aLB5I?BZ- zN_KPzpWQ|69iKpWskMVP`PlBxxF;YW_DO=ZUj&Eemg%co3A^t-5K3clt}UPzXK`T~ za*#}ExV)@8fVPy#sr>bt+nYoAJ?xD(>&t=e5gcI=j$$pA79kVbIzrMqT@o6vA_{$H zAKWwhP8e}G!-bMpKhW{^Kv|0Pk{ruT3@_8(e~@QgoqIi@v|Qs?G@(RxB9+{{dgGG$ zf^HYpdCOjP{|Rkn=^DQ0ekg#6W}NW>s&6v9AMCl;MWyOLA-v4pKXuD%9i{Z+X-poM zH*T%ns)*mkc>bC4H@W&Q&k1arL}jP(p6ZRlf-wO>j-C(i7d-7K|LTyOsJ}AI{Tgiy zI;~wv!e`I10NRVpLPB6a-{qkAyK2ZEq?2)vGXlG zp*IbNkvE4nWP}(u6I5P24|4S1(y=frA(YmS$L_Yl25loYku%racamE3vylkvx97^F zMs#n4%~UV@U(CgO8I9h4p19C_lhouz5DmRM6YF7~u&ZA;s;PsIuQ>P5;0zEYlM+td zL;v6UZIWI^QeH*d_eqtcnoJ#5iw?!de>G_1IY1!U`c8aA!$f%fH~r0jNGAXPTA=^S zAMxb-U_QA=$v?hM)*Rkfm#UAC?>h1)biNpqY7u=$#g3t<1xoe zFE1u{0#8u%r~~n0MPvy=Mb7K8^|a3M(&3!?)v-lByZhFe&Ph`;)g3D3mHNk{*O-n~ z=GoQz@ii8lq^~S;_oI5pa@vv?8#{pd)80O})M@pmxHu#Fs1;krYxOIx`S)-6_7|8% zB~d%4lm?U?75r5t{3~KIQbegyEhk7EdR2cUmJm0#wzjsnCo~#rpNS|bS+?jct*YPt zIP8k*=B1k6#p5S^UgiaviwQbwhVRina~xpV#lF49xAnvi)9Zcy`cSq`7R4vqg$qI5 z)Rb1AGpy8Dq;B_;g->_gc%2T976+G6kxTb<6Bh}Ios3MVY&Zv!T`@VSyIksSw!sqH z9gVuTvo#PhBy!+PmHe_R!RI(uPQ)Vh%T|lVhgR@HaWU|><#GOow)n!*a3#Z$#GIUk zxv)_F0xvqA*})pMmtWPdOK1ycx8Bk)*K+ICcvjH-hF z*+sNSk-jFDcIbt8W8dqumj!)&}L}L-zvB-w*ov_F7gHLp+LI(5HQ*6ewQ8TxUBc_@s_9W$<4vw z!fO>3sN@;lm5~~7un19RmJ3-~B&YwFkNNV^07EdC#mQdj&qT9n@St{& z55w^xvDJrc$*e&U5%5%A;Ns$f*4YtLSJ>D@B_aC-=!jzX0jfaMpc=PY^!nsoTzcF* zUx?m2vMhhDr?d2H*ph8sXJ?Ui+*P7PattI)E3zC!+chmJ=Gi>DVBWnmn01SbgW+cA z8sPAonwo;=`93!^Jf>)hcB_XdjgyOugg!a>Jc?8y7Xc0gsdRO{qc~(YW*u|g0n^+8 zw>mhGO88P;8|($OA^h;qKo}42o;_;>w0h&T1bOFA&eQIQaR#ojzb(h@ zckuidp=oYpN(N*26tLl!x`c;l&qQmO?eloLmz+}b-R-1gIE)#HT5coSEcYe#B<0C2Z;JOB zy_MbFlb8|PayA&xF09iwa0*r@kkHdOImd!Gcum_)LZ8VjcTn_=zmk?g=>ak_or}jc zi`RUq`1$zPYq9c>(FlOT7Za5PY^EY5A>moQPC}1?tnRO&tWqj=T;t3IRjk~DW_9xC z?@wRQ2gw`zTS#b=k}jfr$16|Etl-Yn;yBOY7ug3?R06SnDAb%J<3H7anQK>|eV||v zo|^+ko`j{RDJzhlexSQpPAn{x$4*ktmD_Qtt0p)DsVfK+kkmL7RTPgt9cc()xY*dz zVEKdS9o>o3Z;`_lfFjAD1ct&}J7T$=2R3%lxyIQWD9crwoIie6C*h-aY;|i2a^d4D`AZ zRZsYcnst^>f8283Tb+@?QOALEq`&=*$MolGiZ{VwYWf3%nYU6oYMORzj`O6d7wL6m z>K=6bvWyOoJ%fAyv04jg9`t2)C^JEG4y_Xbq~8HyHZI)y>MB?zR)}V>v*uC|h1C-H zd>-RFiW~2awFYR!Kcv3+WAEkLsc#OKzfNa#Z)l;mD!NbNzs2nIK{@ZjmssYSapHrA zodnE9ELwaft&)ozfd~Beo3O3aruRW_o;$Oo--#%NyeCwkFU%)9&3cCmh36Cn2yqTX}6>Wo@4KQ#}d2^mmW*o2%zHZ<5eYT*${))y>Ec zii(Fw7`}V$SDO0HV?C;5x7eig*GxWqIMBfZ3() zgu6MpHY_#k=de0yXz;a?yrU59DNE}K+x^&KKMJ>q=g%cYD?7^N^bG|4w{lE|ugNSM zVeY+Y*UI$DqtQEju94mdg%VD(Uw(a8v@(=y9G}l&ftcQ1uiORUUd1cxynNh<XO_Y_DZ?U!kMZz^^Gb_i86`+|wo>-5LLjVm_DTO1J;8uY1 z01wOA!`CGwAmtCBU^x1U5O6ou#mnS>Bc>4mZ1U=LqeCyAHyr2W>X%~~IUuTgh}ys5 zxI2YtObx42*oZqtVWU@zuiVHw9oymn1rcodo| z@McG}!V=|Owi2#xT(_LxlGFE+K4Q9LwZ7ZFHEk!+BITY95jM=LD|H%unVYMUZP1oA z?)G#i4(;T@gNyU^O7Zn?xsCn=Onnk`SQ=BmqihYAif;e6)zwJ$eSCb}`8hdPY+0XP zkJ)WU&ftEFCmWz3hU~Gws%mO#d+GIj4Z88e2aG;_w*T_XXN0yZMr}GG?o1piGQDix zt5>^;>F1AaSbUxFd$Ob3abZL@p*bc-H$q6JJ7<+VStsyewW69@Z;ri`j^dj#7o-fl zCD{`tmq^g;ga?_2-pqn0Rq#V|8KSK1xL{6CK<2w#o0l zk=El!S8|7l_r`&KYl{Acp3g&vD5V_$NqpBzGJ=etZQ?C&J@MPhOb;o87d8&}ZNYaAhTN;Lfr*zun}e zsH|)aYY-1lJ~$>od+R8+-3gk#fdM@xf2tEFOyCj2iUH-vxfNRbOWf{~&!b+C7QBu( zI(N=RmiTw%gHxQ;^zP=JVS`)uJ&v3?!(Op{M<=&aR@dlN#3UoP(&4Vt^o+v7krg_D zA>^mPdI3O%^amAk)_87it~zTQfT9Bj&X@X19*x|d?R}|I|4qq_%JET&@4q(zigq^M zhYa8|7Cs_|>hT!4p zDzaIpb2U{~QtK_UQ4$eROMcJr@FuFAt;O~{X5UEDjuLO#81_vYf`uU23xMP^Y=xNU z3U60sckzq4USjX-zw(6z&?=0KysdZ@)FUNL&ExW8jEm*S#NNjoAk+^XJm~lKZ2__Z zP#NGhL1hM58LKo@StSmO#z>Tb3#;WK?UTOd0FIMn3Gi>bQ{WVe5_3@=v-$F3f+oe3 z8|-fw1+a1`?cfoF{$$|$7oAM-2qg4~cyOl}yLSpV8@)SZgfP(`gw$E|u&diUb6*vQ zVXK{r;z?oG)FaCRbXOzv*L!vGhmS2S_5H}Ikb_$gr+2({bI+d=#v4}`_tXZr!(X*` zujf9V)`{Ev9sEzvvwcY|lIAV&A)0Xm(v^4y)jzesBl*=uY(hyK%kOsYwTl_rjy~yo zMKOM3g+%we+)DjfTW03(g#9l)&Vc1MfUHN_qd!_K9JZ%_X4%vz_j(7VSQODZM(IaGXc_Lykp_sz)k z&=(v_m%@Y277$V^k%TKRFCsI*uI=JdCV#f2QNdbXC@f7;LqnPv{%BKf3730M9%tOm zRy-+t#BArs<(&jHzAqc9U6?y?sDhBRQOx0S#ZrAx6q*l$L`qSS1Hylx&|WnBc^^J} z*fWV?DR18%7p(-w4xt#dpn-uZq@B@aDv!_vV2@5}OIA>4WjGWadr@Yi{!Hjv`L5n; z0t{=C;&dAS2En|dy?1{N``Fm{c#ZwLYx)rM{iv-)ZHhh( zkqlVbVjgK~#gfpwf5SKn6~rsn<{`n-`* zmD%o!@d?)5VVhkpVjkwaM32|Vab3!y%lqYeOYKs?vL^1?<8}})sP1)iM7?E4Ve0{_ z77{|RvH*>cJOTxv8P*cx1|k0{&=vh)B15DNz|Rxd!O)5-gKR>|%;4Z6h#{zJQZDrRaQ)Wg0F zeUzSoLG{t~g2;3Zk&VG(CHcT&DWqB(e5Gc1ygg*JK0z6G3M{R{+Tt9u*MN z(2E%1q(Wy4#UTm3etc!B=YOMp!GcLC3c1+~6bo2C=O2|&4{W+q966%Ge|s2Rs)y)R zjm{X?ttxH~j@Gs&NEMufAQYOmu~oInK+nSJ=y(h%kiv)l${98xR}lmc4aFULa?9~H zHcCo=f>j446SPV{i{89p4b?Dnnq)m!7yFP}S^Wnjjexn3X+hv|p_}32@e?_P2glaV zZn`1b4FpE*PW-3ML-WAUwHYxN^Q)gc$C%diM@Al`rA{cTK!LI$58RKa&r>C1&qfQ$ zr$u^Bnf>u!N;G~3U(}C|UExhxl-*q;#rOt21pFJK#KARY;H=sm4)l7G7edxj#a`Jz zEA&#Fp@IdGvSbn}w{J^iMDIZMV~gwG%<&njW_{l60RHGp0C0?qy~`W57|&#g*W12q z%FEoOJe+auue#>2la@s;j?1rLmx#w-x`+H#yw1m^;xi@ufp$$1#L0pliSKV|91s(> zO{n2Ka_#<~y+nzWf9W@g>q=aDxxDg|o|`*~HlAN3C|>|q|9|jN_}2f5vih$-BHxBC z6Kcxz@w0??F3X~0Tbu@TR_`6A-wwbxG?deMYr~#RVy-zV8iK}@!SHIo@Xc>6u}KRF zg>E$~4=JH^TvqAa(75ViGdfx?EfcDt-g#bYaJHXWiD_HFx{^3rer0l}m8Es5%jUOy zlkH-8W#xg<-!?-F9YtiZ+^@5iQAGt!@?p31qD!mkrB$xHL||>Rh#2KVqYlX zO@me~(#*dYcD6V*$nAo~yHby1oICmaBx!H&>DcAvVr^6=eN$Ie+k&)vlm(U5B*21* z_^d^AeN;Pk!tUvpd%fk`Q4*3FEiE}KD^^=untJ=eMpJvMs{L-|iyqjtu3u{^Q}S!jErH+DQBA*{;@^ZijZ>Gswo6; zY%TrT*M5|3a`K(1m;@x{hiRNY=U$V_yd@2Ed@e8~>Du+{d+5baQLx_5BYY#wfJ|%; zd5x3bIq#c%E{~}nDPe^W>a4zUFwH1`foH`$8tAXTSLzV(EwVqsj_(?@@9R{1A1E$TmO!=u@X3#)dUZcN{3^gE`1 zOP&yL9I_CV1cv?m`B~9Qi}EzX-%X0#C1|Sn&zq*E$ynjH?AUaL9x-U@b4r?=va(l# zXK-%dVej9zgGKdDJhd!?nZ zy}r81V4$)QZ6_ceeKY-EW#QAmp7*|p*8FoPKaz&mGwbu|TV+s~0!L6} zTd^H+LVr3(xVQRlE2XzYQq29z3d|n(=M}!|bM*K|ZA{jh4-Mo$M@Vj&0hw#%gv|MU9)p_Zp~LYETHoNx7iIZEk9#rs9JQ zG@O$<4b2EjlLPxtSgo~Ci9Wx@O3Q8i71OY8Ngwr=jpWl0-O!Zu-glzZd0ks4^J_=P z>L)i(V6bIS?9>EJHSKiKFd2+ ztKJJINIc&E{8&|I>$&&)o3n<;*c-V=O7D7fq9%?DG8C(VNZXE9L%Lc)LsB-fbO4$W{K;0(^!&TQ`iy?)YVgcaX%;!Hxb=Ex|g;b-G6omQH2m zE8RuA_gF9XX7j(J0A}GjF*4g~qMG->CNn4JR=&B-!zx!r_lN!85u*IC@D8;%Ar=#Z zEklq}9~QPe(Q%so_XT=J*m?l7)OM;|V0*{L8k(+^{pS06KolVav)A>(x(^$^9>!^u zrBlJd8na4T~q*v`P+7|14RB{#vC~Ip+Q8rODpQ_H*K* zmDA-GjP6~9{xg_h4?PK$4J+T;Bg2sg*VZD&b zKm9;c9E*x-^rJ^P=Ex9NHE6^7fua=L3km&>9hxlr#bgH{F%+PUtQkyKFSh*dlZKv> zhTBQf!${v_I80eTfl?nHv!x|lh@q6?Wo6}DgTlgCZUbKXbdLp+TfbI-&XW>Gy~H-)ITcBY=kcVgs`~RE^mktR~}fxws44YEzR25p@dd zLi*O#rlVNNwMtzynLYrDMQ_Z0>&kR(*t>z)02sX}M1j=6jEqB8rh?&StnJU1Tb8bH z^=t1u0hXG2!G=tK3w&_Y0mSsuOwIKfm+2V?x12?v>&_2dzy1uJ$&M|E=p1MIqHnRv z%1I@xH8eFnr8q<}LV)ZB_KFeTki9|geg#&*x1kiG-+WF8+IAW84g$T`$g_li`sd8= z)6LBhAU_U@{v*00H?Ts7xA@9IQP2yg+@Wy8lJfcP#f>8C9zgAZHELlUoG@TmTAP|1 z8)&#yzIyeFl$10pyEo^-LpbOyI;x%f9&Z5w9K4-48Vz$Abw58N-;c?C=FQ(OpZch+q7tXE9vJbG z^{A4Jrnkn>gP-oNed+AcwCiiYhvp0&T91cg?2h)xdRcu>#kzy6Uq1h7zHp-#XBKd&>l2TH^THlYns?f>2<=`PVH{GWz3e7p6^O`Vkq))nRNYJm` zhL3kYcN7{^Kw`w1TxyZK4yFhBoopM4S#6qnkqN4ij-)r1Hsu`j7D*6gKz z&+~UO`V0s6UDH<$V!soDGV0^UyF0Y|rhDVG%X`qHIWJ9wSf?yVR?)Jrs>JTxRXo*e z*N2C5yTnMT0p3{gsRT0& zKL&CzaJG`0fJZ)ca|6fZy7Z%$B`BlQ^;B0?Rc6t;N&}Rd`Q{x*586vF(Rr=ScO=co ztBw<&pYNWBc0_K&aF92L3ek%PlP**52%FV)qUpK@mN*CV4Fqs=YK zqf9Tv87TI7$#^A2-lw8sJz6YX!+JEuQ5%&$4}Heyd0N_&n{(&$=P4F8$1 z9$N?jR~zZ&4U-PSQcTpgjhYRR;w>U2HNQ-Hf+eiE8#3sR$HY(i0`HQPmF>yC&(3vR zOie9c!gs1UzV=qOLEN+WA(UO5EyB+AQ86*i`Pz{Wb(t^R;nbGPfV$9gd7A&hYPG%b zPq);*KJyp}Zx%MT+3C(}8VL6)L)69XQiI*WDa8-_4TK^kNcC;4th!geGntr-Kizl= z6@znykm>KSrZVSZY)89o_Gf-DyxA>+h|)*cLrwOZn4~1gDHZQs=Cm;0;uw$133~Fx z1;l2gpkv$V*!;|gx*fMoH8rj{{;QIa&!;soh_9+TXU$hC7pdj``r9{e5_-rlW#pu` zb^G@uJ3(+sCDk}@@mbHGp=r{S$d;SLFn0LwHTT^~^X635ENd6e{Y^NNZ|bO;Kgf&S*iJBiLT|fdtACwK63I^H z9dpA)i{#h$6H&Vfig7mHlX)mTCytNo-al-!G|tIkZ5@2!iN*Hna6|NF%ivkjvn3{^cL0OjitGctuZ2tMdFC*3(Iy&!5OS{|B!uZyqA-{Os3sfS42%gmsldNo@ zbgFDyvud6Ox9KFL4Xpvtw5%`~X*f)OQoyvmGE0ey7Sw^_{Fv8C-uKzs%-pY>(d{f8 zGD70Z$fcO$2OOk-RJGBj{mQja)BpHV&5vx(k7iQo-f>F?)Pud0Wi?O=40+#fx9CD{ z7tnJe8xwJ=z;KJhyb1mcd@F1J?A}dwf|~ls5wY{aDtc^puJk6j)hB+)M2`hA1?JI9 zY^vS)J^FRf5`z#OM*8k$n;aV-r)R#>0&aMna5>gr)p5Kt7^p@A;*R4ml^<_g97wmn_AT?&xMJfNqqxP!0{j)AnOwhICb^iaz0wt_5ru{!b6bpqKw8?0rwJExp(MnH~ z_HGYOIz@*r1~QbXN;n_soib}H>*TdUSzNNs_TYmZt)Ow2nnj6L_=zl+$7Mh(6Py06 zI>6~=W#y%MXedH>uuMFvnO)SY5%N<{+vRt0P1&iB2Y1W*5IlF}2Z-~sau%l#J=)&dl`NuapjuwGD~;8xyITWzbDA_8$Q--6 zk|DASwKR7S_kIcRL(T#E0sXd`*HfjbtY*&q*62pL>Ggg;tw4fzsTK35me|oxyFAaE zlzTP##_3-7&CkMP*tR|seY&L@5K}sv#>#Ta#|hdcyw#% zm9%Ij05U+S=Axf9A?b8A@4B-kgXvc9eNwfD(*cUVF;AX}I_qM7b}nf(bNwi8?4uix zlgd;7YXhyo0+Lqe{(Z;EwLxK?%}lfLCFt*~R73dfw!-;IitV`M8cmyWS(P~+wTjTGVK{l~I!bDeZ;#ZQeXZ!TK3 zLv7B{P4Qm#Ab)b!sT!Vw?T^uBzj(fXkF=j#F}f=<6Loc$|C^~bn^6IqF>BH1;tW)m z7`!MBQQ0hft*Z1IuE#`YG2%;?FQ4Ey|MA%a=9VgaCJ+_ty(PS@W_QZQ+R{Eqm4DdW zSATNVx%j@_%FDp=iff6yO~FHpMV?R{c-l0OPPXzSRKrWju1h03|B zTY08~6TI1qQ_Mb*7r9wYe;^3w#^By-vsPk$a<$~WgeRl7)0ew1!3&fosyeu{q{Amh zf8%+;HWCI`)u(m|xr}J+fi-FuAoZ85fqsUL-$7tVoKXewEUk4H%(FD|HR_P0{lV@G~oDf9CZ@)MS(g;(r0&Jj4!%9lrKN;=o>6gtJ}GHZAIB&OHb zs-9-q<3?Z3b5@!u+X?6Yrd-B7{i{Jn4@Me3wFnDnZfs5^bS*ELOL*klHwWQPN2>%X z((1}d?l2i*WbAk@>?EXc_|U!bdifHnreUv;X3RIaCDeKT1iNy0*52=#=&XNcK*V1j*^k=`9zP=8}kk00lU zFHCgMlr6MZ0Ll97_P(#MF(;{zAu^P%%mT9PvRpcWMV|j>Uw0vx-$DB4%)RIyp&ql& zM|=KRsbQi@N-{dIr@O7~nM2aQ9rUj#k84WCSEZz0efbT^T42X%@9X}}WdYgQG1eUz zLcKbeI*meC}MwM-goegWvC0 zv4G`fWx)?8;_*bKqu?EnN_Wwb2p8?2E*|%NHYi0pQ&Uk@hDh%T6eRd=LsM?QS|_Ki zB=Kbsq$8#$tEG|s`Ytwzh5zhepfJzjjh4zadctzhcYFPl()Pa1jrjny{DjI{aDMsa zV9F95u^k5HF>5m(LKi~q&BP{sf8)M%z(hQC4m)z>;%apKB#XMzOON?}my;}jd4Aw` zW0AsX!E2q&?jLTYVt8Bi(dNe1Bo5`(_Z7HC=FfoVQq5?XB~QB}M5((hZT5Em{*@;E9g%W2xf^bZ*F7X8{N!YEoqV0) zEuBf*?9<(>q1@-OJT3eoCx;kWX~h=#_fPohO883UYuw@-oL#|1ZIPn&4PN6+tL8S! zsdt?i%x4b_c*m+)=F%VUEpyp8q(aArbVk^oVQJvS&#A%d>&k6qfq`+z8zSEQjpi z(NvF2?9SI#@`rf|S4er(ar!p+9t)XKMP$f6eLW>;H`$4q7IlzjOm&yuzh^|>-3dn~ zPG*8kSV%y?J3qhKnp9qCgH~-XPGm(v&y8jq(AC_p;F~w4JIgjRAg-&d9HIHO{r>uK zbl1f+!nCV>pLRh6a@b27QRx=tLf|a`HPwOmL(=UfOjd{ch9fi3^<3ni(rC$S%crSF zv0CiVrg$iT0WWHBu)#eUXudXIF9Q)Vni2H^!!6uG$m3A!{zkAcrk~jUN$4whe)lw; zsWEQ&%wtym`EtS@=M*$w2s%2a6n}=XtG2Ro1qy5@p^S~vGlLc4-^C>&E31EW=9ztm zo6qwAjr0KR;cIo?)k0i=lWVGfD3(~2EfXr3m9~Hji^H(0+L;Pk&wNn3J$-{RVS%n; zIw5jT_0e0o4u!e7t&g{aZT26?%+6QRYa!HrQBfc7iHXI$*GV>wrb+4aF&~vk~X3!DsX!0#%xS@QX%yAGmmzf{`XmFjEHACK^>}`B5P|K6sSLMta3v} zDrrh2~U(3f!-?7kAae;v!gre zi5S|519U<;wzkV+$)7Sza1FFtPJL4S_RDDyZPn9f@!t{^PD9HMeHUn{KZC7#ZA%e; zeg_@r0_4v|83?CZ+59>YHGuDxJgRz1aK#h#eSgwYp1NHdmOarvh}|D{z?{t7Rg+@aJeJ2J5)v5XvamFJVOx?Pn{48x11md-R_+lp_CAuQ z?jgx*>K?*Mftu&t|I4|Dul(;ZrQ2{sk2q|uc^CP-r#5Ep)0hg1+(X)(rGGozQ$cf4 zKw!6Xnw^3=e;~~ryyt83Bw;6(|EQhY6$iozw*jh@d8Dz<)P=N11s-0-+m-s{^x?#~sJygVp$U+p8OJ)}eSo|l^RXzJY$ z*AIH%czsg9ftKr%@uA}Pyn7B`71Mqi3rMzWXh`M?9_U0A@}Y8S*WxbeAYBqK5}vCvoBiKPX*-PFK<6` zrYZL3*m)79OfTK<+TjbKVJY7|=`J{|EvPHENKBbW?4V@c+*(&LK2Ym8I@vK@Sm@w0 zLziah<6X3a`lxzHw55(5Y=*80SCOH_=KKD4MO@P3BB_{)>3>3FPVz0t$oHiH?NJJ* z$Fl2kDP@t_-Kp?*%1XUZq1|>}mTxfYoq{VoJiZ#`=99v8k;Sir@_5!4&OH&CsrzLW z8g`AK^ZOdAUrgcbu)(S2UV;zfIsO<5V+T*ce7~ zYQDuT-vS9Wg$Wl5{+{WBr#I*4=`i|}QWR6Yc*s1hR_8L5-KB!l|Jl9Q_Sy^1RK;KW z8q`=ff6it!E7nDB+f7BFa*rnSe*zIdU2jZ}VF%Xzxg9v`TD%hqT)B3ye~ zrr2_@T{R@}MASXMz-e2D&^ve5_BCwe*pArk+*BZ=TQg`@(~j6`_YHk_7^iybQu-pY zi@!ZTxaGWdTsG_|_1w2{PjY85;-r)mDY<&dU%`AEf%o7JfM$4Cu*rkfra@@$ymXo0 za9hkRFI(B#ZhaSSqoQ8o4Gg(PdGm(mk09NVe)d}zEh;J4!Jdq~4P0IIO-WAXd{M8H zS+>6Qwax4X{M~q6HW0^b-B;AE1CkDNOKGRCuOJZEweaDWiTwLmzZ4o7gl|y@oI_@q z{bUgryO#3dqe6`ztjEZ{gOEMG^n3Qvvu6Tio-wNmXU9G#r%0=)s2CWu1*P5iqD?jQ zhr@6_C`0^xo2&l0Tp6)JkCnx0i=G}a_D#zd6!4=5#|Uj8HtPhkV+z%+vCyCfOF!%P zOvU*2jI5;;^26 ztHoF1ZK+^(pA$TJ`qZ4eI9apix@E<_ypq!15gOS>wkaDWT-~HEb=_;`81X-o5rp%u z^I0iq;#yUIgm;vOg%VnWO?{Z=m#^F`P7_% z@0T&tAf|13RCh$^r{+8-+9GAG=TUthA>*xox7ao)rB)^@Rk%7CCK3}ia{X0i!U!2&zbBO=A5X@o`$--xuHFiP#fqcVWI#yN(#g?Bx z`+i?~^!Y0ZEI&rZrf^3o^KK1F#c~MB7%F52wUnxD54*m{;Ia#MRz@*T2W(XAf^IL8 zlyXkSn?D}9)~5HC=~!^tPEO%8nzitmH#U2Sh#J_x$*f@NyF?-M3^e4vpWG=x{o>I4 zZk`N3nY2xN?C|m9Gf?%#Dt2^lPoc3izf;a@T}kmD7hQ}lGNNdOmv{MQ%}sJwtGjjU z)9~}hC(mumP*My0EKN$f!F!`9DRZ>%(-+)A)M~PetI7N)>D?=uwk%GB66OpR=B4T> z=_>Vn@eIYC;xR)rbUL_3NvRyuAa}y2>HZL(x6pbttGYu-JpYVwT+9)Z8{LjK&If{C z@Jvam`Rt(};X_M}Zr+Z$+@esoc$da|`+P*OsHmwajUz&_Gd`iJA})idR?fuJg}FpJ z9F5?D&%?BBUv*&v{mtxxL`b=pH4A2%OIEE&=#f~J-5Zf>T{Sn}ZgKeNQS+Z4jIhhe z$av#B(Gj&v!~U_I(lPIdk7GNY<%_@`Ovm@9i0AD%3o0N-Cx><>o(i zSJc7boBIn&VtTAz+pDMYqoVFkXeX4g*eJZEVvJ`O9tQmxRlHHW*8gvR`oCZRn!GzrcTWp3kED zjT5V&TRIYdi1@x2vdV9)N#cg?WeIop{_vf~M5Fsl~#Vs68)d~>>* zD?0b?T_slcUs*nvA+vSSxUnTEnms}oHk4OzblD2Wx3}0eh9DQ;-`9oN_3lI z&oTSBt~W|3RL{P6L2~ueRXVmib89UW(eiH}ZWwwF1tkh-hkn^Dw-1Q5=0)MLmUBgW zBOvE+ZjO&tWik4kJny@x?L{qUj0uAWoA3M3Uas!#)t*;6J;p9R6MMJtj?iB^1|E<~ zzb#~vnR5+lCcU~DOTu=(?M?Oa8mkq4*_qCq3?q034x6qo2jjF6%4X0KA z{&p#7Epyigt}{3NY3IW2d{~3?r&sYfd3PmmFsYMPaZF9f-} zJ?k#`rK$~UZbI0>_U_ru$Im~`e-M}LagVP$CBAulH{oxGOVTKq39r_Me?z7&?$mJo z`-STwgdlH-R)`IurpD{|jLO@C_ob$8B_ke?C6=fAc0ETkUp6 z@iYxR+uQL_WmQe@&vobFoq{Jk@0*VDHJ-NK6?{K+nowkmu;pwWIKx`(I^S2^X8t5* zh-Q$VtUpS3ms!__=eG4gzHL1-(%ZH}@a_MrkhX%e2?4Ez)H|l0gykkH!1%aU6L6k@aY+!rf^j!yLcs z>G~JLk+ejV_^d7RMe-awId!>Yueu2P0LQ(D`dinW?-Nc`a`fVThLD1e&y8w?@9F*S zW1-|39*GR9bu0BNk59~#4 z51F9C+8BxP<)oV)A7?>a?9L}~af8Td#(s;s(>mm_56M4IkrG0>SB)?^%E9>W+`TtLy6tv9S}-T_emA@b9f0NegZL*8u^H6vPWQ3wgZ9si}(!3!z>faYL=zrWaZTZmo{kvW9qnI(kDS_Hl=YDQrMl`A0cbWC5# z$e2b9v50U)*8O_ge_aTrB@g?biJU^{s$rPh++pP*X<~@+{ zYYU^4n9c%cJ~W9;%*=%3ZG@?}yoJ!-WqWfO?-9!o5FE^6=j-Dm3vklg8@^)`Ty9B8 z$!phkJ4Ybp-JL>8sz#LrftIOA2F93lyaN)0j_&gS&Fr|gaL#$>m^i?m_;`Y|9DYRD zhM>epdMc(^VBogRHe#T^k5KfG>)!Ug}>=X&~cm@Lbkv%{dP6Uh9H3u?0tUlG*C*=L#=0`WIIgh}u>#z^X5b`98lj=eU#{8z5vx`J@RnAqn<1#s3aBoNl%ZaNLuN9QonNbLfw zzVI3#9c?~x8~P|ODzP-XhYuBc%o7TVi&=u%SXl|PRtZ=7=uv`8@Opr{|fS@qC&w$kAsTKKOqocAB&?iOl#0-n*VFI^Iq)I^4pdF^^!urW+h=Y#3q_ZeMYsr@VPMd~glrTq*m=MY zuOWggx5gfKH4F>_uzJIEha74|&HfT3aG)bY*feOclfuUtWMXV$LPC%8(IVw18QT^6 zjtpK{5_*LGR?FN;AG6ay*N%ZNa3A$JjHh9owxhj0hE0hSsxbMV3r>3LP61;AF^){6 zrkqY=)95M{qOUO8lY}0(EiCyE=}aC!uaUx49eAEn{@4=@c^>IAYt0s67dIc>#F}yE zP8V)lD8{gs;o6A$6Uo%!k^>Y+E-Gw%!}*ve@cQ)!or-<6a-=x=BKjYTd-;&|jtB|` zmj#?}MBgc&qCnznq=pru+l{YY2*wlK{pE`r1)`Qki3uTKNrb5@N<0c%NctrBlaNMt z=gyr+kFMD0hEa%CtYRK_QZVGW@-7iJm9OdDUw3wD$Pr%k2)^nmco?Jan?5i;eu9GV z>Kh!4%bI3(KS0A5P@#iq<_aOu8AW7du*>XprvR&oEK%c-|Iyo($3xw(?P(7w52ZyC zl}h1hrJgKNM+(W7UD>h|1|cI6vYk@d8$|ZeJeC++yO>CJl_W-tN?D3Q&h?wl`#$G< z&VTRceBSwk4`#l;zwdhA*L~mD#rZMMCc7tKI?6YJN`*eA4qHq)`_BC;)xt01ZLh2= zac4j@3jDHo!E?bD%bAe!(2gt8-d5?>`v@2_h{F87eRvAyFF zhjJX0(vDP)MI$p3B4MaAD!MRcyCFu6AAxa0l*~fID@QkLbZks%`V#_8puv;WN-hHx zJ&|VEuK1~Z4oRsv$wg3Rih%=#4Qewn zhmbo;T`#b9ZC$S$L@RI>yo0v3!z?J@&HX-?pz}|jJgEc;9PTiwJ}`ldbkh59#(@EZ z33cG9uR!{5K#zM5G?j3kVzr*_ z%Ca{o2afh|yJ>7Un-FGo#?-=X*GQH#M{A+)%}PF#VLn-RpS$=8AJlvNSRvv0lhY`@d=NV?s#3H5g72oR$um6id+_juP&wobN zb!)_EuEmJxRL-f*vD;U2w0M1`mPgn)Js&z}JNU`^TYk9BAA3!E*5WAx9nhoa0h6DS zysL}czLr{b7Y#dRjCiKT+5J973;6!lb#mv$t;8_oo~0=X(aR4}@9T;wN&6jN{QQL( z&+2Za%7rv~exV7^+OLyc9@MVwPSKaDg@_Ndxph#}mNhudNYq^Pnx4EoZN$&Ixm*2C z^`4;HSFO%CS#G)~x^6(uUzh{8CGg9#0woXnfyL23J=V{D9OQa7q^3pxbY}j`j!EjB zc4~ty6iB<*!-!uh&Kr{4@N8BT0gpM)amu0%g)v7Y+8gNTmVm^qunU!noJKqm<2TWg zpSHHb6Z~Yx0fbovRMriL4A=_)^~Z}1i;f&-55EHG+Mr6+J*~rr0|WE9!`B@aZA=l4 zuyK{s-YKlFUn3Bnv};6+l)7WvO5ST_w?-}llsJ*b3i2g4~13S|I-v~Dszr*DB67@sh#ThKqx%1xRY>8NhEXgqU>vq zXPRDB72D4O6UlRw`s+`cnjzP&sjK~HzB?hefA?<@NqM zluk!nM)9#?jRl!})Wc&_&!hULrm%)*wIRe7B{BYPD&s1=uf0_-?5Tue4OQ4QpyC*)II3NE1{N(?1woeZ$tl~XtWu>a?aYs+e;Q6YQcp;)oq~b@GMP;T4y3y&^ zcp{!QO9b62sL_zjqXj(dTiazwcz5)!$qsJ+17$YHF^{I-WhY*(uXk7Mx%#-*b8pc8 zf*Ppe(gI#}yuJSNezT=Fx81>Q`;(lltu3vsb@XQ|8Jv*JOii_sSx1li)pYu$+C3`7 zO2gRr&9B}U(~*Udz4M1d3rB9B+^96io=&*rP}n?F9ikuWVob?70&>obr_tQsejQ!L zAC)2aYpxL%nKb3ja$xnnE~k+6$1gjNEaA5O7c6mjx^O&Q{N{0Ben^hC-feXH$&v<8 z5(D(dP3y2*?SxF+YRH?mwbilQMf=;k(Gp;1*upQ);mHHdbH+^OKsSma(B+|h@83W8 zI9C2vW@>o;UNj1GnaN~A=W377*E3MXN_>cG5ti9|uDO7lMx(+_ub{rZ{>!_n;ojb} z*1;?OM5D^CEp^^DH6@ns#c z_}}+XP*iPi-&MXVP50}Rf1>)%GB@d09o(BKls&UGq3NVhVNMRTM=bI5q5B^)!L2jC zV2yl@MT(ar;qEsH4ROS;fF&$EJRDy|UlW%Ti6pD;E$53;;#jZ;gF0@Y!C7o?7phX6 z*U7uR2gemE)5*6)h<7R%W>r-DMCoNYWWw_D96>~k%qTOFdQBMCATo}u-w1O>%>V1* zjx7ndZ_C*A!CD>ni&}qR^iGT-n~^{M1&N}Gtw&_yMqt6WW}gBT3u@=lA9R`V5(hAD z%jRygxq$&&m1E#*sMrK?)e43Uieil6VZ(IDn*fx8NKnxz3!XOsZHO?nW!8C8Y;0_H z3Oj;z3bY2sdmLJ`OCLj&p$16;uyo-+|E{D&fk6*OGez)}ow5J}JBDPf8RbviZ7bQ6 zVWJBLzI{nm?JNUVQi=(^F51(3|S5XNH4Xs80 z2p5fzji^9-85tQ6Qvr?y)^S844c8WAwQ|?jo8}3#IlvTNwNA)`R^5d?14&m6$v0)^ zH$^-JEMvj&gv)(JLo!LhuN2I=K(H_f5D8M=2(}gztQRDE5+DX=+#Yrw`7CH6K{jmL zkl5x$NS4Z)mI;9J$kmmaoNQ`tt`%Dc`&e;qW()uamyqXrN{X|c9Yko-WfVBEni?AL zAAqEQYkXrc3in`%!??d?=AMPK0J|~~nIgzJ1MBf>F)=07rj$bB4}u=T)GfmB$&2=SO9Jk6Su}<0=+so4xYbwfwJ4okdUKrKUd@u zx1{oqn?HV6GqmVVn*(UPtrffnR~VWY$z{B}I`>O5Q&UsnkmtVb@9P8aeV5loxS#6k zJS8(jS=58_^3W59yQoN=W5rqM42vlRm|9qzl%`Cyo%UCxN````%EZ{%1=32N?}GJ8 zkqRv~U)C{vhbT#+hKi~x$dt(rEWjQEjVULn8=~O0Vx0pkK+y}`5F{#WFl69FEv~o| z^qt-0gwhy5a(Frbi1KhDuo0*He_~y~Ssu$|q2KHG*e;{KykA`G|1rfI#A?}^(tPHq z9T8Xx1_ZUL{JcEih5&xBTl^I%@VM#&5tZYE`R`do2z-(}mlzj+tLisNhU@ol_(p8v zz*!d;;7{Y&vA2~d1i247f}rlY#c&9-z&a0y=(S6IL&KI}@XBe+0;V$wSZ-(|3A77- z6U14vb8;>~4hDxUjqD3|6V6~XqS_T+(l<50xsT;^zzfQ>E0*E~7uVKEh z6Ef7={$~f6&e62Z%tQqQ?!hMkkq=PEuYd_oyI@eJq3H}|5>(+6P8nW~f?vV#4(4(o zHd#|H^Aa@fn%-iv#uI`=Lg-zeup6LZnUt6)m1O(588lZoy1gn)vl-{#<-Sgs-@uN3NlnB(7FMT&b~7Jv({Z#j{@ zueBu^Xil94pQ8110hMFU&eHneX}?S0azJET&4E2;3irED;h}RJex&|da3%_c6O_?@ zbzfh`C=32Qs8g1TgUtgV2i~u*=$=(w9RNum@MvH`gBi6PGk5IR(von_JwnL<@I-H4 zUkechehjbPwlP^-!f` zcUe8#O7#a@42pj|p@s$c3Q0e*t?^iiM*Zy-0mpydv+?$eDy0_ zZqP44+8}h6_07OL<&}et}mq_`*e!~qA8X5`_s558Y zyVlJ9jlXYf3T%;v)&5ftE-k=_ft{~#*}IDlT>C`S?$xs*ZU~O-G~d(So}T15JhkBl zHZNytD(5XExInGC+ziqW-IIX7TeFV-aYJ8jt6(_5-Axh_Aif;GxuBsftt;b1>*YDX z!xJgTytflBK;Xd@V%@`bB83$^O07;Kz{VXE%Q~Q2+E`zo3sINI$X0I|V!u4lW;o8O zMXdvCCmolHmm11WqcN*Xwh!TUiwF+~n1DzCHY!JptA?}YxOc%E5ce7gwquW#{?YvK z;U(Xl8Y2&S-Lebk2YSIq7~-M|f#!K*CX{m1M%%of43e&H3km|gIlmnilCB9;-jc{y zyZ3WD_pI5`)%M zElAryk~6_4BP66D*0HJF^j%Yvwz;{CaD?=WhGF(_Thv}K1{=_ zBX5AkVhM&nNq6lzn5NCeA4r~mYPrlVntxdJVpTbPR|FFWzrC(*tf|b%Th+jUPNz~_ zstGyvVlwxQHX}gB{u}SYAm6m67th1o}QF8P>_Gtck+-$T|ke?3$HXmEt-Sp1* z+C2l`4$3Lo9hMaKShfshw~Yn{>}Q6ENEsN6YH0Ke^2Ev}OO_B|#bDaO3pz_8p1PCC z1*>>%cthsJbTF%L+^;=LH(Y$^wq9EHDqhI{*~u&+_XhLx>&(|95lf((&g3 zh}|}gmj5Ky_S-|;Hd0U6cX2ApeZ5^a{QCKT^p~erkGS3#Y153@yzLh#YZ?|3|CywI MRO<*$pF diff --git a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx index b34b566b7411e..548c9c5695ef1 100644 --- a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx +++ b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx @@ -1,11 +1,21 @@ import './ViewLinkModal.scss' import { IconTrash } from '@posthog/icons' -import { LemonButton, LemonDivider, LemonInput, LemonModal, LemonSelect, LemonTag } from '@posthog/lemon-ui' +import { + LemonButton, + LemonDivider, + LemonDropdown, + LemonInput, + LemonModal, + LemonSelect, + LemonTag, +} from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Field, Form } from 'kea-forms' import { CodeSnippet, Language } from 'lib/components/CodeSnippet' +import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' import { IconSwapHoriz } from 'lib/lemon-ui/icons' +import { useState } from 'react' import { viewLinkLogic } from 'scenes/data-warehouse/viewLinkLogic' import { DatabaseSchemaQueryResponseField } from '~/queries/schema' @@ -44,8 +54,19 @@ export function ViewLinkForm(): JSX.Element { error, fieldName, isNewJoin, + selectedSourceKey, + selectedJoiningKey, + sourceIsUsingHogQLExpression, + joiningIsUsingHogQLExpression, } = useValues(viewLinkLogic) - const { selectJoiningTable, toggleJoinTableModal, selectSourceTable, setFieldName } = useActions(viewLinkLogic) + const { + selectJoiningTable, + toggleJoinTableModal, + selectSourceTable, + setFieldName, + selectSourceKey, + selectJoiningKey, + } = useActions(viewLinkLogic) return (
@@ -82,12 +103,22 @@ export function ViewLinkForm(): JSX.Element {
Source Table Key - + <> + HogQL Expression }]} + placeholder="Select a key" + /> + {sourceIsUsingHogQLExpression && ( + + )} +
@@ -96,12 +127,22 @@ export function ViewLinkForm(): JSX.Element {
Joining Table Key - + <> + HogQL Expression }]} + placeholder="Select a key" + /> + {joiningIsUsingHogQLExpression && ( + + )} +
@@ -151,6 +192,47 @@ export function ViewLinkForm(): JSX.Element { ) } +const HogQLDropdown = ({ + hogQLValue, + onHogQLValueChange, +}: { + hogQLValue: string + onHogQLValueChange: (hogQLValue: string) => void +}): JSX.Element => { + const [isHogQLDropdownVisible, setIsHogQLDropdownVisible] = useState(false) + + return ( +
+ setIsHogQLDropdownVisible(false)} + overlay={ + // eslint-disable-next-line react/forbid-dom-props +
+ { + onHogQLValueChange(currentValue) + setIsHogQLDropdownVisible(false) + }} + /> +
+ } + > + setIsHogQLDropdownVisible(!isHogQLDropdownVisible)} + > + {hogQLValue} + +
+
+ ) +} + interface ViewLinkDeleteButtonProps { table: string column: string diff --git a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx index ac23c43e7ff53..34e63deaf130d 100644 --- a/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx +++ b/frontend/src/scenes/data-warehouse/viewLinkLogic.tsx @@ -15,9 +15,7 @@ import { ViewLinkKeyLabel } from './ViewLinkModal' const NEW_VIEW_LINK: DataWarehouseViewLink = { id: 'new', source_table_name: undefined, - source_table_key: undefined, joining_table_name: undefined, - joining_table_key: undefined, field_name: undefined, } @@ -37,9 +35,11 @@ export const viewLinkLogic = kea([ ], actions: [databaseTableListLogic, ['loadDatabase'], dataWarehouseJoinsLogic, ['loadJoins']], }), - actions({ + actions(({ values }) => ({ selectJoiningTable: (selectedTableName: string) => ({ selectedTableName }), selectSourceTable: (selectedTableName: string) => ({ selectedTableName }), + selectSourceKey: (selectedKey: string) => ({ selectedKey, sourceTable: values.selectedSourceTable }), + selectJoiningKey: (selectedKey: string) => ({ selectedKey, joiningTable: values.selectedJoiningTable }), toggleJoinTableModal: true, toggleEditJoinModal: (join: DataWarehouseViewLink) => ({ join }), toggleNewJoinModal: true, @@ -48,7 +48,7 @@ export const viewLinkLogic = kea([ setError: (error: string) => ({ error }), setFieldName: (fieldName: string) => ({ fieldName }), clearModalFields: true, - }), + })), reducers({ joinToEdit: [ null as DataWarehouseViewLink | null, @@ -84,6 +84,20 @@ export const viewLinkLogic = kea([ clearModalFields: () => null, }, ], + selectedSourceKey: [ + null as string | null, + { + selectSourceKey: (_, { selectedKey }) => selectedKey, + toggleEditJoinModal: (_, { join }) => join.source_table_key ?? null, + }, + ], + selectedJoiningKey: [ + null as string | null, + { + selectJoiningKey: (_, { selectedKey }) => selectedKey, + toggleEditJoinModal: (_, { join }) => join.joining_table_key ?? null, + }, + ], fieldName: [ '' as string, { @@ -112,44 +126,21 @@ export const viewLinkLogic = kea([ forms(({ actions, values }) => ({ viewLink: { defaults: NEW_VIEW_LINK, - errors: ({ source_table_name, joining_table_name, joining_table_key, source_table_key }) => { - let joining_table_key_err: string | undefined = undefined - let source_table_key_err: string | undefined = undefined - - if (!joining_table_key) { - joining_table_key_err = 'Must select a join key' - } - - if (!source_table_key) { - source_table_key_err = 'Must select a join key' - } - - if ( - joining_table_key && - source_table_key && - values.selectedJoiningTable?.columns?.find((n) => n.key == joining_table_key)?.type !== - values.selectedSourceTable?.columns?.find((n) => n.key == source_table_key)?.type - ) { - joining_table_key_err = 'Join key types must match' - source_table_key_err = 'Join key types must match' - } - + errors: ({ source_table_name, joining_table_name }) => { return { source_table_name: values.isNewJoin && !source_table_name ? 'Must select a table' : undefined, joining_table_name: !joining_table_name ? 'Must select a table' : undefined, - source_table_key: source_table_key_err, - joining_table_key: joining_table_key_err, } }, - submit: async ({ joining_table_name, source_table_name, source_table_key, joining_table_key }) => { + submit: async ({ joining_table_name, source_table_name }) => { if (values.joinToEdit?.id && values.selectedSourceTable) { // Edit join try { await api.dataWarehouseViewLinks.update(values.joinToEdit.id, { source_table_name: source_table_name ?? values.selectedSourceTable.name, - source_table_key, + source_table_key: values.selectedSourceKey ?? undefined, joining_table_name, - joining_table_key, + joining_table_key: values.selectedJoiningKey ?? undefined, field_name: values.fieldName, }) @@ -164,9 +155,9 @@ export const viewLinkLogic = kea([ try { await api.dataWarehouseViewLinks.create({ source_table_name: source_table_name ?? values.selectedSourceTable.name, - source_table_key, + source_table_key: values.selectedSourceKey ?? undefined, joining_table_name, - joining_table_key, + joining_table_key: values.selectedJoiningKey ?? undefined, field_name: values.fieldName, }) @@ -222,6 +213,26 @@ export const viewLinkLogic = kea([ (s) => [s.selectedJoiningTableName, s.tables], (selectedJoiningTableName, tables) => tables.find((row) => row.name === selectedJoiningTableName), ], + sourceIsUsingHogQLExpression: [ + (s) => [s.selectedSourceKey, s.selectedSourceTable], + (sourceKey, sourceTable) => { + if (sourceKey === null) { + return false + } + const column = sourceTable?.columns.find((n) => n.key == sourceKey) + return !column + }, + ], + joiningIsUsingHogQLExpression: [ + (s) => [s.selectedJoiningKey, s.selectedJoiningTable], + (joiningKey, joiningTable) => { + if (joiningKey === null) { + return false + } + const column = joiningTable?.columns.find((n) => n.key == joiningKey) + return !column + }, + ], tableOptions: [ (s) => [s.tables], (tables) => diff --git a/mypy-baseline.txt b/mypy-baseline.txt index d4beb524d4186..1c1de99e0bdaf 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -4,6 +4,7 @@ posthog/temporal/common/utils.py:0: error: Argument 2 to "__get__" of "classmeth posthog/hogql/database/argmax.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/argmax.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/argmax.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/argmax.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/numbers.py:0: error: Incompatible types in assignment (expression has type "dict[str, IntegerDatabaseField]", variable has type "dict[str, FieldOrTable]") [assignment] posthog/hogql/database/schema/numbers.py:0: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/numbers.py:0: note: Consider using "Mapping" instead, which is covariant in the value type @@ -55,9 +56,11 @@ posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incomp posthog/hogql/database/schema/log_entries.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/log_entries.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/log_entries.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/schema/log_entries.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/log_entries.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/log_entries.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/log_entries.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/schema/log_entries.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/groups.py:0: error: Incompatible types in assignment (expression has type "dict[str, DatabaseField]", variable has type "dict[str, FieldOrTable]") [assignment] posthog/hogql/database/schema/groups.py:0: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/groups.py:0: note: Consider using "Mapping" instead, which is covariant in the value type @@ -67,24 +70,25 @@ posthog/hogql/database/schema/groups.py:0: note: Consider using "Mapping" instea posthog/hogql/resolver_utils.py:0: error: Argument 1 to "lookup_field_by_name" has incompatible type "SelectQueryType | SelectUnionQueryType"; expected "SelectQueryType" [arg-type] posthog/hogql/database/schema/persons.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "settings" [union-attr] posthog/hogql/database/schema/persons.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] -posthog/hogql/database/schema/persons.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] -posthog/hogql/database/schema/persons.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance -posthog/hogql/database/schema/persons.py:0: note: Consider using "Sequence" instead, which is covariant posthog/hogql/parser.py:0: error: Key expression in dictionary comprehension has incompatible type "str"; expected type "Literal['expr', 'order_expr', 'select']" [misc] posthog/hogql/parser.py:0: error: Statement is unreachable [unreachable] posthog/hogql/parser.py:0: error: Item "None" of "list[Expr] | None" has no attribute "__iter__" (not iterable) [union-attr] posthog/hogql/parser.py:0: error: "None" has no attribute "text" [attr-defined] posthog/hogql/parser.py:0: error: "None" has no attribute "text" [attr-defined] posthog/hogql/parser.py:0: error: Statement is unreachable [unreachable] +posthog/hogql/database/schema/person_distinct_ids.py:0: error: Argument 1 to "select_from_person_distinct_ids_table" has incompatible type "dict[str, list[str]]"; expected "dict[str, list[str | int]]" [arg-type] posthog/hogql/database/schema/cohort_people.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/cohort_people.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/cohort_people.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/schema/cohort_people.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/session_replay_events.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/session_replay_events.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/session_replay_events.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/schema/session_replay_events.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/session_replay_events.py:0: error: Argument "chain" to "Field" has incompatible type "list[str]"; expected "list[str | int]" [arg-type] posthog/hogql/database/schema/session_replay_events.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/session_replay_events.py:0: note: Consider using "Sequence" instead, which is covariant +posthog/hogql/database/schema/session_replay_events.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | int]") [operator] posthog/hogql/database/schema/event_sessions.py:0: error: Statement is unreachable [unreachable] posthog/plugins/utils.py:0: error: Subclass of "str" and "bytes" cannot exist: would have incompatible method signatures [unreachable] posthog/plugins/utils.py:0: error: Statement is unreachable [unreachable] @@ -243,27 +247,16 @@ posthog/temporal/data_imports/external_data_job.py:0: error: Argument 2 to "Data posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible default for argument "context" (default has type "None", argument has type "HogQLContext") [assignment] posthog/hogql/transforms/lazy_tables.py:0: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True posthog/hogql/transforms/lazy_tables.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase +posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible types in assignment (expression has type "dict[Never, Never]", variable has type "list[ConstraintOverride]") [assignment] posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible default for argument "context" (default has type "None", argument has type "HogQLContext") [assignment] posthog/hogql/transforms/lazy_tables.py:0: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True posthog/hogql/transforms/lazy_tables.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase -posthog/hogql/transforms/lazy_tables.py:0: error: Argument 1 to "append" of "list" has incompatible type "list[FieldType]"; expected "list[FieldType | PropertyType]" [arg-type] -posthog/hogql/transforms/lazy_tables.py:0: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance -posthog/hogql/transforms/lazy_tables.py:0: note: Consider using "Sequence" instead, which is covariant -posthog/hogql/transforms/lazy_tables.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] -posthog/hogql/transforms/lazy_tables.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] -posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] -posthog/hogql/transforms/lazy_tables.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] -posthog/hogql/transforms/lazy_tables.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] -posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible types in assignment (expression has type "PropertyType | FieldType", variable has type "FieldType") [assignment] -posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] -posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] -posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] -posthog/hogql/transforms/lazy_tables.py:0: error: List item 0 has incompatible type "SelectQueryType | None"; expected "SelectQueryType" [list-item] -posthog/hogql/transforms/lazy_tables.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] -posthog/hogql/transforms/lazy_tables.py:0: error: List item 0 has incompatible type "SelectQueryType | None"; expected "SelectQueryType" [list-item] -posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible types in assignment (expression has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | None", target has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType") [assignment] -posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] +posthog/hogql/transforms/lazy_tables.py:0: error: Non-overlapping equality check (left operand type: "TableType", right operand type: "LazyTableType") [comparison-overlap] +posthog/hogql/transforms/lazy_tables.py:0: error: Non-overlapping equality check (left operand type: "TableType", right operand type: "LazyTableType") [comparison-overlap] +posthog/hogql/transforms/lazy_tables.py:0: error: Name "chain" already defined on line 0 [no-redef] +posthog/hogql/transforms/lazy_tables.py:0: error: Subclass of "TableType" and "LazyTableType" cannot exist: would have incompatible method signatures [unreachable] posthog/hogql/transforms/lazy_tables.py:0: error: Statement is unreachable [unreachable] +posthog/hogql/transforms/lazy_tables.py:0: error: Incompatible types in assignment (expression has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType", variable has type "SelectQueryAliasType | None") [assignment] posthog/hogql/transforms/in_cohort.py:0: error: Incompatible default for argument "context" (default has type "None", argument has type "HogQLContext") [assignment] posthog/hogql/transforms/in_cohort.py:0: note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True posthog/hogql/transforms/in_cohort.py:0: note: Use https://github.com/hauntsaninja/no_implicit_optional to automatically upgrade your codebase diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 34651d435fb9b..e52c9362b4398 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -375,7 +375,8 @@ concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'key'), ''), 'null'), '^"|"$', '')), '')) FROM events INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -390,7 +391,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), ifNull(equals(events__pdi__person.properties___email, 'tom@posthog.com'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 @@ -409,7 +410,8 @@ concat(ifNull(toString(events.event), ''), ' ', ifNull(toString(nullIf(nullIf(events.mat_key, ''), 'null')), '')) FROM events INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -424,7 +426,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), ifNull(equals(events__pdi__person.properties___email, 'tom@posthog.com'), 0), less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-10 12:14:05.000000', 6, 'UTC')), greater(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-01-09 12:14:00.000000', 6, 'UTC'))) ORDER BY events.event ASC LIMIT 101 diff --git a/posthog/hogql/database/argmax.py b/posthog/hogql/database/argmax.py index 0302ac14ddb26..c6e479db07951 100644 --- a/posthog/hogql/database/argmax.py +++ b/posthog/hogql/database/argmax.py @@ -3,7 +3,7 @@ def argmax_select( table_name: str, - select_fields: Dict[str, List[str]], + select_fields: Dict[str, List[str | int]], group_fields: List[str], argmax_field: str, deleted_field: Optional[str] = None, diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index 3ed88e35555f8..aec1800c71eb1 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -1,8 +1,8 @@ from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Literal, Optional, TypedDict from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pydantic import ConfigDict, BaseModel +from posthog.hogql import ast from posthog.hogql.context import HogQLContext - from posthog.hogql.database.models import ( FieldTraverser, StringDatabaseField, @@ -167,7 +167,7 @@ def create_hogql_database( elif modifiers.personsOnEventsMode == PersonsOnEventsMode.v2_enabled: database.events.fields["event_person_id"] = StringDatabaseField(name="person_id") database.events.fields["override"] = LazyJoin( - from_field="event_person_id", + from_field=["event_person_id"], join_table=PersonOverridesTable(), join_function=join_with_person_overrides_table, ) @@ -203,8 +203,19 @@ def create_hogql_database( source_table = database.get_table(join.source_table_name) joining_table = database.get_table(join.joining_table_name) - source_table.fields[join.joining_table_name] = LazyJoin( - from_field=join.joining_table_key, + field = parse_expr(join.source_table_key) + if not isinstance(field, ast.Field): + raise HogQLException("Data Warehouse Join HogQL expression should be a Field node") + from_field = field.chain + + field = parse_expr(join.joining_table_key) + if not isinstance(field, ast.Field): + raise HogQLException("Data Warehouse Join HogQL expression should be a Field node") + to_field = field.chain + + source_table.fields[join.field_name] = LazyJoin( + from_field=from_field, + to_field=to_field, join_table=joining_table, join_function=join.join_function, ) diff --git a/posthog/hogql/database/models.py b/posthog/hogql/database/models.py index 1aa7beeede0da..e95a26614bed8 100644 --- a/posthog/hogql/database/models.py +++ b/posthog/hogql/database/models.py @@ -114,7 +114,8 @@ class LazyJoin(FieldOrTable): join_function: Callable[[str, str, Dict[str, Any], "HogQLContext", "SelectQuery"], Any] join_table: Table | str - from_field: str + from_field: List[str | int] + to_field: Optional[List[str | int]] = None def resolve_table(self, context: "HogQLContext") -> Table: if isinstance(self.join_table, Table): @@ -133,7 +134,7 @@ class LazyTable(Table): model_config = ConfigDict(extra="forbid") - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers) -> Any: + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers) -> Any: raise NotImplementedException("LazyTable.lazy_select not overridden") diff --git a/posthog/hogql/database/schema/cohort_people.py b/posthog/hogql/database/schema/cohort_people.py index da11fc2fcafea..72080419b7355 100644 --- a/posthog/hogql/database/schema/cohort_people.py +++ b/posthog/hogql/database/schema/cohort_people.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, List +from typing import Dict, List from posthog.hogql.database.models import ( StringDatabaseField, @@ -16,14 +16,14 @@ "cohort_id": IntegerDatabaseField(name="cohort_id"), "team_id": IntegerDatabaseField(name="team_id"), "person": LazyJoin( - from_field="person_id", + from_field=["person_id"], join_table="persons", join_function=join_with_persons_table, ), } -def select_from_cohort_people_table(requested_fields: Dict[str, List[str]]): +def select_from_cohort_people_table(requested_fields: Dict[str, List[str | int]]): from posthog.hogql import ast table_name = "raw_cohort_people" @@ -34,12 +34,14 @@ def select_from_cohort_people_table(requested_fields: Dict[str, List[str]]): "cohort_id": ["cohort_id"], **requested_fields, } - fields: List[ast.Expr] = [ast.Field(chain=[table_name] + chain) for name, chain in requested_fields.items()] + fields: List[ast.Expr] = [ + ast.Alias(alias=name, expr=ast.Field(chain=[table_name] + chain)) for name, chain in requested_fields.items() + ] return ast.SelectQuery( select=fields, select_from=ast.JoinExpr(table=ast.Field(chain=[table_name])), - group_by=fields, + group_by=[ast.Field(chain=[name]) for name, chain in requested_fields.items()], having=ast.CompareOperation( op=ast.CompareOperationOp.Gt, left=ast.Call(name="sum", args=[ast.Field(chain=[table_name, "sign"])]), @@ -65,7 +67,7 @@ def to_printed_hogql(self): class CohortPeople(LazyTable): fields: Dict[str, FieldOrTable] = COHORT_PEOPLE_FIELDS - def lazy_select(self, requested_fields: Dict[str, Any], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_cohort_people_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/events.py b/posthog/hogql/database/schema/events.py index 5a4a7f132af80..825393654f127 100644 --- a/posthog/hogql/database/schema/events.py +++ b/posthog/hogql/database/schema/events.py @@ -70,7 +70,7 @@ class EventsTable(Table): "$window_id": StringDatabaseField(name="$window_id"), # Lazy table that adds a join to the persons table "pdi": LazyJoin( - from_field="distinct_id", + from_field=["distinct_id"], join_table=PersonDistinctIdsTable(), join_function=join_with_person_distinct_ids_table, ), @@ -86,36 +86,36 @@ class EventsTable(Table): "person_id": FieldTraverser(chain=["pdi", "person_id"]), "$group_0": StringDatabaseField(name="$group_0"), "group_0": LazyJoin( - from_field="$group_0", + from_field=["$group_0"], join_table=GroupsTable(), join_function=join_with_group_n_table(0), ), "$group_1": StringDatabaseField(name="$group_1"), "group_1": LazyJoin( - from_field="$group_1", + from_field=["$group_1"], join_table=GroupsTable(), join_function=join_with_group_n_table(1), ), "$group_2": StringDatabaseField(name="$group_2"), "group_2": LazyJoin( - from_field="$group_2", + from_field=["$group_2"], join_table=GroupsTable(), join_function=join_with_group_n_table(2), ), "$group_3": StringDatabaseField(name="$group_3"), "group_3": LazyJoin( - from_field="$group_3", + from_field=["$group_3"], join_table=GroupsTable(), join_function=join_with_group_n_table(3), ), "$group_4": StringDatabaseField(name="$group_4"), "group_4": LazyJoin( - from_field="$group_4", + from_field=["$group_4"], join_table=GroupsTable(), join_function=join_with_group_n_table(4), ), "session": LazyJoin( - from_field="$session_id", + from_field=["$session_id"], join_table=EventsSessionSubTable(), join_function=join_with_events_table_session_duration, ), diff --git a/posthog/hogql/database/schema/groups.py b/posthog/hogql/database/schema/groups.py index 39382b246349b..bb237d68e8070 100644 --- a/posthog/hogql/database/schema/groups.py +++ b/posthog/hogql/database/schema/groups.py @@ -25,7 +25,7 @@ } -def select_from_groups_table(requested_fields: Dict[str, List[str]]): +def select_from_groups_table(requested_fields: Dict[str, List[str | int]]): return argmax_select( table_name="raw_groups", select_fields=requested_fields, @@ -83,7 +83,7 @@ def to_printed_hogql(self): class GroupsTable(LazyTable): fields: Dict[str, FieldOrTable] = GROUPS_TABLE_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_groups_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/log_entries.py b/posthog/hogql/database/schema/log_entries.py index a7ac459aab4ab..c14e90e26da50 100644 --- a/posthog/hogql/database/schema/log_entries.py +++ b/posthog/hogql/database/schema/log_entries.py @@ -35,7 +35,7 @@ def to_printed_hogql(self): class ReplayConsoleLogsLogEntriesTable(LazyTable): fields: Dict[str, FieldOrTable] = LOG_ENTRIES_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): fields: List[ast.Expr] = [ast.Field(chain=["log_entries"] + chain) for name, chain in requested_fields.items()] return ast.SelectQuery( @@ -58,7 +58,7 @@ def to_printed_hogql(self): class BatchExportLogEntriesTable(LazyTable): fields: Dict[str, FieldOrTable] = LOG_ENTRIES_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): fields: List[ast.Expr] = [ast.Field(chain=["log_entries"] + chain) for name, chain in requested_fields.items()] return ast.SelectQuery( diff --git a/posthog/hogql/database/schema/person_distinct_ids.py b/posthog/hogql/database/schema/person_distinct_ids.py index 65c0fb22a6722..02144b35fc3d8 100644 --- a/posthog/hogql/database/schema/person_distinct_ids.py +++ b/posthog/hogql/database/schema/person_distinct_ids.py @@ -21,14 +21,14 @@ "distinct_id": StringDatabaseField(name="distinct_id"), "person_id": StringDatabaseField(name="person_id"), "person": LazyJoin( - from_field="person_id", + from_field=["person_id"], join_table="persons", join_function=join_with_persons_table, ), } -def select_from_person_distinct_ids_table(requested_fields: Dict[str, List[str]]): +def select_from_person_distinct_ids_table(requested_fields: Dict[str, List[str | int]]): # Always include "person_id", as it's the key we use to make further joins, and it'd be great if it's available if "person_id" not in requested_fields: requested_fields = {**requested_fields, "person_id": ["person_id"]} @@ -82,7 +82,7 @@ def to_printed_hogql(self): class PersonDistinctIdsTable(LazyTable): fields: Dict[str, FieldOrTable] = PERSON_DISTINCT_IDS_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_person_distinct_ids_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/person_overrides.py b/posthog/hogql/database/schema/person_overrides.py index 5be6dd1e7d5ae..a33a7439b4982 100644 --- a/posthog/hogql/database/schema/person_overrides.py +++ b/posthog/hogql/database/schema/person_overrides.py @@ -24,7 +24,7 @@ } -def select_from_person_overrides_table(requested_fields: Dict[str, List[str]]): +def select_from_person_overrides_table(requested_fields: Dict[str, List[str | int]]): return argmax_select( table_name="raw_person_overrides", select_fields=requested_fields, @@ -74,7 +74,7 @@ def to_printed_hogql(self): class PersonOverridesTable(Table): fields: Dict[str, FieldOrTable] = PERSON_OVERRIDES_FIELDS - def lazy_select(self, requested_fields: Dict[str, Any], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_person_overrides_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/persons.py b/posthog/hogql/database/schema/persons.py index f823f1ce3c9f4..a248da56b7307 100644 --- a/posthog/hogql/database/schema/persons.py +++ b/posthog/hogql/database/schema/persons.py @@ -26,14 +26,14 @@ "properties": StringJSONDatabaseField(name="properties"), "is_identified": BooleanDatabaseField(name="is_identified"), "pdi": LazyJoin( - from_field="id", + from_field=["id"], join_table=PersonsPDITable(), join_function=persons_pdi_join, ), } -def select_from_persons_table(requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): +def select_from_persons_table(requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): version = modifiers.personsArgMaxVersion if version == PersonsArgMaxVersion.auto: version = PersonsArgMaxVersion.v1 @@ -85,7 +85,7 @@ def select_from_persons_table(requested_fields: Dict[str, List[str]], modifiers: def join_with_persons_table( from_table: str, to_table: str, - requested_fields: Dict[str, List[str]], + requested_fields: Dict[str, List[str | int]], context: HogQLContext, node: SelectQuery, ): @@ -123,7 +123,7 @@ def to_printed_hogql(self): class PersonsTable(LazyTable): fields: Dict[str, FieldOrTable] = PERSONS_FIELDS - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_persons_table(requested_fields, modifiers) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/persons_pdi.py b/posthog/hogql/database/schema/persons_pdi.py index 9c7fcf9e03e43..9f476f407b4d2 100644 --- a/posthog/hogql/database/schema/persons_pdi.py +++ b/posthog/hogql/database/schema/persons_pdi.py @@ -15,7 +15,7 @@ # :NOTE: We already have person_distinct_ids.py, which most tables link to. This persons_pdi.py is a hack to # make "select persons.pdi.distinct_id from persons" work while avoiding circular imports. Don't use directly. -def persons_pdi_select(requested_fields: Dict[str, List[str]]): +def persons_pdi_select(requested_fields: Dict[str, List[str | int]]): # Always include "person_id", as it's the key we use to make further joins, and it'd be great if it's available if "person_id" not in requested_fields: requested_fields = {**requested_fields, "person_id": ["person_id"]} @@ -33,7 +33,7 @@ def persons_pdi_select(requested_fields: Dict[str, List[str]]): def persons_pdi_join( from_table: str, to_table: str, - requested_fields: Dict[str, List[str]], + requested_fields: Dict[str, List[str | int]], context: HogQLContext, node: SelectQuery, ): @@ -63,7 +63,7 @@ class PersonsPDITable(LazyTable): "person_id": StringDatabaseField(name="person_id"), } - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return persons_pdi_select(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/session_replay_events.py b/posthog/hogql/database/schema/session_replay_events.py index 7a2097c2b8b5f..c9d564c7d4588 100644 --- a/posthog/hogql/database/schema/session_replay_events.py +++ b/posthog/hogql/database/schema/session_replay_events.py @@ -37,7 +37,7 @@ "event_count": IntegerDatabaseField(name="event_count"), "message_count": IntegerDatabaseField(name="message_count"), "pdi": LazyJoin( - from_field="distinct_id", + from_field=["distinct_id"], join_table=PersonDistinctIdsTable(), join_function=join_with_person_distinct_ids_table, ), @@ -64,7 +64,7 @@ def to_printed_hogql(self): return "raw_session_replay_events" -def select_from_session_replay_events_table(requested_fields: Dict[str, List[str]]): +def select_from_session_replay_events_table(requested_fields: Dict[str, List[str | int]]): from posthog.hogql import ast table_name = "raw_session_replay_events" @@ -115,7 +115,7 @@ class SessionReplayEventsTable(LazyTable): "first_url": StringDatabaseField(name="first_url"), } - def lazy_select(self, requested_fields: Dict[str, List[str]], modifiers: HogQLQueryModifiers): + def lazy_select(self, requested_fields: Dict[str, List[str | int]], modifiers: HogQLQueryModifiers): return select_from_session_replay_events_table(requested_fields) def to_printed_clickhouse(self, context): diff --git a/posthog/hogql/database/schema/static_cohort_people.py b/posthog/hogql/database/schema/static_cohort_people.py index f209b1186e55e..97d90cbd6dcac 100644 --- a/posthog/hogql/database/schema/static_cohort_people.py +++ b/posthog/hogql/database/schema/static_cohort_people.py @@ -16,7 +16,7 @@ class StaticCohortPeople(Table): "cohort_id": IntegerDatabaseField(name="cohort_id"), "team_id": IntegerDatabaseField(name="team_id"), "person": LazyJoin( - from_field="person_id", + from_field=["person_id"], join_table="persons", join_function=join_with_persons_table, ), diff --git a/posthog/hogql/test/__snapshots__/test_query.ambr b/posthog/hogql/test/__snapshots__/test_query.ambr index 4a900f6fad538..6ee77080d738f 100644 --- a/posthog/hogql/test/__snapshots__/test_query.ambr +++ b/posthog/hogql/test/__snapshots__/test_query.ambr @@ -128,8 +128,12 @@ -- ClickHouse SELECT e.event AS event, s.session_id AS session_id - FROM events AS e LEFT JOIN session_replay_events AS s ON equals(s.session_id, nullIf(nullIf(e.`$session_id`, ''), 'null')) - WHERE and(equals(s.team_id, 420), equals(e.team_id, 420), isNotNull(nullIf(nullIf(e.`$session_id`, ''), 'null'))) + FROM events AS e LEFT JOIN ( + SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 420) + GROUP BY session_replay_events.session_id) AS s ON equals(s.session_id, nullIf(nullIf(e.`$session_id`, ''), 'null')) + WHERE and(equals(e.team_id, 420), isNotNull(nullIf(nullIf(e.`$session_id`, ''), 'null'))) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -155,8 +159,12 @@ -- ClickHouse SELECT e.event AS event, s.session_id AS session_id - FROM session_replay_events AS s LEFT JOIN events AS e ON equals(nullIf(nullIf(e.`$session_id`, ''), 'null'), s.session_id) - WHERE and(equals(e.team_id, 420), equals(s.team_id, 420), isNotNull(nullIf(nullIf(e.`$session_id`, ''), 'null'))) + FROM ( + SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 420) + GROUP BY session_replay_events.session_id) AS s LEFT JOIN events AS e ON equals(nullIf(nullIf(e.`$session_id`, ''), 'null'), s.session_id) + WHERE and(equals(e.team_id, 420), isNotNull(nullIf(nullIf(e.`$session_id`, ''), 'null'))) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -182,8 +190,12 @@ -- ClickHouse SELECT e.event AS event, s.session_id AS session_id - FROM events AS e LEFT JOIN session_replay_events AS s ON equals(s.session_id, replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', '')) - WHERE and(equals(s.team_id, 420), equals(e.team_id, 420), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', ''))) + FROM events AS e LEFT JOIN ( + SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 420) + GROUP BY session_replay_events.session_id) AS s ON equals(s.session_id, replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', '')) + WHERE and(equals(e.team_id, 420), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', ''))) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -209,8 +221,12 @@ -- ClickHouse SELECT e.event AS event, s.session_id AS session_id - FROM session_replay_events AS s LEFT JOIN events AS e ON equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', ''), s.session_id) - WHERE and(equals(e.team_id, 420), equals(s.team_id, 420), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', ''))) + FROM ( + SELECT session_replay_events.session_id AS session_id + FROM session_replay_events + WHERE equals(session_replay_events.team_id, 420) + GROUP BY session_replay_events.session_id) AS s LEFT JOIN events AS e ON equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', ''), s.session_id) + WHERE and(equals(e.team_id, 420), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', ''))) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -450,7 +466,7 @@ SELECT e.event AS event, toTimeZone(e.timestamp, %(hogql_val_1)s) AS timestamp, e__pdi.distinct_id AS distinct_id, e__pdi__person.properties___sneaky_mail AS sneaky_mail FROM events AS e INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -463,7 +479,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE equals(e.team_id, 420) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -481,7 +497,7 @@ SELECT events.event AS event, toTimeZone(events.timestamp, %(hogql_val_0)s) AS timestamp, events__pdi.distinct_id AS distinct_id, events__pdi__person.id AS id FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -491,7 +507,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -509,7 +525,7 @@ SELECT events.event AS event, toTimeZone(events.timestamp, %(hogql_val_1)s) AS timestamp, events__pdi.distinct_id AS distinct_id, events__pdi__person.properties___sneaky_mail AS sneaky_mail FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -522,7 +538,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -540,7 +556,7 @@ SELECT e.event AS event, toTimeZone(e.timestamp, %(hogql_val_1)s) AS timestamp, e__pdi__person.properties___sneaky_mail AS sneaky_mail FROM events AS e INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -553,7 +569,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE equals(e.team_id, 420) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -571,7 +587,7 @@ SELECT s__pdi__person.properties___sneaky_mail AS sneaky_mail, count() FROM events AS s INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS s__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -584,7 +600,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS s__pdi__person ON equals(s__pdi.person_id, s__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS s__pdi__person ON equals(s__pdi.s__pdi___person_id, s__pdi__person.id) WHERE equals(s.team_id, 420) GROUP BY s__pdi__person.properties___sneaky_mail LIMIT 10 @@ -629,7 +645,12 @@ -- ClickHouse SELECT pdi.distinct_id AS distinct_id, pdi__person.properties___sneaky_mail AS sneaky_mail - FROM person_distinct_id2 AS pdi INNER JOIN ( + FROM ( + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 420) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS pdi INNER JOIN ( SELECT person.id AS id, replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', '') AS properties___sneaky_mail FROM person WHERE and(equals(person.team_id, 420), ifNull(in(tuple(person.id, person.version), ( @@ -638,8 +659,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS pdi__person ON equals(pdi.person_id, pdi__person.id) - WHERE equals(pdi.team_id, 420) + SETTINGS optimize_aggregation_in_order=1) AS pdi__person ON equals(pdi.pdi___person_id, pdi__person.id) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -655,14 +675,18 @@ -- ClickHouse SELECT pdi.distinct_id AS distinct_id, toTimeZone(pdi__person.created_at, %(hogql_val_0)s) AS created_at - FROM person_distinct_id2 AS pdi INNER JOIN ( + FROM ( + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 420) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS pdi INNER JOIN ( SELECT argMax(person.created_at, person.version) AS created_at, person.id AS id FROM person WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS pdi__person ON equals(pdi.person_id, pdi__person.id) - WHERE equals(pdi.team_id, 420) + SETTINGS optimize_aggregation_in_order=1) AS pdi__person ON equals(pdi.pdi___person_id, pdi__person.id) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -677,9 +701,23 @@ ''' -- ClickHouse - SELECT e.event AS event, toTimeZone(e.timestamp, %(hogql_val_0)s) AS timestamp, pdi.distinct_id AS distinct_id, p.id AS id, replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(p.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', '') AS sneaky_mail - FROM events AS e LEFT JOIN person_distinct_id2 AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN person AS p ON equals(p.id, pdi.person_id) - WHERE and(equals(p.team_id, 420), equals(pdi.team_id, 420), equals(e.team_id, 420)) + SELECT e.event AS event, toTimeZone(e.timestamp, %(hogql_val_1)s) AS timestamp, pdi.distinct_id AS distinct_id, p.id AS id, p.properties___sneaky_mail AS sneaky_mail + FROM events AS e LEFT JOIN ( + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + FROM person_distinct_id2 + WHERE equals(person_distinct_id2.team_id, 420) + GROUP BY person_distinct_id2.distinct_id + HAVING ifNull(equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0), 0)) AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN ( + SELECT person.id AS id, replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', '') AS properties___sneaky_mail + FROM person + WHERE and(equals(person.team_id, 420), ifNull(in(tuple(person.id, person.version), ( + SELECT person.id AS id, max(person.version) AS version + FROM person + WHERE equals(person.team_id, 420) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) + SETTINGS optimize_aggregation_in_order=1) AS p ON equals(p.id, pdi.person_id) + WHERE equals(e.team_id, 420) LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 @@ -717,7 +755,7 @@ SELECT events.event AS event, toTimeZone(events.timestamp, %(hogql_val_1)s) AS timestamp, events__pdi__person.id AS id, events__pdi__person.properties___sneaky_mail AS sneaky_mail FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -730,7 +768,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=1 diff --git a/posthog/hogql/test/__snapshots__/test_resolver.ambr b/posthog/hogql/test/__snapshots__/test_resolver.ambr index 78eba9a1eb515..1b086d067a621 100644 --- a/posthog/hogql/test/__snapshots__/test_resolver.ambr +++ b/posthog/hogql/test/__snapshots__/test_resolver.ambr @@ -3266,14 +3266,19 @@ table_type: { field: "person" lazy_join: { - from_field: "person_id", + from_field: [ + "person_id" + ], join_function: , - join_table: "persons" + join_table: "persons", + to_field: None } table_type: { field: "pdi" lazy_join: { - from_field: "distinct_id", + from_field: [ + "distinct_id" + ], join_function: , join_table: { fields: { @@ -3282,7 +3287,8 @@ person_id: {}, team_id: {} } - } + }, + to_field: None } table_type: } @@ -3393,14 +3399,19 @@ table_type: { field: "person" lazy_join: { - from_field: "person_id", + from_field: [ + "person_id" + ], join_function: , - join_table: "persons" + join_table: "persons", + to_field: None } table_type: { field: "pdi" lazy_join: { - from_field: "distinct_id", + from_field: [ + "distinct_id" + ], join_function: , join_table: { fields: { @@ -3409,7 +3420,8 @@ person_id: {}, team_id: {} } - } + }, + to_field: None } table_type: } @@ -3516,7 +3528,9 @@ table_type: { field: "pdi" lazy_join: { - from_field: "distinct_id", + from_field: [ + "distinct_id" + ], join_function: , join_table: { fields: { @@ -3525,7 +3539,8 @@ person_id: {}, team_id: {} } - } + }, + to_field: None } table_type: } @@ -3634,7 +3649,9 @@ table_type: { field: "pdi" lazy_join: { - from_field: "distinct_id", + from_field: [ + "distinct_id" + ], join_function: , join_table: { fields: { @@ -3643,7 +3660,8 @@ person_id: {}, team_id: {} } - } + }, + to_field: None } table_type: } @@ -3723,9 +3741,12 @@ table_type: { field: "person" lazy_join: { - from_field: "person_id", + from_field: [ + "person_id" + ], join_function: , - join_table: "persons" + join_table: "persons", + to_field: None } table_type: } diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 4734fed012c91..bdbb322d54397 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -21,7 +21,7 @@ def resolve_lazy_tables( @dataclasses.dataclass class JoinToAdd: - fields_accessed: Dict[str, List[str]] + fields_accessed: Dict[str, List[str | int]] lazy_join: LazyJoin from_table: str to_table: str @@ -29,11 +29,43 @@ class JoinToAdd: @dataclasses.dataclass class TableToAdd: - fields_accessed: Dict[str, List[str]] + fields_accessed: Dict[str, List[str | int]] lazy_table: LazyTable +@dataclasses.dataclass +class ConstraintOverride: + alias: str + table_name: str + chain_to_replace: List[str | int] + + +class FieldChainReplacer(TraversingVisitor): + overrides: List[ConstraintOverride] = {} + + def __init__(self, overrides: List[ConstraintOverride]) -> None: + super().__init__() + self.overrides = overrides + + def visit_field(self, node: ast.Field): + for constraint in self.overrides: + if node.chain == constraint.chain_to_replace: + node.chain = [constraint.table_name, constraint.alias] + + +class LazyFinder(TraversingVisitor): + found_lazy: bool = False + + def visit_lazy_join_type(self, node: ast.LazyJoinType): + self.found_lazy = True + + def visit_lazy_table_type(self, node: ast.TableType): + self.found_lazy = True + + class LazyTableResolver(TraversingVisitor): + lazy_finder_counter = 0 + def __init__( self, dialect: Literal["hogql", "clickhouse"], @@ -43,15 +75,19 @@ def __init__( super().__init__() self.stack_of_fields: List[List[ast.FieldType | ast.PropertyType]] = [[]] if stack else [] self.context = context - self.dialect = dialect + self.dialect: Literal["hogql", "clickhouse"] = dialect def visit_property_type(self, node: ast.PropertyType): if node.joined_subquery is not None: # we have already visited this property return - if isinstance(node.field_type.table_type, ast.LazyJoinType) or isinstance( - node.field_type.table_type, ast.LazyTableType - ): + + if isinstance(node.field_type.table_type, ast.TableAliasType): + table_type: ast.TableOrSelectType | ast.TableAliasType = node.field_type.table_type.table_type + else: + table_type = node.field_type.table_type + + if isinstance(table_type, ast.LazyJoinType) or isinstance(table_type, ast.LazyTableType): if self.context and self.context.within_non_hogql_query: # If we're in a non-HogQL query, traverse deeper, just like we normally would have. self.visit(node.field_type) @@ -62,7 +98,12 @@ def visit_property_type(self, node: ast.PropertyType): self.stack_of_fields[-1].append(node) def visit_field_type(self, node: ast.FieldType): - if isinstance(node.table_type, ast.LazyJoinType) or isinstance(node.table_type, ast.LazyTableType): + if isinstance(node.table_type, ast.TableAliasType): + table_type: ast.TableOrSelectType | ast.TableAliasType = node.table_type.table_type + else: + table_type = node.table_type + + if isinstance(table_type, ast.LazyJoinType) or isinstance(table_type, ast.LazyTableType): # Each time we find a field, we place it in a list for processing in "visit_select_query" if len(self.stack_of_fields) == 0: raise HogQLException("Can't access a lazy field when not in a SelectQuery context") @@ -73,8 +114,11 @@ def visit_select_query(self, node: ast.SelectQuery): if not select_type: raise HogQLException("Select query must have a type") + assert node.type is not None + assert select_type is not None + # Collect each `ast.Field` with `ast.LazyJoinType` - field_collector: List[ast.FieldType] = [] + field_collector: List[ast.FieldType | ast.PropertyType] = [] self.stack_of_fields.append(field_collector) # Collect all visited fields on lazy tables into field_collector @@ -96,15 +140,23 @@ def visit_select_query(self, node: ast.SelectQuery): # Look for tables without requested fields to support cases like `select count() from table` join = node.select_from while join: - if isinstance(join.table.type, ast.LazyTableType): - fields = [] + if join.table is not None and isinstance(join.table.type, ast.LazyTableType): + fields: List[ast.FieldType | ast.PropertyType] = [] for field_or_property in field_collector: if isinstance(field_or_property, ast.FieldType): - if field_or_property.table_type == join.table.type: - fields.append(field_or_property) + if isinstance(field_or_property.table_type, ast.TableAliasType): + if field_or_property.table_type.table_type == join.table.type: + fields.append(field_or_property) + else: + if field_or_property.table_type == join.table.type: + fields.append(field_or_property) elif isinstance(field_or_property, ast.PropertyType): - if field_or_property.field_type.table_type == join.table.type: - fields.append(field_or_property) + if isinstance(field_or_property.field_type.table_type, ast.TableAliasType): + if field_or_property.field_type.table_type.table_type == join.table.type: + fields.append(field_or_property) + else: + if field_or_property.field_type.table_type == join.table.type: + fields.append(field_or_property) if len(fields) == 0: table_name = join.alias or get_long_table_name(select_type, join.table.type) tables_to_add[table_name] = TableToAdd(fields_accessed={}, lazy_table=join.table.type.table) @@ -123,8 +175,16 @@ def visit_select_query(self, node: ast.SelectQuery): # Traverse the lazy tables until we reach a real table, collecting them in a list. # Usually there's just one or two. - table_types: List[ast.LazyJoinType | ast.LazyTableType] = [] - while isinstance(table_type, ast.LazyJoinType) or isinstance(table_type, ast.LazyTableType): + table_types: List[ast.LazyJoinType | ast.LazyTableType | ast.TableAliasType] = [] + while ( + isinstance(table_type, ast.TableAliasType) + or isinstance(table_type, ast.LazyJoinType) + or isinstance(table_type, ast.LazyTableType) + ): + if isinstance(table_type, ast.TableAliasType): + table_types.append(table_type) + table_type = table_type.table_type + break if isinstance(table_type, ast.LazyJoinType): table_types.append(table_type) table_type = table_type.table_type @@ -146,11 +206,13 @@ def visit_select_query(self, node: ast.SelectQuery): ) new_join = joins_to_add[to_table] if table_type == field.table_type: - chain = [] + chain: List[str | int] = [] chain.append(field.name) if property is not None: chain.extend(property.chain) - property.joined_subquery_field_name = f"{field.name}___{'___'.join(property.chain)}" + property.joined_subquery_field_name = ( + f"{field.name}___{'___'.join(map(lambda x: str(x), property.chain))}" + ) new_join.fields_accessed[property.joined_subquery_field_name] = chain else: new_join.fields_accessed[field.name] = chain @@ -167,18 +229,83 @@ def visit_select_query(self, node: ast.SelectQuery): chain.append(field.name) if property is not None: chain.extend(property.chain) - property.joined_subquery_field_name = f"{field.name}___{'___'.join(property.chain)}" + property.joined_subquery_field_name = ( + f"{field.name}___{'___'.join(map(lambda x: str(x), property.chain))}" + ) new_table.fields_accessed[property.joined_subquery_field_name] = chain else: new_table.fields_accessed[field.name] = chain + elif isinstance(table_type, ast.TableAliasType): + if isinstance(table_type.table_type, ast.LazyJoinType): + from_table = get_long_table_name(select_type, table_type.table_type) + to_table = get_long_table_name(select_type, table_type) + if to_table not in joins_to_add: + joins_to_add[to_table] = JoinToAdd( + fields_accessed={}, # collect here all fields accessed on this table + lazy_join=table_type.table_type.lazy_join, + from_table=from_table, + to_table=to_table, + ) + new_join = joins_to_add[to_table] + if table_type == field.table_type: + chain: List[str | int] = [] + chain.append(field.name) + if property is not None: + chain.extend(property.chain) + property.joined_subquery_field_name = ( + f"{field.name}___{'___'.join(map(lambda x: str(x), property.chain))}" + ) + new_join.fields_accessed[property.joined_subquery_field_name] = chain + else: + new_join.fields_accessed[field.name] = chain + elif isinstance(table_type.table_type, ast.LazyTableType): + table_name = get_long_table_name(select_type, table_type) + if table_name not in tables_to_add: + tables_to_add[table_name] = TableToAdd( + fields_accessed={}, # collect here all fields accessed on this table + lazy_table=cast(ast.LazyTable, table_type.table_type.table), + ) + new_table = tables_to_add[table_name] + if table_type == field.table_type: + chain = [] + chain.append(field.name) + if property is not None: + chain.extend(property.chain) + property.joined_subquery_field_name = ( + f"{field.name}___{'___'.join(map(lambda x: str(x), property.chain))}" + ) + new_table.fields_accessed[property.joined_subquery_field_name] = chain + else: + new_table.fields_accessed[field.name] = chain # Make sure we also add fields we will use for the join's "ON" condition into the list of fields accessed. # Without this "pdi.person.id" won't work if you did not ALSO select "pdi.person_id" explicitly for the join. + join_constraint_overrides: Dict[str, List[ConstraintOverride]] = {} + + def create_override(table_name: str, field_chain: List[str | int]) -> None: + alias = f"{table_name}___{'___'.join(map(lambda x: str(x), field_chain))}" + + if table_name in tables_to_add: + tables_to_add[table_name].fields_accessed[alias] = field_chain + else: + joins_to_add[table_name].fields_accessed[alias] = field_chain + + join_constraint_overrides[table_name] = [ + *join_constraint_overrides.get(table_name, []), + ConstraintOverride( + alias=alias, + table_name=table_name, + chain_to_replace=[table_name, *field_chain], + ), + ] + for new_join in joins_to_add.values(): - if new_join.from_table in joins_to_add: - joins_to_add[new_join.from_table].fields_accessed[new_join.lazy_join.from_field] = [ - new_join.lazy_join.from_field - ] + if new_join.from_table in joins_to_add or new_join.from_table in tables_to_add: + create_override(new_join.from_table, new_join.lazy_join.from_field) + if new_join.lazy_join.to_field is not None and ( + new_join.to_table in joins_to_add or new_join.to_table in tables_to_add + ): + create_override(new_join.to_table, new_join.lazy_join.to_field) # For all the collected tables, create the subqueries, and add them to the table. for table_name, table_to_add in tables_to_add.items(): @@ -190,7 +317,13 @@ def visit_select_query(self, node: ast.SelectQuery): join_ptr = node.select_from while join_ptr: - if join_ptr.table.type == old_table_type: + if join_ptr.table is not None and ( + join_ptr.table.type == old_table_type + or ( + isinstance(old_table_type, ast.TableAliasType) + and join_ptr.table.type == old_table_type.table_type + ) + ): join_ptr.table = subquery join_ptr.type = select_type.tables[table_name] join_ptr.alias = table_name @@ -206,10 +339,19 @@ def visit_select_query(self, node: ast.SelectQuery): self.context, node, ) - join_to_add = cast(ast.JoinExpr, clone_expr(join_to_add, clear_locations=True)) + + overrides = [ + *join_constraint_overrides.get(join_scope.to_table, []), + *join_constraint_overrides.get(join_scope.from_table, []), + ] + if len(overrides) != 0: + FieldChainReplacer(overrides).visit(join_to_add) + + join_to_add = cast(ast.JoinExpr, clone_expr(join_to_add, clear_locations=True, clear_types=True)) join_to_add = cast(ast.JoinExpr, resolve_types(join_to_add, self.context, self.dialect, [node.type])) - select_type.tables[to_table] = join_to_add.type + if join_to_add.type is not None: + select_type.tables[to_table] = join_to_add.type join_ptr = node.select_from added = False @@ -252,3 +394,12 @@ def visit_select_query(self, node: ast.SelectQuery): field_or_property.joined_subquery = table_type self.stack_of_fields.pop() + + # When joining a lazy table to another lazy table, the joined table doesn't get resolved + # Doing another pass solves this for us + if self.lazy_finder_counter < 20: + lazy_finder = LazyFinder() + lazy_finder.visit(node) + if lazy_finder.found_lazy: + self.lazy_finder_counter = self.lazy_finder_counter + 1 + self.visit_select_query(node) diff --git a/posthog/hogql/transforms/test/__snapshots__/test_lazy_tables.ambr b/posthog/hogql/transforms/test/__snapshots__/test_lazy_tables.ambr index 5c75c0ab02cca..0f20b0df0bd44 100644 --- a/posthog/hogql/transforms/test/__snapshots__/test_lazy_tables.ambr +++ b/posthog/hogql/transforms/test/__snapshots__/test_lazy_tables.ambr @@ -1,4 +1,63 @@ # serializer version: 1 +# name: TestLazyJoins.test_lazy_join_on_lazy_table + ''' + + SELECT cohort_people__new_person.id AS id + FROM ( + SELECT cohortpeople.person_id AS person_id, cohortpeople.cohort_id AS cohort_id, cohortpeople.person_id AS cohort_people___person_id + FROM cohortpeople + WHERE equals(cohortpeople.team_id, 420) + GROUP BY person_id, cohort_id, cohort_people___person_id + HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0)) AS cohort_people LEFT JOIN ( + SELECT persons.id AS id, id AS cohort_people__new_person___id + FROM ( + SELECT person.id AS id + FROM person + WHERE equals(person.team_id, 420) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) + SETTINGS optimize_aggregation_in_order=1) AS persons) AS cohort_people__new_person ON equals(cohort_people.cohort_people___person_id, cohort_people__new_person.cohort_people__new_person___id) + LIMIT 10000 + ''' +# --- +# name: TestLazyJoins.test_lazy_join_on_lazy_table_with_person_properties + ''' + + SELECT persons__events.event AS event + FROM ( + SELECT argMax(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', ''), person.version) AS persons___properties___email, person.id AS id + FROM person + WHERE equals(person.team_id, 420) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) + SETTINGS optimize_aggregation_in_order=1) AS persons LEFT JOIN ( + SELECT events.event AS event, event AS persons__events___event + FROM events + WHERE equals(events.team_id, 420)) AS persons__events ON equals(persons.persons___properties___email, persons__events.persons__events___event) + LIMIT 10000 + ''' +# --- +# name: TestLazyJoins.test_lazy_join_on_lazy_table_with_properties + ''' + + SELECT cohort_people__new_person.id AS id + FROM ( + SELECT cohortpeople.person_id AS person_id, cohortpeople.cohort_id AS cohort_id, cohortpeople.person_id AS cohort_people___person_id + FROM cohortpeople + WHERE equals(cohortpeople.team_id, 420) + GROUP BY person_id, cohort_id, cohort_people___person_id + HAVING ifNull(greater(sum(cohortpeople.sign), 0), 0)) AS cohort_people LEFT JOIN ( + SELECT persons.id AS id, persons.properties___email AS cohort_people__new_person___properties___email + FROM ( + SELECT argMax(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, %(hogql_val_0)s), ''), 'null'), '^"|"$', ''), person.version) AS properties___email, person.id AS id + FROM person + WHERE equals(person.team_id, 420) + GROUP BY person.id + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) + SETTINGS optimize_aggregation_in_order=1) AS persons) AS cohort_people__new_person ON equals(cohort_people.cohort_people___person_id, cohort_people__new_person.cohort_people__new_person___properties___email) + LIMIT 10000 + ''' +# --- # name: TestLazyJoins.test_resolve_lazy_table_as_select_table ''' @@ -52,7 +111,7 @@ SELECT person_distinct_ids__person.`properties___$browser` AS `$browser` FROM ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_distinct_ids___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -62,7 +121,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS person_distinct_ids__person ON equals(person_distinct_ids.person_id, person_distinct_ids__person.id) + SETTINGS optimize_aggregation_in_order=1) AS person_distinct_ids__person ON equals(person_distinct_ids.person_distinct_ids___person_id, person_distinct_ids__person.id) LIMIT 10000 ''' # --- @@ -71,7 +130,7 @@ SELECT person_distinct_ids__person.`properties___$browser___in___json` AS `$browser__in__json` FROM ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_distinct_ids___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -81,7 +140,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS person_distinct_ids__person ON equals(person_distinct_ids.person_id, person_distinct_ids__person.id) + SETTINGS optimize_aggregation_in_order=1) AS person_distinct_ids__person ON equals(person_distinct_ids.person_distinct_ids___person_id, person_distinct_ids__person.id) LIMIT 10000 ''' # --- @@ -104,7 +163,7 @@ SELECT events.event AS event, events__pdi__person.id AS id FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -114,7 +173,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' @@ -124,7 +183,7 @@ SELECT events.event AS event, events__pdi__person.`properties___$browser` AS `$browser` FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -134,7 +193,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' @@ -144,7 +203,7 @@ SELECT events.event AS event, events__pdi__person.properties AS properties, events__pdi__person.properties___name AS name FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -154,7 +213,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' @@ -164,7 +223,7 @@ SELECT events.event AS event, events__pdi__person.id AS id FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -174,7 +233,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' diff --git a/posthog/hogql/transforms/test/__snapshots__/test_property_types.ambr b/posthog/hogql/transforms/test/__snapshots__/test_property_types.ambr index 794edd853024c..259ae9de0f210 100644 --- a/posthog/hogql/transforms/test/__snapshots__/test_property_types.ambr +++ b/posthog/hogql/transforms/test/__snapshots__/test_property_types.ambr @@ -17,7 +17,7 @@ SELECT multiply(toFloat64OrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, %(hogql_val_1)s), ''), 'null'), '^"|"$', '')), toFloat64OrNull(events__pdi__person.properties___tickets)) FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -27,7 +27,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' @@ -46,7 +46,7 @@ SELECT parseDateTime64BestEffortOrNull(events__pdi__person.properties___provided_timestamp, 6, %(hogql_val_1)s) AS provided_timestamp FROM events INNER JOIN ( - SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id + SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 420) GROUP BY person_distinct_id2.distinct_id @@ -56,7 +56,7 @@ WHERE equals(person.team_id, 420) GROUP BY person.id HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) - SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE equals(events.team_id, 420) LIMIT 10000 ''' diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 131fcb227fbbc..8e93e16df1470 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -8,6 +8,7 @@ from posthog.hogql.printer import print_ast from posthog.hogql.test.utils import pretty_print_in_tests from posthog.test.base import BaseTest +from posthog.warehouse.models.join import DataWarehouseJoin class TestLazyJoins(BaseTest): @@ -86,3 +87,45 @@ def _print_select(self, select: str): "clickhouse", ) return pretty_print_in_tests(query, self.team.pk) + + @pytest.mark.usefixtures("unittest_snapshot") + def test_lazy_join_on_lazy_table(self): + DataWarehouseJoin( + team=self.team, + source_table_name="cohort_people", + source_table_key="person_id", + joining_table_name="persons", + joining_table_key="id", + field_name="new_person", + ).save() + + printed = self._print_select("select new_person.id from cohort_people") + assert printed == self.snapshot + + @pytest.mark.usefixtures("unittest_snapshot") + def test_lazy_join_on_lazy_table_with_properties(self): + DataWarehouseJoin( + team=self.team, + source_table_name="cohort_people", + source_table_key="person_id", + joining_table_name="persons", + joining_table_key="properties.email", + field_name="new_person", + ).save() + + printed = self._print_select("select new_person.id from cohort_people") + assert printed == self.snapshot + + @pytest.mark.usefixtures("unittest_snapshot") + def test_lazy_join_on_lazy_table_with_person_properties(self): + DataWarehouseJoin( + team=self.team, + source_table_name="persons", + source_table_key="properties.email", + joining_table_name="events", + joining_table_key="event", + field_name="events", + ).save() + + printed = self._print_select("select events.event from persons") + assert printed == self.snapshot diff --git a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr index bec07968fb0f8..29a1483cafc1c 100644 --- a/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/hogql_queries/insights/funnels/test/__snapshots__/test_funnel.ambr @@ -458,6 +458,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -473,7 +474,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-07-01 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('$pageview', 'user signed up')), or(and(ifNull(ilike(e__pdi__person.properties___email, '%.com%'), 0), ifNull(equals(e__pdi__person.properties___age, '20'), 0)), or(ifNull(ilike(e__pdi__person.properties___email, '%.org%'), 0), ifNull(equals(e__pdi__person.properties___age, '28'), 0)))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) WHERE ifNull(equals(step_0, 1), 0))) GROUP BY aggregation_target, @@ -567,6 +568,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -582,7 +584,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-07-01 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('$pageview', 'user signed up')), or(and(ifNull(ilike(e__pdi__person.properties___email, '%.com%'), 0), ifNull(equals(e__pdi__person.properties___age, '20'), 0)), or(ifNull(ilike(e__pdi__person.properties___email, '%.org%'), 0), ifNull(equals(e__pdi__person.properties___age, '28'), 0)))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) WHERE ifNull(equals(step_0, 1), 0))) GROUP BY aggregation_target, @@ -680,6 +682,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -695,7 +698,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-07-01 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('$pageview', 'user signed up')), or(and(ifNull(ilike(e__pdi__person.properties___email, '%.com%'), 0), ifNull(equals(e__pdi__person.properties___age, '20'), 0)), or(ifNull(ilike(e__pdi__person.properties___email, '%.org%'), 0), ifNull(equals(e__pdi__person.properties___age, '28'), 0)))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) WHERE ifNull(equals(step_0, 1), 0))) GROUP BY aggregation_target, @@ -793,6 +796,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -808,7 +812,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-01-01 00:00:00.000000', 6, 'UTC')), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), toDateTime64('2020-07-01 23:59:59.999999', 6, 'UTC'))), in(e.event, tuple('$pageview', 'user signed up')), or(and(ifNull(ilike(e__pdi__person.properties___email, '%.com%'), 0), ifNull(equals(e__pdi__person.properties___age, '20'), 0)), or(ifNull(ilike(e__pdi__person.properties___email, '%.org%'), 0), ifNull(equals(e__pdi__person.properties___age, '28'), 0)))), or(ifNull(equals(step_0, 1), 0), ifNull(equals(step_1, 1), 0), ifNull(equals(step_2, 1), 0))))))) WHERE ifNull(equals(step_0, 1), 0))) GROUP BY aggregation_target, diff --git a/posthog/hogql_queries/insights/test/__snapshots__/test_lifecycle_query_runner.ambr b/posthog/hogql_queries/insights/test/__snapshots__/test_lifecycle_query_runner.ambr index d9d4a543c4c00..879021d448e4d 100644 --- a/posthog/hogql_queries/insights/test/__snapshots__/test_lifecycle_query_runner.ambr +++ b/posthog/hogql_queries/insights/test/__snapshots__/test_lifecycle_query_runner.ambr @@ -64,6 +64,7 @@ FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -75,7 +76,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 00:00:00', 6, 'UTC'))), toIntervalDay(1))), less(toTimeZone(events.timestamp, 'UTC'), plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-19 23:59:59', 6, 'UTC'))), toIntervalDay(1))), ifNull(in(person_id, (SELECT cohortpeople.person_id AS person_id FROM cohortpeople @@ -140,6 +141,7 @@ FROM events SAMPLE 0.1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -151,7 +153,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 00:00:00', 6, 'UTC'))), toIntervalDay(1))), less(toTimeZone(events.timestamp, 'UTC'), plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-19 23:59:59', 6, 'UTC'))), toIntervalDay(1))), equals(events.event, '$pageview')) GROUP BY person_id) GROUP BY start_of_period, @@ -211,6 +213,7 @@ FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -222,7 +225,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 00:00:00', 6, 'UTC'))), toIntervalDay(1))), less(toTimeZone(events.timestamp, 'UTC'), plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-19 23:59:59', 6, 'UTC'))), toIntervalDay(1))), equals(events.event, '$pageview')) GROUP BY person_id) GROUP BY start_of_period, @@ -282,6 +285,7 @@ FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -293,7 +297,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'US/Pacific'), minus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 00:00:00', 6, 'US/Pacific'))), toIntervalDay(1))), less(toTimeZone(events.timestamp, 'US/Pacific'), plus(toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-19 23:59:59', 6, 'US/Pacific'))), toIntervalDay(1))), equals(events.event, '$pageview')) GROUP BY person_id) GROUP BY start_of_period, diff --git a/posthog/hogql_queries/insights/test/__snapshots__/test_retention_query_runner.ambr b/posthog/hogql_queries/insights/test/__snapshots__/test_retention_query_runner.ambr index 3e815818de18b..4e2714d2e7212 100644 --- a/posthog/hogql_queries/insights/test/__snapshots__/test_retention_query_runner.ambr +++ b/posthog/hogql_queries/insights/test/__snapshots__/test_retention_query_runner.ambr @@ -539,6 +539,7 @@ FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -553,7 +554,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), and(equals(events.event, '$pageview'), ifNull(equals(events__pdi__person.properties___email, 'person1@test.com'), 0)), and(greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfDay(toDateTime64('2020-06-10 00:00:00.000000', 6, 'UTC'))), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-06-17 00:00:00.000000', 6, 'UTC'))))) AS target_event JOIN (SELECT toStartOfDay(toTimeZone(events.timestamp, 'UTC')) AS event_date, @@ -580,6 +581,7 @@ FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS events__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -594,7 +596,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__pdi__person ON equals(events__pdi.events__pdi___person_id, events__pdi__person.id) WHERE and(equals(events.team_id, 2), and(equals(events.event, '$pageview'), ifNull(equals(events__pdi__person.properties___email, 'person1@test.com'), 0)), and(greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfDay(toDateTime64('2020-06-10 00:00:00.000000', 6, 'UTC'))), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2020-06-17 00:00:00.000000', 6, 'UTC'))))) AS target_event) WHERE and(or(1, isNull(breakdown_values)), or(1, isNull(intervals_from_base)))) AS actor_activity GROUP BY breakdown_values, diff --git a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr index fabfda5c56bf3..2f1c4c1de0917 100644 --- a/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr +++ b/posthog/hogql_queries/insights/trends/test/__snapshots__/test_trends.ambr @@ -67,6 +67,7 @@ FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -81,7 +82,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-07 23:59:59', 6, 'UTC'))), ifNull(equals(e__pdi__person.`properties___$bool_prop`, 'x'), 0), and(equals(e.event, 'sign up'), ifNull(in(e__pdi.person_id, (SELECT cohortpeople.person_id AS person_id FROM cohortpeople @@ -622,6 +623,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -636,7 +638,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC')))), and(equals(e.event, '$pageview'), and(or(ifNull(equals(e__pdi__person.properties___name, 'p1'), 0), ifNull(equals(e__pdi__person.properties___name, 'p2'), 0), ifNull(equals(e__pdi__person.properties___name, 'p3'), 0)), ifNull(in(e__pdi.person_id, (SELECT cohortpeople.person_id AS person_id FROM cohortpeople @@ -691,6 +693,7 @@ FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -705,7 +708,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(and(equals(e.event, '$pageview'), and(or(ifNull(equals(e__pdi__person.properties___name, 'p1'), 0), ifNull(equals(e__pdi__person.properties___name, 'p2'), 0), ifNull(equals(e__pdi__person.properties___name, 'p3'), 0)), ifNull(in(e__pdi.person_id, (SELECT cohortpeople.person_id AS person_id FROM cohortpeople @@ -1076,7 +1079,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1091,7 +1095,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-26 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-02 23:59:59', 6, 'UTC'))), equals(e.event, 'event_name'), ifNull(equals(e__pdi__person.properties___name, 'Jane'), 0)) GROUP BY day_start) GROUP BY day_start @@ -1292,6 +1296,7 @@ FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1307,7 +1312,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(equals(e.event, 'sign up'), ifNull(equals(e__pdi__person.properties___filter_prop, 'filter_val'), 0))) GROUP BY value ORDER BY count DESC, value DESC @@ -1357,6 +1362,7 @@ FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1372,7 +1378,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(equals(e.event, 'sign up'), ifNull(equals(e__pdi__person.properties___filter_prop, 'filter_val'), 0), or(ifNull(equals(transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'some_val2', 'some_val'], ['$$_posthog_breakdown_other_$$', 'some_val2', 'some_val'], '$$_posthog_breakdown_other_$$'), '$$_posthog_breakdown_other_$$'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val2'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), toIntervalDay(30))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id, breakdown_value) AS e @@ -1716,7 +1722,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1731,7 +1738,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'watched movie'), ifNull(equals(e__pdi__person.properties___name, 'person1'), 0)) GROUP BY day_start) GROUP BY day_start @@ -1759,7 +1766,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1774,7 +1782,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'watched movie'), ifNull(equals(e__pdi__person.properties___name, 'person1'), 0)) GROUP BY day_start) GROUP BY day_start @@ -1828,7 +1836,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1843,7 +1852,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'watched movie'), ifNull(equals(e__pdi__person.properties___name, 'person1'), 0)) GROUP BY day_start) GROUP BY day_start @@ -1897,7 +1906,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -1912,7 +1922,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'watched movie'), ifNull(equals(e__pdi__person.properties___name, 'person1'), 0)) GROUP BY day_start) GROUP BY day_start @@ -3056,7 +3066,8 @@ count(e.uuid) AS count FROM events AS e INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3073,7 +3084,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(or(ifNull(notILike(e__pdi__person.properties___email, '%@posthog.com%'), 1), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0)), or(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'safari'), 0))))) GROUP BY value ORDER BY count DESC, value DESC @@ -3111,7 +3122,8 @@ transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], ['$$_posthog_breakdown_other_$$', 'test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3128,7 +3140,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(or(ifNull(notILike(e__pdi__person.properties___email, '%@posthog.com%'), 1), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0)), or(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'safari'), 0))), or(ifNull(equals(transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], ['$$_posthog_breakdown_other_$$', 'test2@posthog.com', 'test@gmail.com', 'test5@posthog.com', 'test4@posthog.com', 'test3@posthog.com'], '$$_posthog_breakdown_other_$$'), '$$_posthog_breakdown_other_$$'), 0), ifNull(equals(e__pdi__person.properties___email, 'test2@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test@gmail.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test5@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test4@posthog.com'), 0), ifNull(equals(e__pdi__person.properties___email, 'test3@posthog.com'), 0))) GROUP BY day_start, breakdown_value) @@ -3150,7 +3162,8 @@ count(e.uuid) AS count FROM events AS e INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3167,7 +3180,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'chrome'), 0)), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0), ifNull(ilike(e__pdi__person.properties___email, '%@posthog.com%'), 0)))) GROUP BY value ORDER BY count DESC, value DESC @@ -3205,7 +3218,8 @@ transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'test2@posthog.com'], ['$$_posthog_breakdown_other_$$', 'test2@posthog.com'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3222,7 +3236,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-07-01 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), and(ifNull(equals(e__pdi__person.`properties___$os`, 'android'), 0), ifNull(equals(e__pdi__person.`properties___$browser`, 'chrome'), 0)), and(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(e.properties, 'key'), ''), 'null'), '^"|"$', ''), 'val'), 0), ifNull(ilike(e__pdi__person.properties___email, '%@posthog.com%'), 0)), or(ifNull(equals(transform(ifNull(e__pdi__person.properties___email, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'test2@posthog.com'], ['$$_posthog_breakdown_other_$$', 'test2@posthog.com'], '$$_posthog_breakdown_other_$$'), '$$_posthog_breakdown_other_$$'), 0), ifNull(equals(e__pdi__person.properties___email, 'test2@posthog.com'), 0))) GROUP BY day_start, breakdown_value) @@ -3279,7 +3293,8 @@ toStartOfDay(toTimeZone(e.timestamp, 'UTC')) AS day_start FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3294,7 +3309,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0)) GROUP BY day_start) GROUP BY day_start @@ -3313,7 +3328,8 @@ count(e.uuid) AS count FROM events AS e INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3328,7 +3344,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) GROUP BY value ORDER BY count DESC, value DESC @@ -3366,7 +3382,8 @@ transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'some_val'], ['$$_posthog_breakdown_other_$$', 'some_val'], '$$_posthog_breakdown_other_$$') AS breakdown_value FROM events AS e SAMPLE 1 INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -3381,7 +3398,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfDay(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-24 00:00:00', 6, 'UTC')))), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-31 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'some_val'], ['$$_posthog_breakdown_other_$$', 'some_val'], '$$_posthog_breakdown_other_$$'), '$$_posthog_breakdown_other_$$'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0))) GROUP BY day_start, breakdown_value) @@ -4188,7 +4205,8 @@ WHERE and(equals(events.team_id, 2), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -4203,7 +4221,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC')))), and(greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'))) GROUP BY value ORDER BY count DESC, value DESC @@ -4228,7 +4246,8 @@ WHERE and(equals(events.team_id, 2), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(events.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), ifNull(notEquals(id, ''), 1)) GROUP BY id) AS e__session ON equals(e.`$session_id`, e__session.id) INNER JOIN - (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -4243,7 +4262,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), greaterOrEquals(toTimeZone(e.timestamp, 'UTC'), toStartOfWeek(assumeNotNull(parseDateTime64BestEffortOrNull('2019-12-28 00:00:00', 6, 'UTC')), 0)), lessOrEquals(toTimeZone(e.timestamp, 'UTC'), assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-04 23:59:59', 6, 'UTC'))), equals(e.event, 'sign up'), or(ifNull(equals(transform(ifNull(e__pdi__person.`properties___$some_prop`, '$$_posthog_breakdown_null_$$'), ['$$_posthog_breakdown_other_$$', 'some_val', 'another_val'], ['$$_posthog_breakdown_other_$$', 'some_val', 'another_val'], '$$_posthog_breakdown_other_$$'), '$$_posthog_breakdown_other_$$'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'some_val'), 0), ifNull(equals(e__pdi__person.`properties___$some_prop`, 'another_val'), 0))) GROUP BY e__session.id, breakdown_value) @@ -4806,6 +4825,7 @@ FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -4820,7 +4840,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(e__pdi__person.properties___name, 'person-1'), 0), ifNull(equals(e__pdi__person.properties___name, 'person-2'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) @@ -4862,6 +4882,7 @@ FROM events AS e SAMPLE 1 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, + argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS e__pdi___person_id, person_distinct_id2.distinct_id AS distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, 2) @@ -4876,7 +4897,7 @@ FROM person WHERE equals(person.team_id, 2) GROUP BY person.id - HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.person_id, e__pdi__person.id) + HAVING ifNull(equals(argMax(person.is_deleted, person.version), 0), 0))), 0)) SETTINGS optimize_aggregation_in_order=1) AS e__pdi__person ON equals(e__pdi.e__pdi___person_id, e__pdi__person.id) WHERE and(equals(e.team_id, 2), and(equals(e.event, '$pageview'), or(ifNull(equals(e__pdi__person.properties___name, 'person-1'), 0), ifNull(equals(e__pdi__person.properties___name, 'person-2'), 0))), ifNull(greaterOrEquals(timestamp, minus(assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-01 00:00:00', 6, 'UTC')), toIntervalDay(7))), 0), ifNull(lessOrEquals(timestamp, assumeNotNull(parseDateTime64BestEffortOrNull('2020-01-12 23:59:59', 6, 'UTC'))), 0)) GROUP BY timestamp, actor_id) AS e WHERE and(ifNull(lessOrEquals(e.timestamp, plus(d.timestamp, toIntervalDay(1))), 0), ifNull(greater(e.timestamp, minus(d.timestamp, toIntervalDay(6))), 0)) diff --git a/posthog/warehouse/api/view_link.py b/posthog/warehouse/api/view_link.py index 27315e5a7c61d..38c35159939f4 100644 --- a/posthog/warehouse/api/view_link.py +++ b/posthog/warehouse/api/view_link.py @@ -65,15 +65,10 @@ def _validate_join_key(self, join_key: Optional[str], table: Optional[str], team database = create_hogql_database(team_id) try: - table_instance = database.get_table(table) + database.get_table(table) except Exception: raise serializers.ValidationError(f"Invalid table: {table}") - try: - table_instance.fields[join_key] - except Exception: - raise serializers.ValidationError(f"Invalid join key: {join_key}") - return diff --git a/posthog/warehouse/models/join.py b/posthog/warehouse/models/join.py index 966e7841a7d06..c06e4179d6939 100644 --- a/posthog/warehouse/models/join.py +++ b/posthog/warehouse/models/join.py @@ -6,6 +6,7 @@ from posthog.hogql.ast import SelectQuery from posthog.hogql.context import HogQLContext from posthog.hogql.errors import HogQLException +from posthog.hogql.parser import parse_expr from posthog.models.team import Team from posthog.models.utils import CreatedMetaFields, DeletedMetaFields, UUIDModel from posthog.warehouse.models.datawarehouse_saved_query import DataWarehouseSavedQuery @@ -53,15 +54,30 @@ def _join_function( if not requested_fields: raise HogQLException(f"No fields requested from {to_table}") + left = parse_expr(self.source_table_key) + if not isinstance(left, ast.Field): + raise HogQLException("Data Warehouse Join HogQL expression should be a Field node") + left.chain = [from_table, *left.chain] + + right = parse_expr(self.joining_table_key) + if not isinstance(right, ast.Field): + raise HogQLException("Data Warehouse Join HogQL expression should be a Field node") + right.chain = [to_table, *right.chain] + join_expr = ast.JoinExpr( - table=ast.Field(chain=[self.joining_table_name]), + table=ast.SelectQuery( + select=[ + ast.Alias(alias=alias, expr=ast.Field(chain=chain)) for alias, chain in requested_fields.items() + ], + select_from=ast.JoinExpr(table=ast.Field(chain=[self.joining_table_name])), + ), join_type="LEFT JOIN", alias=to_table, constraint=ast.JoinConstraint( expr=ast.CompareOperation( op=ast.CompareOperationOp.Eq, - left=ast.Field(chain=[from_table, self.source_table_key]), - right=ast.Field(chain=[to_table, self.joining_table_key]), + left=left, + right=right, ) ), )