From 2678737d5ebc891e62dbb419ac5d1c630a7f8c9b Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Thu, 26 Feb 2026 13:31:15 +0100 Subject: [PATCH] prepared statements. changes to entity --- ca_core/__pycache__/entity.cpython-313.pyc | Bin 6667 -> 5906 bytes .../__pycache__/group_member.cpython-313.pyc | Bin 1372 -> 1951 bytes ca_core/__pycache__/metadata.cpython-313.pyc | Bin 2009 -> 1926 bytes ca_core/__pycache__/property.cpython-313.pyc | Bin 1506 -> 1279 bytes ca_core/entity.py | 129 ++++++++------- ca_core/group_member.py | 35 +++-- ca_core/metadata.py | 25 +-- ca_core/property.py | 32 ++-- create_tables.sql | 81 ++++++---- create_tables.sql.old | 48 ++++++ tests/__init__.py | 0 tests/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 129 bytes tests/__pycache__/test_all.cpython-313.pyc | Bin 0 -> 1364 bytes tests/__pycache__/test_entity.cpython-313.pyc | Bin 7338 -> 5177 bytes tests/__pycache__/test_group.cpython-313.pyc | Bin 6557 -> 6983 bytes .../__pycache__/test_property.cpython-313.pyc | Bin 5763 -> 5578 bytes tests/test_entity.py | 132 +++++++--------- tests/test_group.py | 148 +++++++++--------- tests/test_property.py | 139 ++++++++-------- 19 files changed, 414 insertions(+), 355 deletions(-) create mode 100644 create_tables.sql.old create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-313.pyc create mode 100644 tests/__pycache__/test_all.cpython-313.pyc diff --git a/ca_core/__pycache__/entity.cpython-313.pyc b/ca_core/__pycache__/entity.cpython-313.pyc index ae62474f16d8d7dac3505b88121ca4d323aecc9c..c3ad9887b4a9f3ba9b1e7d3b6c845775f3061bcc 100644 GIT binary patch literal 5906 zcmeHLPfQ%w8K0RQ_Wv&S21sylJT@_SV+>7gf&pWfn6*tz7H5Y|w{@e{dUp&{!w!8j zivvw6a!NP_8acI^NcF8o>cQ%zK2oVteH^rjf~Hnvsi#OiAV@Dh_50qN*?xQ zL!WlOc{A_teDC*u-|y{qDCFlL{blfuDmQW5Kk>myf}PpE2ANx&%*p&^j<7pVcv(2W z5kYo=b;%-FQEmX+AiKf3We-@7>;>zUePDgEAFN*vfDOn&ut7NlHYA6^h9~)G<2N{9 zJnOQ@#hVBhK5zdE%q=d-C3whGC7Dn1r}$obz7p1|?8C0zYv1#(r0bZS&-gel>5_#8 zc$QrQH#sg7;JB;!NRuq~+T$ra)N8GQkEzgLnB?1F+ZzxNxG3S3c7S5G5)5cdkKMT?V&|5i<`z26v+<m%%J@8>0Wi_y_R~ z@9S$nTO0eGyh%vei;*hDo4N{R)!d|xl1O3Zf)D>U&)Neae8jZ`b z#01WUja!5wrHu7w};1)FKlJGH} z92&SOzI%MmD!@*88=e%g&j3z>(Q~zdqLQ@YCm#S~`OBn6 zARcf!I)ZL72GEd_C))IMnvG44Kxq`NZCKheJ~zC*_x<`c$2@*st{F0ZTVBxp_Lj+C| ztuXuF0FX#9#cGTNDF&4^?VTKj8fV}l?Pw^cIr0u(&`?M@Z4uyVM9)#)MSvQ0&fOW(1g~1Bxw>$DaS^br{ccJYHWjCtc zcIiHfcFWC3XyMR!etKSF54x1ivyGWnb~1Nw4GQ?t+o$Wj8D&kAMqtg5D`6~ln}S?-)VsTz7zWYOKnCIMlB zlBX45-J~(->ljy%Ng0tkAg6OAC!uT{F8$A7N^yR}8++hed#^5!))aLf|3|NV@ayHN zqUR+_ssqLFpy?fa6lwZh=St`29e?QlefOVQzKRa5M~D9A{(I=Jq4nsS=39yN19CAk zv3R+feVcE0t+#aEZ8g0opF+X74V{E|pxbf;GPk&^rL#|pPYS(uO{va#M}`v`z^_6O zPExVgnY=PGJP~u`ogAC6PZMx(*l~#P5M@rgq2yKtDwpXR+X{>{!h{I!k)6sb#G0g; z_9lH$LB&e#U>3QDA}C}tdiD$PTuE@hdT;dpym@l;->A8F6B8~s^}sA-zI$TI?L6(3 zHa4901SXA7*&FeddS@+ZI6mSy(9yL7H6qgnOtCV5Q7M35Q^K!*QZXE74M2g87kaG% zy=T{2*MhatyW<=Z2rs_z1P9JRuivi&WtYuoVtk>m#Ah}9h#MQhSJ#seKA1Zl_))2~ z+nu4ZJCQvw;Jpu*j+qLN4zG=@_3g={9~n#irr7j@b?cDr*78ZHhI{E>0`;AXij`uO z^rNqK;Ip~Hq2)2iS*Z9SRvrRV;n<50q7T~k=Ggbv18t_*_Jj4T16ai7v-_0d69q^O zpEg$b6niH>fqE-&?S|>-+L^t1b8czS6kC`xkusfB3Lvk;o2mffdU^m!9j*!=LVV%q z?(pI4-->^r$ZIg59ndNHS>C*IlZJ2>HgJ|egLWKm)Jr_b>YRt-&*0KgY1E1w=5qu0 zQ|6hsz7!MR%V!_o{n_OWQ58Aq$FDLq! zX!gY`qGsi?m;Rz?gZ4X@J=NZ8*OWNW1AIKn)8R-A$J;r(%2k1Xd~|XN>iiMwp^C;; zX}#TX_vc@V<8%q4jid|SlWsKl2OIt4g`RHd`Ge{+stE|jI!e!!O6U$Cxa4dqILs-CSB9LYbq8C(8{3T-8}m0Gg(u`T*j%e2BF6V^&g(BxQ}SSHms zls`m}$mzmyH?fNt6^bS;x@in_(WRCuP_#l=W*u!=DvH+J@>8p{T>dA@2fABuiH58U--u=HnXytg~}sN;w1heM~ypAdC9hu zqc+J7(k>B@L~?+1NKTMW$pz9S2_OZ@4bm-nKzbxENU!7r>682*{ZasAKxzWnBsGI< z9_NEwzQxHSX}h@wyb-wY|LR{sJmTV9l!r=Pk@z@&jPEhmE@G?dG0fII<~?hV+dIvA z#>;VI4r@5c)?^ikg=9TPt(wr3^+iC8WF0g^|udm!8nYpSuL3JW4 zy4XFDyCHW^t1|7Lx~_DmQ^|CW%H2l$!&7(kEyK~k2j_$H?IjXf@-%7^ zX=~eq{KMXvk!P-^Mc1}xUa{zX`)NzNwmV#GIji~3F55ZZJIey++f($0>R%jMg_*e; zV?--p#u$Oh`o@@OIc9rNW0Y(-=sqrJj}(6DF*GKM7a~$P8iQNxvbBMo+0+d=B#zVE z^i*;xBu-6VomA4v>++ql^#sfP-#;}H4@+X_el;YXJvAbQLn2lq@sW{Aqi8r5k47#; z&Vh}Uds5uEpp6P}qi(F*DOftE6P#c7RHju?6R=|yZJ&)TdiQE%FWm}#(&+hC!9_8}{~0Rl zD?V<22^1i4Vxd3^#*Y(YE3Vf1wY}ok>tGK_w7|Uf7v6A(x#7;p^8+RVY~uYCdptij zN^>`rjGVE+0-Ubm?BG#UR;O~=jF``fDXY!%yXBqzj%;eb`XUxZ(U%`Pm?_a#s8a|$ zobIW>2@Svl6~B}@fnimCIZJbslgTL=FeyvF2XArDR8#N0=7KPr)nb)fK| zBm9(Vjx{D06&9XH42-b}zJ_!$_!#=~NcU zj$saBYEnt5vZ{06qUM6U&R?gypvK_Fh;8mlY@?`DoqUv2vbvL|vYFfs;8hbjB@IJ3 z)jS2pqx(~8T2Ydday}1>(VZD(T*<3JVuVGv0Zo9#48U}+F-#JprHUa8(aZod%VgOa zDaOU~aH(H|sKT;~3(CTWC32+>%W4RY%kz2fy-(Pq?-v8dHR1SD%NxHq`0(Jb4*dG? zFAx8|?Qg;U#bE!RoPY8E(Z3kHq`eni+$j}XVlx-(;jeWzwAgm=n;n{PbQ26j2eJz8 zEOV&aPebM^%9A$QF(tcFrWzoo2mStF#P|(^3jv(ij}7td{nac zeYwUDsscyjTvu|e$%xCLq%s|hz1GK^m7oMZ=86=Kn&5rray%02>0cq1hx{DhkWRy9P^S z=$Vzco?qx&I8-7D)82}%?X#_mt~Y-JuHM01{Yo(~rU_$Z13kCUwXnNHK3HR*gT=s* zCJa>>Xf@YCC}q$*1(jFJb&M=lG|i=}fq}5Y42nxM5&|Q)q{w0n{*Nm!2TaenoKG63 z1oR}lD8Qv6npTC&o;mj)06B*@3YXr%%n#O*!m^_9gyGdH${>ZHE{I?qWopzwU&tcB zn+%O%63oEcYN;H+`$q#y|Ly#&)@IW$Q!S% z`huqVD(4an)L3P9n07KHQjEs6al5rjah37t5H$Q5HbyV6QsBV+j(J~+T-ro|A1`{_ zHDX5ZhMxL*7$Dl(#=2wnhGeXP?wYD}ha=I`(C+7`wKZnfI~OXEbDL=G=KT{IX*0|Y zJ=H2@=85Ci#b5j8%rmj4`e+W~n=Mk3C5$t1IC0Ll`pW&7+ zWaV3o?i+Ce7Yhhxa{eZ@3}Hr_v+(*WxKzwktw>Qz2K(j`+R68xkmxJ<`nT48eZ}?x zd#ZInhhfpci}66v0Bz<@jE&WPcWc#mSB9e(pyO}h+TiPl=KHkMKYT*2to?cfKo;8Y zK%jQ-6CFLc04O2{yUIs`Je!QN6svY^)q4WRrz*V&-F^p`ig9R$`#0G!d*GWZPsnAq zgy0t13D0R43Otr(XPfLSik&;KOpan$VziTS0T!+p@-p;b&OmYNjYZ*vkBxH*BQ+FG z-pFO9C*=Wp30`3~rG5xv+0OI)-#I`0Ry4ift=xHeTXaJT`ueUj;eJvv2z^B8nbw diff --git a/ca_core/__pycache__/group_member.cpython-313.pyc b/ca_core/__pycache__/group_member.cpython-313.pyc index 4d44604235ea3d9aa35a75f0f1f2aab99fd830f8..94f4adbcf54a0c9dc2566160ce1764d9b8b12c4a 100644 GIT binary patch literal 1951 zcmc&#&u<$=6rTN=wcQxUp+HI_jTBO4Ns15HQI!IvY8E9Bwvsn4MO3of^~71Qz0vGC zCFT-Nw5JvcB#t>Iw;prMpJ2&7SgQ&lapPpk>798yyNMIkRP@r3X5PFvv+uq6-Z!&@ z@$n>L?Vsy=&Q}UTe}fV?HJS`o*yML)p?k;@SQqaJdDW6Gq3>lX#@4)S$)QDpJ$cbm zLQRHsrm3N()W&oz&}!6eZIzvBkZ6#`eL`U>Q@2iRIgZUFgqK(!ED8|5go;d_Zn-f^rYFIw~RI8);jigw#WkZ{EFZ5@E66~ zUt`kr9e)RZvtkqt>{LIU^#YZz2WrLkooy1x@KvCM1#3^OHn%Hvr;1&QohILb`O<6@ zo9 z1!Lq#Vz+z}XdA@0H{B))l6NZg7BMJwsjdX7-J+gLDe#5@VJI*!kf{5>gL-3V!Ij*m z+aS4?M`&*A2PbD&%C<{Mj_1R3`Dkk=$W*G;^01L+m=AF4{l${S`023L%^AvlJr7tc^kG~3sg8O7EO&Z=P?B5uX?V+_ML&E?Z7GJ`Hqc!6|@jx%vu5D#); zMWAmn8B9{raJR`>kSE#7JFI)}vSAnXILJNCU?=Tp)8RSk1Y5JC=YdTTfP?>>nAci1Bbgk6uy2#;xL_Nqp2I69k_#6eVyy4R5 zicmmc8urezVF-%lULyPO!ZB35ub~Us$2T6`=%y}r<1`NH(&J%+H zIGh3FZ2*IN^~&xB$AwO@Z2-edu$Xw58i|FetAXS+{Xp`38eU)u1Ph@JeIB3?GfqhD zlZIPu)yZcRV29s=_az(pk{}3uH6y5dULP^>l#8R8W6=6nWI^4Z=_4i%nTd&hS`nuA Rtz*PQKLhiRU%)(!^)Gsusi*(| delta 648 zcmZWm&ui2`6rRZ>({$@@t)pfh?%q6!c=Av1xWrzHXCYvFQ}Df{LB)ajUgrJ!zV~uCc~q(Ws8%Zs&)5C8ofUCV z1G%k0$6E(`SNJva%Wlp*yYN)p6t^tOV$5T0=39Z;x8~0>%?InUKji6{dZnU%qI^4* zQ6GB%Pz?wI8$lD8x>!6TdG=M_4_&U)?wiK)H- z7tNo(n!0$N2jghdX;8iRr3%{+_qW5I>b+0^Rg6$nUqV1GyYeR34My&R(fHICw|VLa zN5TZgB2sH?xyD=Ov2%U%U;d#5V;E1}4V6TjYu&DI6#vH2t$^=b56 zB=3I|-(_QmW(<6piq2M&3Y`GZZ^-wPivubjvV0fCKHXL?Uhj9+9iZKDy!s(RCOGGr tw7GnzGlu4UVLs2E&?`HKFqq9FA4`|R>&?OH33GTet04?-oYHqr_8W>KnV$dv diff --git a/ca_core/__pycache__/metadata.cpython-313.pyc b/ca_core/__pycache__/metadata.cpython-313.pyc index 4cf6b62fcb368a93aeff2ad0a504da087b89d00a..eebe600239dd85de3040e8cdddff04a25cd0977d 100644 GIT binary patch delta 277 zcmcb~-^S1PnU|M~0SKf_7G(Zm-pCig$jCf7p0SZpdh$a?2}aw=e;AEqIDra^g@Hr^ z!wnX$cE3iy3&I8)OgCs;WwD;@#dMRAcd`m|BcmKpiwH+@er|4RUWx7GBg|SdAWi5x zjW?ujh`7pPH<^p&5u^C#w=7bOjIxtC*lO7rG@1OW3_+&I7L+FCWG2UFr&ijk7V9V! z6lInrmZU=YlUdk}6qG?miz7Sg28Zn2ieFioS=lDXuzMS70Odb0Gcq!MVq<31 j_{77=X!D7KnNb_WvInvBKJhR!>VM{8VPq@f0V)On)!<4? delta 383 zcmZqUzsb+{nU|M~0SH)JretcdY~%}IWMrHi&)CQ)JNY4_1hayz!sK6!Mlx(bCB?!( zqJiNC3s<{equ&K#qYa@OJg&0XPL^YBo_v}qjFEG)5_2P?0#Ks}S8{%CZfaf$NdEz5 zEg3#6`b~GRZm77*;sDm)$r8^fxcMcE6eFY9WEQsCdL~WgDjlxi)RJO_r2LW$g@V$g zoXli}#Jm)Rf}+f_#FA8n?9|F)g_8XA)RK(UBCaZLE(ItE4RCP`aaG7oElEsCEJ=im z2fKzq&4~x<1-ebOSO;brTm-DzZ}JW{BQKCq diff --git a/ca_core/__pycache__/property.cpython-313.pyc b/ca_core/__pycache__/property.cpython-313.pyc index 872b54a171a6c6f299808780c9ce42dca681d331..e24e311826473589ee98f7f2f59cb87b11d34d51 100644 GIT binary patch delta 317 zcmaFF{hyQfGcPX}0}wcyEXd@Z$SbK~2INd-2xf?32xg37G+_y5l48hWDPlqrVFrmz zbSh+H&}8zfYMuDMkwpOrCO0z1X)*(~7fS<)28J6fTncA;86O? z%g)O787R##u|RCH0#h;zgC>jLrJ})shRlzOD-xn^V5bog`frgp z%FN34fn%~eYo>!aP{9XgMn=XDY#fYi3!FbN0I40!UqI9cF*Zj11;rm2fYc7-FCglJ R5G$kog5<9ZK&l87U;vF_P-p-E delta 538 zcmZ{iO-sW-5QcY?6xt014+TG{Fenu&Qb9a;5z!J+(I|}{Afg1b)s;5UO%$7h+LK;O z^s4?0|9}YM!QbFj5Ao})8?_M+&S8g{-Qj)aWj|66YWg*uCP>81#VLDiq}#K-qBuha zT0sV0!Gnq+b)YSUONQKm8v4$(l$U*tj9QipCTw$On{uxPWru^eNrBoP^J)c{ECchv ztZ-^tHK6;<_1r$N9O{DYc$>`LC=kEixUOS8n8K?)Sg5+4;y6NK5+&4Qy{?By$s|Rk zX6{qn?VfD}OP@&`q3g zz5)q!X-@E*OInK>@eF 0: - raise ValueError("Creator cannot be deleted because it has created other entities") - cursor.execute("DELETE FROM entity WHERE id=%s AND creator IS NULL", (creator_id,)) - if cursor.rowcount == 0: - raise ValueError("Creator not found or already deleted") +def revoke_entity(cursor, entity_id, requesting_creator_id): + _verify_ownership(cursor, entity_id, requesting_creator_id) + cursor.execute( + "UPDATE entity SET status=%s WHERE id=%s", ("revoked", entity_id) + ) # ------------------------ -# Getters +# Getters / Setters # ------------------------ def get_entity(cursor, entity_id): - cursor.execute("SELECT * FROM entity WHERE id=%s", (entity_id,)) + cursor.execute( + "SELECT * FROM entity WHERE id=%s AND status='active'", (entity_id,) + ) row = cursor.fetchone() if not row: - raise ValueError("Entity not found") + raise ValueError("Entity not found or inactive") return row def get_entity_id(cursor, name): - cursor.execute("SELECT id FROM entity WHERE name=%s", (name,)) + cursor.execute( + "SELECT id FROM entity WHERE name=%s AND status='active'", (name,) + ) row = cursor.fetchone() if not row: - raise ValueError("Entity not found") + raise ValueError("Entity not found or inactive") return row["id"] def get_entity_public_key(cursor, entity_id): - cursor.execute("SELECT public_key FROM entity WHERE id=%s", (entity_id,)) + cursor.execute( + "SELECT public_key FROM entity WHERE id=%s AND status='active'", (entity_id,) + ) row = cursor.fetchone() if not row: - raise ValueError("Entity not found") + raise ValueError("Entity not found or inactive") return row["public_key"] def get_entity_name(cursor, entity_id): - cursor.execute("SELECT name FROM entity WHERE id=%s", (entity_id,)) + cursor.execute( + "SELECT name FROM entity WHERE id=%s AND status='active'", (entity_id,) + ) row = cursor.fetchone() if not row: - raise ValueError("Entity not found") + raise ValueError("Entity not found or inactive") return row["name"] -# ------------------------ -# Setters -# ------------------------ def set_entity_name(cursor, entity_id, new_name, requesting_creator_id): _verify_ownership(cursor, entity_id, requesting_creator_id) cursor.execute("UPDATE entity SET name=%s WHERE id=%s", (new_name, entity_id)) @@ -161,10 +159,11 @@ def set_entity_name(cursor, entity_id, new_name, requesting_creator_id): def set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id): _verify_ownership(cursor, entity_id, requesting_creator_id) - cursor.execute("UPDATE entity SET public_key=%s WHERE id=%s", (public_key, entity_id)) + cursor.execute( + "UPDATE entity SET public_key=%s WHERE id=%s", (public_key, entity_id) + ) def set_entity_keys(cursor, entity_id, public_key, requesting_creator_id): - # only public_key for current schema set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id) diff --git a/ca_core/group_member.py b/ca_core/group_member.py index 36216db..4ab3142 100644 --- a/ca_core/group_member.py +++ b/ca_core/group_member.py @@ -1,27 +1,42 @@ # ca_core/group_member.py -def add_group_member(cursor, group_id: int, person_id: int, role: str): +def add_group_member(cursor, group_id: int, member_id: int, role: str): + # Verify group exists and is active + cursor.execute("SELECT type, status FROM entity WHERE id=%s", (group_id,)) + row = cursor.fetchone() + if not row or row["status"] != "active" or row["type"] != "group": + raise ValueError("Invalid or inactive group") + + # Verify member exists and is active + cursor.execute("SELECT status FROM entity WHERE id=%s", (member_id,)) + row = cursor.fetchone() + if not row or row["status"] != "active": + raise ValueError("Invalid or inactive member") + cursor.execute( - "INSERT INTO group_member (group_id, person_id, role) VALUES (%s, %s, %s)", - (group_id, person_id, role) + "INSERT INTO group_member (group_id, member_id, role) VALUES (%s, %s, %s)", + (group_id, member_id, role) ) -def remove_group_member(cursor, group_id: int, person_id: int): + +def remove_group_member(cursor, group_id: int, member_id: int): cursor.execute( - "DELETE FROM group_member WHERE group_id = %s AND person_id = %s", - (group_id, person_id) + "DELETE FROM group_member WHERE group_id=%s AND member_id=%s", + (group_id, member_id) ) -def get_groups_for_person(cursor, person_id: int): + +def get_groups_for_member(cursor, member_id: int): cursor.execute( - "SELECT group_id, role FROM group_member WHERE person_id = %s", - (person_id,) + "SELECT group_id, role FROM group_member WHERE member_id=%s", + (member_id,) ) return cursor.fetchall() + def get_members_of_group(cursor, group_id: int): cursor.execute( - "SELECT person_id, role FROM group_member WHERE group_id = %s", + "SELECT member_id, role FROM group_member WHERE group_id=%s", (group_id,) ) return cursor.fetchall() diff --git a/ca_core/metadata.py b/ca_core/metadata.py index dc1cdb3..0413485 100644 --- a/ca_core/metadata.py +++ b/ca_core/metadata.py @@ -1,35 +1,40 @@ +# ca_core/metadata.py + def get_name(cursor): cursor.execute("SELECT name FROM metadata LIMIT 1") row = cursor.fetchone() return row['name'] if row else None + def set_name(cursor, value): - cursor.execute("UPDATE metadata SET name = %s", (value,)) + cursor.execute("UPDATE metadata SET name=%s", (value,)) + def get_comment(cursor): cursor.execute("SELECT comment FROM metadata LIMIT 1") row = cursor.fetchone() return row['comment'] if row else None + def set_comment(cursor, value): - cursor.execute("UPDATE metadata SET comment = %s", (value,)) + cursor.execute("UPDATE metadata SET comment=%s", (value,)) + def get_public_key(cursor): cursor.execute("SELECT public_key FROM metadata LIMIT 1") row = cursor.fetchone() return row['public_key'] if row else None + def get_private_key(cursor): cursor.execute("SELECT private_key FROM metadata LIMIT 1") row = cursor.fetchone() return row['private_key'] if row else None -def set_keys(cursor, public_key, private_key): - """ - Sets both public and private keys together - """ - cursor.execute(""" - UPDATE metadata - SET public_key = %s, private_key = %s - """, (public_key, private_key)) + +def set_keys(cursor, public_key, private_key): + cursor.execute( + "UPDATE metadata SET public_key=%s, private_key=%s", + (public_key, private_key) + ) diff --git a/ca_core/property.py b/ca_core/property.py index ff30d4d..eaacf42 100644 --- a/ca_core/property.py +++ b/ca_core/property.py @@ -1,18 +1,17 @@ +# ca_core/property.py + def set_property(cursor, entity_id: int, property_name: str): - """ - Adds a property for the entity. If it already exists, does nothing. - """ - cursor.execute(""" + cursor.execute( + """ INSERT INTO property (id, property_name) VALUES (%s, %s) ON CONFLICT (id, property_name) DO NOTHING - """, (entity_id, property_name)) + """, + (entity_id, property_name) + ) -def delete_property(cursor, entity_id, property_name): - """ - Remove a property from an entity. - Raises ValueError if the property does not exist. - """ + +def delete_property(cursor, entity_id: int, property_name: str): cursor.execute( "DELETE FROM property WHERE id=%s AND property_name=%s", (entity_id, property_name) @@ -20,14 +19,11 @@ def delete_property(cursor, entity_id, property_name): if cursor.rowcount == 0: raise ValueError("Property not found") + def get_properties(cursor, entity_id: int): - """ - Returns a list of property names for the given entity. - """ - cursor.execute(""" - SELECT property_name - FROM property - WHERE id = %s - """, (entity_id,)) + cursor.execute( + "SELECT property_name FROM property WHERE id=%s", + (entity_id,) + ) return [row['property_name'] for row in cursor.fetchall()] diff --git a/create_tables.sql b/create_tables.sql index 581ccf4..7d2524b 100644 --- a/create_tables.sql +++ b/create_tables.sql @@ -1,43 +1,62 @@ -drop table metadata; +-- ------------------------ +-- Metadata table (singleton) +-- ------------------------ +DROP TABLE IF EXISTS metadata; -create table metadata( - name varchar(50), - comment varchar(200), - private_key varchar(500), - public_key varchar(500) +CREATE TABLE metadata ( + name VARCHAR(50), + comment VARCHAR(200), + private_key VARCHAR(500), + public_key VARCHAR(500) ); -insert into metadata default vALUES; - -drop table entity cascade; - -create table entity( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), - creator INT REFERENCES entity(id), - name varchar(100) NOT NULL, - group_p BOOLEAN NOT NULL, - geo_offset BIGINT, - --private_key VARCHAR(300) NOT NULL, - public_key VARCHAR(300) NOT NULL, - expiration DATE +INSERT INTO metadata DEFAULT VALUES; +-- ------------------------ +-- Entity table +-- ------------------------ +DROP TABLE IF EXISTS entity CASCADE; +CREATE TABLE entity ( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), + creator INT REFERENCES entity(id), + name VARCHAR(100) NOT NULL, + type VARCHAR(10) NOT NULL DEFAULT 'person', -- 'creator', 'person', 'group', 'device' + geo_offset BIGINT, + public_key VARCHAR(300) NOT NULL, + expiration DATE, + status VARCHAR(10) NOT NULL DEFAULT 'active' ); -drop table group_member; +-- Indexes +CREATE INDEX idx_entity_name ON entity(name); +CREATE INDEX idx_entity_expiration ON entity(expiration); -create table group_member( - group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - person_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - role VARCHAR(10), - PRIMARY KEY (group_id,person_id) +ALTER TABLE entity ADD CONSTRAINT entity_name_unique UNIQUE (name); + +-- ------------------------ +-- Group Member table +-- ------------------------ +DROP TABLE IF EXISTS group_member; + +CREATE TABLE group_member ( + group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + member_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + role VARCHAR(10), + PRIMARY KEY (group_id, member_id) ); -drop table property; +CREATE INDEX idx_group_member_member_group ON group_member(member_id, group_id); -create table property( - id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - property_name VARCHAR(100), - PRIMARY KEY (id, property_name) +-- ------------------------ +-- Property table +-- ------------------------ +DROP TABLE IF EXISTS property; + +CREATE TABLE property ( + id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + property_name VARCHAR(100), + PRIMARY KEY (id, property_name) ); + diff --git a/create_tables.sql.old b/create_tables.sql.old new file mode 100644 index 0000000..3e59690 --- /dev/null +++ b/create_tables.sql.old @@ -0,0 +1,48 @@ +drop table metadata; + +create table metadata( + name varchar(50), + comment varchar(200), + private_key varchar(500), + public_key varchar(500) +); + +insert into metadata default vALUES; + +drop table entity cascade; + +create table entity( + id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), + creator INT REFERENCES entity(id), + name varchar(100) NOT NULL, + group_p BOOLEAN NOT NULL, + geo_offset BIGINT, + --private_key VARCHAR(300) NOT NULL, + public_key VARCHAR(300) NOT NULL, + expiration DATE, + status VARCHAR(10) NOT NULL DEFAULT 'active' + + +); + +CREATE INDEX idx_entity_name ON entity(name); + +drop table group_member; + +create table group_member( + group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + person_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + role VARCHAR(10), + PRIMARY KEY (group_id,person_id) +); + +CREATE INDEX idx_group_member_person_group ON group_member(person_id, group_id); + +drop table property; + +create table property( + id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, + property_name VARCHAR(100), + PRIMARY KEY (id, property_name) +); diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-313.pyc b/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..397683402dad9fc513b031b91eec70c8e59c770c GIT binary patch literal 129 zcmey&%ge<81jqFjWP<3&AOZ#$p^VQgK*m&tbOudEzm*I{OhDdekkl;&{fzwFRQ=N8 z)FS`eWV)Z&t2{rLFIyv&mLc)fzkTO2mI`6;D2sdh!IKn)-Ri$RQ!%#4hTMa)1J E0E5~az5oCK literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_all.cpython-313.pyc b/tests/__pycache__/test_all.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f74ef4dbcce98b2887c380a2b068a0b2b90027b5 GIT binary patch literal 1364 zcmb_c%}*0S6o12RKepY4LQ#|hSu2TY0#su#0nwYp05)b96BC+j7Q4bqyDhU*wH&lP zRpSL>Ogwtj#DByiw5DXhNWzI*WB&nXwq0!CMB*g-e(&QqZ{Ezj*|wrYfQ{cbzt}(e z0FJrgk8oFMiwNKo=%AxTz#T*wF}rWEZ$ZF<3K^vNY=1UGE!tyrRUIxW_cGi`!YQ$Ah zsr5qMN|js-r-~bPif3>=X_&d(RI!W)7||14ao;jgfkWj|Q&IO6wXP&8^M~?qQy$xs z$Lg|Ld2l33wRAPTeWw|p+>K9u->AoD8{*ylGwysnKGzWMopryc$7dSi>{)l0eH4vV zmfEZYpkc$vo3>*ZCwwQY@1>U(UM;xnSdv4Sw>%ZxvP(ln_8fBdD&{YMirk_>+hO}9 zj6g6?^xV!_hCzcx6SITGygxDjL15ltSmALt&&#C>()@!oN&Pppe4m;E!?x4W% z8{k1cg|`C@&*i;)yN|CilmAuRO(t3bLZ}@C6gz;(FSycTs#ptG!?l6xz&6^sy+b}f z{P?ChGO;@{QIAhHVCoo#4q*5I2ATXLN@%ck8ASQrR$aKd&#z;APZ|HZxvSi&%0OF`u(qC4-2nYZG literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_entity.cpython-313.pyc b/tests/__pycache__/test_entity.cpython-313.pyc index 5c90fc836aff3da2da5f2672fdf75f649d5ac1c7..fe1813996fe9368b0d0b71c05dd8817309c41980 100644 GIT binary patch literal 5177 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 diff --git a/tests/__pycache__/test_group.cpython-313.pyc b/tests/__pycache__/test_group.cpython-313.pyc index b7a2e3c0274b61b37d148a49cce39965d4372bde..dd55b6e33eb9864a8bdc7ea6894c1e1d10cbf741 100644 GIT binary patch literal 6983 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 R_ltACJpaVaa1q1)zX0!Aiv0ip diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc index 2efb10d3f7b774c830a1475bc2544bf6351e3a97..ebab9f6949569b120df9f5b929b0a72afa9f64bf 100644 GIT binary patch literal 5578 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 literal 5763 zcmdT|Uu+Y}8K1Rx?Xj~?!X-fB09i;l;*dW*4k`i%I_FQ1gNwO(lPeY1+dB3pXW{Jn zX4kz-t~#A6wN0v4&}-=hsS@F(kSBsviBq52hxVByPEF@Ps?<{@-cUkSsZaf8*K0e@ z8p>5VwG-vr+4*L^8PEK_-}miohC)FErR(}H(|@l+=x^-BDMGQb`3Y36A{oitIYjIy zM>u%*ob#OZ60gMCyyy6{0uih_4|N~$OQ;1MK(Y`MDE`lGi_gI9ZIF(MC3en$(x=+>2v^m3CvqaCT#GFYw zCfJiJzHAOcI&h^L-W@itKYiUz5q1})sbs{+{ktTCm(oE}`(U)`*Im#V=h{-VC}MZfP-1s3atV* zM5kVU#v96YrzZ2xDkV1ae>zGikGfcbOdMcQT@KW+D*I%=11wm1>6cVkbA^}t8f7d8 zK+{}3J3_~bdku4xep|&H;q4=8t~e;yJ#&tF$KDi*h_Tr~YC=m)tEZBQZvfWo0Q|~i zB5CSmW>y-DkH?0hVzt zQtzNX;k*q}UoVldQzXBPMg%1%n?H; zgkh0-;f4Cu57cDNR0-QgS`P-nSCu4D6YwKwuvk&jDH>8WqGvJ+IJ2Q^G+;lOMzngV z$`sLaS&FR(v@da#H`L6e!MsGG2m9aEr`7%(Owgabl?BcIfv5YvEC_(Ez%zRA}v8ZSB1;HeNn6f98t1@aO|K^$giiHOf~d8< z&~kXS<*?J~-9=xiQ!8}(S!fe}2--qFj6y8>S%k_}=2q~mF7XnLlr?2WhA}SG6K8B# zRkn<&(y`a4j{)#=u?SC^peMx)788l&CBht<3dxKPXS@&U_Q?~;>Sd}4GOWL^(Vbh4 z)@f#)j6M(_BX%ZmmIWMM!C`iy_8DESpRdp3mZJ7@!+gU!ZY3QsWXm?PfN5>xp8$8y zW^wl|gVqw)g1uq^#xHKaR2dzxqc1-#j&V5(ce}@m(L)1{k3#}bU1V;YbNXzVxfFHE zBc5;OujChn%Byo~e>UuL0*gXlyPtcC0pbK--xcXKtl#YfIW% z%DdS(o!h)1p$(t81;THosIZO4@^jUnPB{9Bd{T*W>ixxgOf3vgT9y! zP$Wm7WeeJxEwoh)r2Dr6uBr>8a^t%oYGG~!f|`ZKJ*#-n!r1i-*DfrYH$S@Z(K4Xy z)Kk&uAZbxp$1iRNP$^^n9ZYY9Easn8Ke#uG-);H;qP&=%E=}vju(Im$Xvrf}B^@FrwLKYVBQPC!fcp_~8 zfdt=5WOC{VA&@&+$9)`Zm5+N{u(Liol;Sy67%t&~_SQUBx9%rlI^*jgAUKA06hiw} zLi>I_yB_L(AT}06X;qXKn{T$JJw}-G2^s|I1)8Pr9VAIGj6C)rZJ~sv;7U4A7C>~;Q&Wd^4S!a*MI$Hy# zGM&qq>1;-&k?k<8(RC1(Vtfdq77JOCuXaqVsrlk*(XxQ|uHd~3>gvH)*Ku?^L6wrh z$N!t)c7aw6aJ$gnmjz+P#m4hlby;l*+U(lnnQ;^PxLr|rD%0B1`_7$r)^-oii4S%eq#yCSzbZ+9(qEf3!vy)(MDYjCdauf+^B zweUtBM<5?9W&q6#-_PTovi4jacU5XXT*rqk_lpSDS0QPIW!n+b+GLiO$08w$6@^`x zC<+ZK%Cw%!WmsKQ6qb<}H4Ta~nI?vrNo%U6gW9hsDLttuWIr={h>4?2oM7TrCSGGA z27>NX6vIrI>74OU) z!@h0tClg9iC-y}PVZ3A^l46F7p#*%NSj&2o4d