summaryrefslogtreecommitdiff
path: root/python/skytools/dbstruct.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/skytools/dbstruct.py')
-rw-r--r--python/skytools/dbstruct.py380
1 files changed, 380 insertions, 0 deletions
diff --git a/python/skytools/dbstruct.py b/python/skytools/dbstruct.py
new file mode 100644
index 00000000..22333429
--- /dev/null
+++ b/python/skytools/dbstruct.py
@@ -0,0 +1,380 @@
+"""Find table structure and allow CREATE/DROP elements from it.
+"""
+
+import sys, re
+
+from sqltools import fq_name_parts, get_table_oid
+
+__all__ = ['TableStruct',
+ 'T_TABLE', 'T_CONSTRAINT', 'T_INDEX', 'T_TRIGGER',
+ 'T_RULE', 'T_GRANT', 'T_OWNER', 'T_PKEY', 'T_ALL']
+
+T_TABLE = 1 << 0
+T_CONSTRAINT = 1 << 1
+T_INDEX = 1 << 2
+T_TRIGGER = 1 << 3
+T_RULE = 1 << 4
+T_GRANT = 1 << 5
+T_OWNER = 1 << 6
+T_PKEY = 1 << 20 # special, one of constraints
+T_ALL = ( T_TABLE | T_CONSTRAINT | T_INDEX
+ | T_TRIGGER | T_RULE | T_GRANT | T_OWNER )
+
+#
+# Utility functions
+#
+
+def find_new_name(curs, name):
+ """Create new object name for case the old exists.
+
+ Needed when creating a new table besides old one.
+ """
+ # cut off previous numbers
+ m = re.search('_[0-9]+$', name)
+ if m:
+ name = name[:m.start()]
+
+ # now loop
+ for i in range(1, 1000):
+ tname = "%s_%d" % (name, i)
+ q = "select count(1) from pg_class where relname = %s"
+ curs.execute(q, [tname])
+ if curs.fetchone()[0] == 0:
+ return tname
+
+ # failed
+ raise Exception('find_new_name failed')
+
+def rx_replace(rx, sql, new_part):
+ """Find a regex match and replace that part with new_part."""
+ m = re.search(rx, sql, re.I)
+ if not m:
+ raise Exception('rx_replace failed')
+ p1 = sql[:m.start()]
+ p2 = sql[m.end():]
+ return p1 + new_part + p2
+
+#
+# Schema objects
+#
+
+class TElem(object):
+ """Keeps info about one metadata object."""
+ SQL = ""
+ type = 0
+ def get_create_sql(self, curs):
+ """Return SQL statement for creating or None if not supported."""
+ return None
+ def get_drop_sql(self, curs):
+ """Return SQL statement for dropping or None of not supported."""
+ return None
+
+class TConstraint(TElem):
+ """Info about constraint."""
+ type = T_CONSTRAINT
+ SQL = """
+ SELECT conname as name, pg_get_constraintdef(oid) as def, contype
+ FROM pg_constraint WHERE conrelid = %(oid)s
+ """
+ def __init__(self, table_name, row):
+ self.table_name = table_name
+ self.name = row['name']
+ self.defn = row['def']
+ self.contype = row['contype']
+
+ # tag pkeys
+ if self.contype == 'p':
+ self.type += T_PKEY
+
+ def get_create_sql(self, curs, new_table_name=None):
+ fmt = "ALTER TABLE ONLY %s ADD CONSTRAINT %s %s;"
+ if new_table_name:
+ name = self.name
+ if self.contype in ('p', 'u'):
+ name = find_new_name(curs, self.name)
+ sql = fmt % (new_table_name, name, self.defn)
+ else:
+ sql = fmt % (self.table_name, self.name, self.defn)
+ return sql
+
+ def get_drop_sql(self, curs):
+ fmt = "ALTER TABLE ONLY %s DROP CONSTRAINT %s;"
+ sql = fmt % (self.table_name, self.name)
+ return sql
+
+class TIndex(TElem):
+ """Info about index."""
+ type = T_INDEX
+ SQL = """
+ SELECT n.nspname || '.' || c.relname as name,
+ pg_get_indexdef(i.indexrelid) as defn
+ FROM pg_index i, pg_class c, pg_namespace n
+ WHERE c.oid = i.indexrelid AND i.indrelid = %(oid)s
+ AND n.oid = c.relnamespace
+ AND NOT EXISTS
+ (select objid from pg_depend
+ where classid = %(pg_class_oid)s
+ and objid = c.oid
+ and deptype = 'i')
+ """
+ def __init__(self, table_name, row):
+ self.name = row['name']
+ self.defn = row['defn'] + ';'
+
+ def get_create_sql(self, curs, new_table_name = None):
+ if not new_table_name:
+ return self.defn
+ name = find_new_name(curs, self.name)
+ pnew = "INDEX %s ON %s " % (name, new_table_name)
+ rx = r"\bINDEX[ ][a-z0-9._]+[ ]ON[ ][a-z0-9._]+[ ]"
+ sql = rx_replace(rx, self.defn, pnew)
+ return sql
+ def get_drop_sql(self, curs):
+ return 'DROP INDEX %s;' % self.name
+
+class TRule(TElem):
+ """Info about rule."""
+ type = T_RULE
+ SQL = """
+ SELECT rulename as name, pg_get_ruledef(oid) as def
+ FROM pg_rewrite
+ WHERE ev_class = %(oid)s AND rulename <> '_RETURN'::name
+ """
+ def __init__(self, table_name, row, new_name = None):
+ self.table_name = table_name
+ self.name = row['name']
+ self.defn = row['def']
+
+ def get_create_sql(self, curs, new_table_name = None):
+ if not new_table_name:
+ return self.defn
+ rx = r"\bTO[ ][a-z0-9._]+[ ]DO[ ]"
+ pnew = "TO %s DO " % new_table_name
+ return rx_replace(rx, self.defn, pnew)
+
+ def get_drop_sql(self, curs):
+ return 'DROP RULE %s ON %s' % (self.name, self.table_name)
+
+class TTrigger(TElem):
+ """Info about trigger."""
+ type = T_TRIGGER
+ SQL = """
+ SELECT tgname as name, pg_get_triggerdef(oid) as def
+ FROM pg_trigger
+ WHERE tgrelid = %(oid)s AND NOT tgisconstraint
+ """
+ def __init__(self, table_name, row):
+ self.table_name = table_name
+ self.name = row['name']
+ self.defn = row['def'] + ';'
+
+ def get_create_sql(self, curs, new_table_name = None):
+ if not new_table_name:
+ return self.defn
+
+ rx = r"\bON[ ][a-z0-9._]+[ ]"
+ pnew = "ON %s " % new_table_name
+ return rx_replace(rx, self.defn, pnew)
+
+ def get_drop_sql(self, curs):
+ return 'DROP TRIGGER %s ON %s' % (self.name, self.table_name)
+
+class TOwner(TElem):
+ """Info about table owner."""
+ type = T_OWNER
+ SQL = """
+ SELECT pg_get_userbyid(relowner) as owner FROM pg_class
+ WHERE oid = %(oid)s
+ """
+ def __init__(self, table_name, row, new_name = None):
+ self.table_name = table_name
+ self.name = 'Owner'
+ self.owner = row['owner']
+
+ def get_create_sql(self, curs, new_name = None):
+ if not new_name:
+ new_name = self.table_name
+ return 'ALTER TABLE %s OWNER TO %s;' % (new_name, self.owner)
+
+class TGrant(TElem):
+ """Info about permissions."""
+ type = T_GRANT
+ SQL = "SELECT relacl FROM pg_class where oid = %(oid)s"
+ acl_map = {
+ 'r': 'SELECT', 'w': 'UPDATE', 'a': 'INSERT', 'd': 'DELETE',
+ 'R': 'RULE', 'x': 'REFERENCES', 't': 'TRIGGER', 'X': 'EXECUTE',
+ 'U': 'USAGE', 'C': 'CREATE', 'T': 'TEMPORARY'
+ }
+ def acl_to_grants(self, acl):
+ if acl == "arwdRxt": # ALL for tables
+ return "ALL"
+ return ", ".join([ self.acl_map[c] for c in acl ])
+
+ def parse_relacl(self, relacl):
+ if relacl is None:
+ return []
+ if len(relacl) > 0 and relacl[0] == '{' and relacl[-1] == '}':
+ relacl = relacl[1:-1]
+ list = []
+ for f in relacl.split(','):
+ user, tmp = f.strip('"').split('=')
+ acl, who = tmp.split('/')
+ list.append((user, acl, who))
+ return list
+
+ def __init__(self, table_name, row, new_name = None):
+ self.name = table_name
+ self.acl_list = self.parse_relacl(row['relacl'])
+
+ def get_create_sql(self, curs, new_name = None):
+ if not new_name:
+ new_name = self.name
+
+ list = []
+ for user, acl, who in self.acl_list:
+ astr = self.acl_to_grants(acl)
+ sql = "GRANT %s ON %s TO %s;" % (astr, new_name, user)
+ list.append(sql)
+ return "\n".join(list)
+
+ def get_drop_sql(self, curs):
+ list = []
+ for user, acl, who in self.acl_list:
+ sql = "REVOKE ALL FROM %s ON %s;" % (user, self.name)
+ list.append(sql)
+ return "\n".join(list)
+
+class TColumn(TElem):
+ """Info about table column."""
+ SQL = """
+ select a.attname as name,
+ a.attname || ' '
+ || format_type(a.atttypid, a.atttypmod)
+ || case when a.attnotnull then ' not null' else '' end
+ || case when a.atthasdef then ' ' || d.adsrc else '' end
+ as def
+ from pg_attribute a left join pg_attrdef d
+ on (d.adrelid = a.attrelid and d.adnum = a.attnum)
+ where a.attrelid = %(oid)s
+ and not a.attisdropped
+ and a.attnum > 0
+ order by a.attnum;
+ """
+ def __init__(self, table_name, row):
+ self.name = row['name']
+ self.column_def = row['def']
+
+class TTable(TElem):
+ """Info about table only (columns)."""
+ type = T_TABLE
+ def __init__(self, table_name, col_list):
+ self.name = table_name
+ self.col_list = col_list
+
+ def get_create_sql(self, curs, new_name = None):
+ if not new_name:
+ new_name = self.name
+ sql = "create table %s (" % new_name
+ sep = "\n\t"
+ for c in self.col_list:
+ sql += sep + c.column_def
+ sep = ",\n\t"
+ sql += "\n);"
+ return sql
+
+ def get_drop_sql(self, curs):
+ return "DROP TABLE %s;" % self.name
+
+#
+# Main table object, loads all the others
+#
+
+class TableStruct(object):
+ """Collects and manages all info about table.
+
+ Allow to issue CREATE/DROP statements about any
+ group of elements.
+ """
+ def __init__(self, curs, table_name):
+ """Initializes class by loading info about table_name from database."""
+
+ self.table_name = table_name
+
+ # fill args
+ schema, name = fq_name_parts(table_name)
+ args = {
+ 'schema': schema,
+ 'table': name,
+ 'oid': get_table_oid(curs, table_name),
+ 'pg_class_oid': get_table_oid(curs, 'pg_catalog.pg_class'),
+ }
+
+ # load table struct
+ self.col_list = self._load_elem(curs, args, TColumn)
+ self.object_list = [ TTable(table_name, self.col_list) ]
+
+ # load additional objects
+ to_load = [TConstraint, TIndex, TTrigger, TRule, TGrant, TOwner]
+ for eclass in to_load:
+ self.object_list += self._load_elem(curs, args, eclass)
+
+ def _load_elem(self, curs, args, eclass):
+ list = []
+ curs.execute(eclass.SQL % args)
+ for row in curs.dictfetchall():
+ list.append(eclass(self.table_name, row))
+ return list
+
+ def create(self, curs, objs, new_table_name = None, log = None):
+ """Issues CREATE statements for requested set of objects.
+
+ If new_table_name is giver, creates table under that name
+ and also tries to rename all indexes/constraints that conflict
+ with existing table.
+ """
+
+ for o in self.object_list:
+ if o.type & objs:
+ sql = o.get_create_sql(curs, new_table_name)
+ if not sql:
+ continue
+ if log:
+ log.info('Creating %s' % o.name)
+ log.debug(sql)
+ curs.execute(sql)
+
+ def drop(self, curs, objs, log = None):
+ """Issues DROP statements for requested set of objects."""
+ for o in self.object_list:
+ if o.type & objs:
+ sql = o.get_drop_sql(curs)
+ if not sql:
+ continue
+ if log:
+ log.info('Dropping %s' % o.name)
+ log.debug(sql)
+ curs.execute(sql)
+
+ def get_column_list(self):
+ """Returns list of column names the table has."""
+
+ res = []
+ for c in self.col_list:
+ res.append(c.name)
+ return res
+
+def test():
+ import psycopg
+ db = psycopg.connect("dbname=fooz")
+ curs = db.cursor()
+
+ s = TableStruct(curs, "public.data1")
+
+ s.drop(curs, T_ALL)
+ s.create(curs, T_ALL)
+ s.create(curs, T_ALL, "data1_new")
+ s.create(curs, T_PKEY)
+
+if __name__ == '__main__':
+ test()
+