From e4a6435c11b415bb866a4c20134616f8c3b1edf2 Mon Sep 17 00:00:00 2001 From: andreas-p Date: Thu, 14 Apr 2022 14:23:35 +0200 Subject: [PATCH] Partition Tables --- modPg/Database.py | 7 +- modPg/Partition.png | Bin 0 -> 1939 bytes modPg/PartitionedTable.png | Bin 0 -> 1828 bytes modPg/Schema.py | 3 + modPg/Table.py | 128 ++++++++++++++++++++++++++++++------- modPg/_objects.py | 7 ++ 6 files changed, 119 insertions(+), 26 deletions(-) create mode 100644 modPg/Partition.png create mode 100644 modPg/PartitionedTable.png diff --git a/modPg/Database.py b/modPg/Database.py index f4314c8..13bdcf5 100644 --- a/modPg/Database.py +++ b/modPg/Database.py @@ -74,9 +74,10 @@ def FindObject(self, patterns, schemaOid, kind=None): if patterns[0] includes a dot, schemaOid is overridden """ - # relkind: r = ordinary table, i = index, S = sequence, v = view, m = materialized view, - # c = composite type, t = TOAST table, f = foreign table - # P=Proc + # relkind: r = ordinary table, i = index, S = sequence, t = TOAST table, v = view, + # m = materialized view, c = composite type, f = foreign table, + # p = partitioned table, I = partitioned index + # P=Proc queries=[] if len(patterns) > 1: # dtype=patterns[0] diff --git a/modPg/Partition.png b/modPg/Partition.png new file mode 100644 index 0000000000000000000000000000000000000000..f174f0597ef09cb7c39129813b8b4d600c194f23 GIT binary patch literal 1939 zcmV;E2WP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1bvcH=k<{O2j=2n0xQIS$wN`UZ3SDK1iM*@^8; zn~?}X5K%+{kfPeZf4ceyKc;Av6(3x5PUB~zjhr9^y?$|?N!xke7y1zH^y<99P#Mf- zUHY``6ZG=+z~O`PyKgr1x&)uo)|*Q#-Hs82r#DD0dULwrTgT=^#P>I(X#cRCee_;K?``caS%-+aol!H` zbSwkIjF6l8n8WZm1hStaVg#LZ0p(Q}oiPeUVnn`gS)1Ta~+DNtq&pIB)6jniMD0MC_R%7n#e(*Mm+Ki&LfsNTy65$SNo z3b=UWHDCDM>;gH#2BGQ%9O!KCwAA`5-xmYF;o*a_xULZp1% z3m`=BEdWCX3}{D2$dUHUo(ORaASeMjqc$=aAQ7$r$<`Rh%5&i18_#%}wk+&;sL`81 zgcxT$xQQl#ENcb%<843=B{5<}#EF-nBua9|Strgp??#hL-gxW9JMVq)(WhX74H8`N zA%qxG?XCA#Qih%u(bD#200S_$?En#|c`n> zu>_5FB7-w9#+Jai9}FO&d2pr!S<8dD!I_SXq7Vj18QiE!gE3HO8?kgcx%*=7E#8!b zw|L`M%&9@$-(XIGx<}rguvW)=u1v;O6^@u{gX+Wm7Lwusiw*}N;jeq$(T%1&-gWQK z>eC0N-^gwG)ul_AYW0Yx_91hQr52guYR^IzbjYY>$O2~LDLSLp(&(b=8T7_P^BHuc zsR?*X)@eUhOVhj-L_9BlDKy7X)HKN`yv}CXH>Q^K^kq>Ml9t=nsM(nN^n!Ms!vcxO zX9)PLLG0b$yjzbKi-a2@x|88=?t#C&jKJm$HpdWLT>wr2g)18%#g<K7G)>b`Nhv8k)u-(pkGt8xj*2~(xL1yEfO{uMFT0`=Uj|TJBN*Nd*=1IrK5%*G z;vX=4KmG^jbQR!I*0z=a00D(*LqkwWLqi~Na&Km7Y-Iodc$|HaJxIeq9K~PLN=2#; zb`WvMP@OD@6%i+`LJ=yITA@`3lS{v#Nkfw2;wZQl9Q;_UI=DFN>fkB}f*&A`PELw0 zQsV!TLW>v=j{EWM-sA2aAT%mWH3Q>-s#!)l5f`($RWbOA0QwL_3^Nil^|@3k3D5C$ z4J|`YC=z_$LT$f#b<6LxD z;F)0~lb$CI6N|+TmOGf043&72IHIT;&3P| zMu6Zh(5&0`_pxm^PXPZjaHX~V)dn#ANqW7lMUH^}ZQ$a%t;u`9=pn3010qNS#tmY3ljhU3ljkVnw%H_000McNliru<^&E1 zCnIks5n})V0Q^ZrK~y-)#gk1+!$1_rfAcb_P5l6|f(t!?=kN@Hta1d8;vurh4GLbv zy$c146-z;>n!FhoAt6zMNjLsi^WNkCwHO0%bNk}i0oPY^W9#Aa;)uXFrh#%`3-&{0 zDvrQ-Hk&m7v%_Pa-vjQ~uzIUJZCkN$!bb_OTcK3Pmx8KN=oy7NbP1(;)H~Ew9O-28 z{Y?x8_`G#L}YvgYb}$>geZzS0)Hl`owF=!c4?Xp6R62ak~GoV?j3=}!+e(? Z%{NT2Ql*e^hsyu}002ovPDHLkV1kkbo>~9^ literal 0 HcmV?d00001 diff --git a/modPg/PartitionedTable.png b/modPg/PartitionedTable.png new file mode 100644 index 0000000000000000000000000000000000000000..d82522129f63aa782b5972e94545bef98e3f5e22 GIT binary patch literal 1828 zcmV+<2iy3GP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1YulI$o9{MRYw2p}Pr<6xGExxpNNlBL|P>U#aU zy4r*d7D7stLN>Mk{_XS+E@tb=X^1{~Z*aNf5@!seSy!4*YutS97jqlj$?2S6hy=Z? z*O0e31#0B zrs>njc&Dqt1w^STw1l~d6*Hf1A&_yiWb_VC$e{-vjGY)lrxG_31mtdeX8_GLKzBjD zjp#A@=jb8$oVQ#aV(E8`FnBs)^?C4Th%b)iM8x+Kflc_doqhLSWAAP4E<2Bi`khgR zZqXTQpm-qUGM9O@fLk#Rvn8}>vMC^^nqm&LVv}V=ypwgd+HAXRvoE#)qT|MZ#>|Cf z%Ay2LTx9U%MzfuRB&ia?WZ_045c7$}PQPjTD>lM&wNPaXV|LR2helsbzS5}oGD2ic zJYxl1JTezB26N*HEf8Y+=1pgSZ*9|WF8PC01%o=n9I?S{j%DId`;l8Rc^2p?`gJNS zg}MzOMDVQuV+9NtZ$+)Q$OjJAB90LRWgusqjROo=RjvTZPQ)_>jy!y0O|Vm!l|2tJ zY7>Z1@e;sIk_@t(Q{+!@5jm7q)Tyf1ppi6b_Trsa?|ty2$mJlx1r0ug5JL(%N_0`9 zk0HjGVosa{HBggJA;pwZ&P*AOGHhk&XGjh{1r(vb_&r?{fUmr!C!C0DLe zQ6{Rdp~jkOZk8GiJ}Cv-+vMz?$8x@nmY~g_AW% zDTJE|JIaX;&cGNu3&z7>013^5GZU?IJeV7tnZ!60$^fZ@8>)6N1`1uXtlgg6eK7Ye z-bmxOc;mk@M}xZGV2(iDXWpK$R>ya)OvctJ95FS5(}(#MGjW7Q4<%#3M=P!;ur*## z5?|=))di!MV%iww9@r#$QgaMZzt)Jb(%SkkYogx^%x#dK zMfeh6dLH4mh4Je!)xC>#W^&_C|ki2nYGz^Y;PL zv()%WPhTm#0+7BA;rD^j3p@A{Aic!#7k=pHoZkZ^|3(sC?b2@~<1+L{5?%q)F99<2 zP7+?#=$$0I_Vi8?UIEfONqFt)og};hq<50=&xP%cQ$p_~;kOIh%N6p6N%-T!_Hu=M z03;oMkcs{We(yWT5m_530004nX+uL$Nkc;*aB^>EX>4Tx0C=2zkv&MmKpe$i(@I6E z4t5Z6$WWauh!qhhtwIqhlv<%x2a`*`ph-iL;^HW{799LotU9+0Yt2!bCVj!sUB zE>hzEl0u6Z503ls?%w0>9UwF+Of>`JfT~$WIuRGMxm7XviU9f$L<}<$GxfPtDhbc= zbq^n3@1i`*``n+SSIL_U@QK8;OgAjz4dSUyOXs{#9A+g+AwDM_Gw6cEk6f2se&bwp zSm2pqBa@yd4ik&T4wgHZl?;`5k~pHM8s!UFmle)ioYhK=weHDZ7|Lra%Uq{9j06_3 zgcL-`sG*DsEJSJ5NHLM7{kVsJ$nmGhC6lWRMvetkp+a)};D7MDTeA?GaFarDpzFo9 zKSqGyF3_yo_V=-EH%|cnGjOG~{nZ9A{YiSgtwoN2{%zpmx~<83z~v4w_@qmQpV2qvfPq_}XU*-cwU5&WAVXaxZ-9eCV6;ft>mKj!?(FT~Gp+u90KJiN#mg44 z)c^nh24YJ`L;#=wrvS0(HS87u000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2j&D0 z2q-#?)TL|y006*AL_t(I%k7dq3c^4TM&HI55yVCdtpu-NWv8Wuoj0-a1lo8SJ%FVb zuu<@@)j|Zxn%(SL1cN^aA-0)nW?&xggAo9o@z#=}7Ne$cpF^)z$8$90d_c7zsT39E zyvB13LPY$V60FZ2%Oh+~(awD(`Z~u|dh9PQcNO9XmOyEwGWbD=(iW)= 10: + sql.AddCol("pg_get_expr(relpartbound, rel.oid, true) AS relpartbound") + sql.AddLeft("pg_partitioned_table pt ON partrelid=rel.oid") + sql.AddCol("pg_get_expr(partexprs, partrelid, true) AS partexprs") + sql.AddCol('partstrat') + sql.AddCol("(SELECT array_to_string(array_agg(attname), ', ') FROM pg_attribute WHERE attnum=any(partattrs) AND attrelid=partrelid) AS partattrs") + if cls.relispartition: + sql.AddWhere("relispartition") + else: + sql.AddWhere("not relispartition") sql.AddCol("description") sql.AddJoin("pg_namespace ns ON ns.oid=rel.relnamespace") sql.AddLeft("pg_tablespace ta ON ta.oid=rel.reltablespace") sql.AddLeft("pg_description des ON (des.objoid=rel.oid AND des.objsubid=0)") sql.AddLeft("pg_constraint c ON c.conrelid=rel.oid AND c.contype='p'") - sql.AddWhere("relkind", 'r') - sql.AddWhere("relnamespace", parentNode.parentNode.GetOid()) + sql.AddWhere("relkind ='%s'" % cls.relkind) + sql.AddWhere("relnamespace", cls.GetParentSchemaOid(parentNode)) sql.AddOrder("CASE WHEN nspname='%s' THEN ' ' else nspname END" % "public") sql.AddOrder("relname") return sql - def GetIcon(self): - icons=[] - icons.append("Table") + icons=[self.__class__.__name__] if self.GetOid() in self.GetDatabase().favourites: icons.append('fav') return self.GetImageId(icons) @@ -93,9 +102,18 @@ def GetProperties(self): (xlt("Persistence"), "%s (%s)" % (self.info['relpersistence'], xlt(persistenceStr.get(self.info['relpersistence'], "unknown")))), (xlt("Rows (estimated)"), int(self.info['reltuples'])), (xlt("Rows (counted)"), self.rowcount), - (xlt("ACL"), self.info['acl']) ] + if self.relkind == 'p': + if self.info['partstrat'] == 'r': + key="RANGE(%s)" % self.info['partattrs'] + else: + key=self.info['partexprs'] + self.AddProperty(xlt("Partition Key"), key) + elif self.relispartition: + self.AddProperty(xlt("Partition of Table"), self.GetPartitionMaster()) + self.AddProperty(xlt("Partition"), self.info['relpartbound']) + self.AddProperty(xlt("ACL"), self.info['acl']) self.AddProperty(xlt("Description"), self.info['description']) return self.properties @@ -134,6 +152,19 @@ def GetStatisticsQuery(self): 'cols': self.GetServer().ExpandColDefs(cols)} + def _getRefTables(self, col, refcol): + rels=self.GetCursor().ExecuteDictList(""" + SELECT relname AS name, nspname + FROM pg_depend + JOIN pg_class r ON r.oid=%s + JOIN pg_namespace n ON n.oid=relnamespace + WHERE classid=to_regclass('pg_class') + AND refclassid=to_regclass('pg_class') + AND deptype='a' AND relkind IN ('r', 'p') + AND %s=%s + """ % (refcol, col, self.info['oid'])) + return rels + def GetSql(self): self.populateColumns() cols=[] @@ -141,7 +172,6 @@ def GetSql(self): cols.append(quoteIdent(col['attname']) + ' ' + self.colTypeName(col)); constraints=[] - self.populateConstraints() for constraint in self.constraints: @@ -171,16 +201,26 @@ def GetSql(self): sql=[] - sql.append("CREATE TABLE " + self.NameSql()) - sql.append("("); - sql.append(" " + ",\n ".join(cols)) - if (self.info.get('relhasoids')): - sql.append(") WITH OIDs;") - else: - sql.append(");") - sql.append("") - sql.append("ALTER TABLE " + self.NameSql() + " OWNER TO " + quoteIdent(self.info['owner']) + ";") - sql.extend(constraints) + if self.relispartition: + sql.append("CREATE TABLE " + self.NameSql() + " PARTITION OF " + self.GetPartitionMaster()) + sql.append(" " + self.info['relpartbound']+";") + else: + pi="" + if (self.relkind == 'p'): + if self.info['partstrat'] == 'r': + pi=" PARTITION BY RANGE(%s)" % self.info['partattrs'] + else: + pi=" PARTITION BY %s" % self.info['partexprs'] + sql.append("CREATE TABLE " + self.NameSql()) + sql.append("("); + sql.append(" " + ",\n ".join(cols)) + if (self.info.get('relhasoids')): + sql.append(") WITH OIDs%s;" % pi) + else: + sql.append(")%s;" % pi) + sql.append("") + sql.append("ALTER TABLE " + self.NameSql() + " OWNER TO " + quoteIdent(self.info['owner']) + ";") + sql.extend(constraints) sql.extend(self.getAclDef('relacl', "arwdDxt")) sql.extend(self.getCommentDef()) return "\n".join(sql); @@ -549,7 +589,47 @@ def _getDetails(row): values.append( (con, icon) ) self.control.Fill(values, 'fullname') -nodeinfo= [ { "class" : Table, "parents": ["Schema"], "sort": 10, "collection": "Tables", "pages": [ColumnsPage, ConstraintPage, "StatisticsPage" , "SqlPage"] } ] +class Partition(Table): + relkind='r' + typename=xlt("Partition") + shortname=xlt("Partition") + relispartition=True + + @classmethod + def CheckPresent(cls, parentNode): + return parentNode.GetServer().version >= 10 + + def GetPartitionMaster(self): + if not hasattr(self, 'partitionMaster'): + rels=self._getRefTables('objid', 'refobjid') + self.partitionMaster= self.FullName(rels[0]) + return self.partitionMaster + + +class PartitionedTable(Table): + relkind='p' + typename=xlt("Partitioned Table") + shortname=xlt("Partitioned Table") + + @classmethod + def CheckPresent(cls, parentNode): + return parentNode.GetServer().version >= 10 + + def GetProperties(self): + if not len(self.properties): + super(PartitionedTable, self).GetProperties() + rels=self._getRefTables('refobjid', 'objid') + partitions=list(map(self.FullName, rels)) + self.AddProperty(xlt("Partitions"), partitions) + return self.properties + + + +nodeinfo= [ { "class" : Table, "parents": ["Schema"], "sort": 10, "collection": "Tables", "pages": [ColumnsPage, ConstraintPage, "StatisticsPage" , "SqlPage"] }, +# { "class" : Partition, "parents": ["Schema"], "sort": 11, "collection": "Partitions", "pages": [ColumnsPage, ConstraintPage, "StatisticsPage" , "SqlPage"] }, + { "class" : Partition, "parents": ["PartitionedTable"], "sort": 11, "pages": [ColumnsPage, ConstraintPage, "StatisticsPage" , "SqlPage"] }, + { "class" : PartitionedTable, "parents": ["Schema"], "sort": 12, "collection": "Partitioned Tables", "pages": [ColumnsPage, ConstraintPage, "StatisticsPage" , "SqlPage"] }, + ] pageinfo=[ColumnsPage, ConstraintPage] @@ -567,3 +647,5 @@ def OnExecute(_parentWin, node): menuinfo = [ { "class" : RowCount, "nodeclasses" : Table, 'sort': 80 }, ] + + diff --git a/modPg/_objects.py b/modPg/_objects.py index 0e525ba..2b516f2 100644 --- a/modPg/_objects.py +++ b/modPg/_objects.py @@ -213,4 +213,11 @@ def GetSchemaOid(self): oid=self.info.get('nspoid') return oid + @staticmethod + def GetParentSchemaOid(parentNode): + while parentNode: + if not isinstance(parentNode, Collection): + return parentNode.GetSchemaOid() + parentNode=parentNode.parentNode + \ No newline at end of file