1 """Find table structure and allow CREATE/DROP elements from it.
2 """
3
4 import sys, re
5
6 from sqltools import fq_name_parts, get_table_oid
7
8 __all__ = ['TableStruct',
9 'T_TABLE', 'T_CONSTRAINT', 'T_INDEX', 'T_TRIGGER',
10 'T_RULE', 'T_GRANT', 'T_OWNER', 'T_PKEY', 'T_ALL']
11
12 T_TABLE = 1 << 0
13 T_CONSTRAINT = 1 << 1
14 T_INDEX = 1 << 2
15 T_TRIGGER = 1 << 3
16 T_RULE = 1 << 4
17 T_GRANT = 1 << 5
18 T_OWNER = 1 << 6
19 T_PKEY = 1 << 20
20 T_ALL = ( T_TABLE | T_CONSTRAINT | T_INDEX
21 | T_TRIGGER | T_RULE | T_GRANT | T_OWNER )
22
23
24
25
26
28 """Create new object name for case the old exists.
29
30 Needed when creating a new table besides old one.
31 """
32
33 m = re.search('_[0-9]+$', name)
34 if m:
35 name = name[:m.start()]
36
37
38 for i in range(1, 1000):
39 tname = "%s_%d" % (name, i)
40 q = "select count(1) from pg_class where relname = %s"
41 curs.execute(q, [tname])
42 if curs.fetchone()[0] == 0:
43 return tname
44
45
46 raise Exception('find_new_name failed')
47
49 """Find a regex match and replace that part with new_part."""
50 m = re.search(rx, sql, re.I)
51 if not m:
52 raise Exception('rx_replace failed')
53 p1 = sql[:m.start()]
54 p2 = sql[m.end():]
55 return p1 + new_part + p2
56
57
58
59
60
62 """Keeps info about one metadata object."""
63 SQL = ""
64 type = 0
66 """Return SQL statement for creating or None if not supported."""
67 return None
69 """Return SQL statement for dropping or None of not supported."""
70 return None
71
73 """Info about constraint."""
74 type = T_CONSTRAINT
75 SQL = """
76 SELECT conname as name, pg_get_constraintdef(oid) as def, contype
77 FROM pg_constraint WHERE conrelid = %(oid)s
78 """
80 self.table_name = table_name
81 self.name = row['name']
82 self.defn = row['def']
83 self.contype = row['contype']
84
85
86 if self.contype == 'p':
87 self.type += T_PKEY
88
90 fmt = "ALTER TABLE ONLY %s ADD CONSTRAINT %s %s;"
91 if new_table_name:
92 name = self.name
93 if self.contype in ('p', 'u'):
94 name = find_new_name(curs, self.name)
95 sql = fmt % (new_table_name, name, self.defn)
96 else:
97 sql = fmt % (self.table_name, self.name, self.defn)
98 return sql
99
101 fmt = "ALTER TABLE ONLY %s DROP CONSTRAINT %s;"
102 sql = fmt % (self.table_name, self.name)
103 return sql
104
106 """Info about index."""
107 type = T_INDEX
108 SQL = """
109 SELECT n.nspname || '.' || c.relname as name,
110 pg_get_indexdef(i.indexrelid) as defn
111 FROM pg_index i, pg_class c, pg_namespace n
112 WHERE c.oid = i.indexrelid AND i.indrelid = %(oid)s
113 AND n.oid = c.relnamespace
114 AND NOT EXISTS
115 (select objid from pg_depend
116 where classid = %(pg_class_oid)s
117 and objid = c.oid
118 and deptype = 'i')
119 """
121 self.name = row['name']
122 self.defn = row['defn'] + ';'
123
125 if not new_table_name:
126 return self.defn
127 name = find_new_name(curs, self.name)
128 pnew = "INDEX %s ON %s " % (name, new_table_name)
129 rx = r"\bINDEX[ ][a-z0-9._]+[ ]ON[ ][a-z0-9._]+[ ]"
130 sql = rx_replace(rx, self.defn, pnew)
131 return sql
133 return 'DROP INDEX %s;' % self.name
134
136 """Info about rule."""
137 type = T_RULE
138 SQL = """
139 SELECT rulename as name, pg_get_ruledef(oid) as def
140 FROM pg_rewrite
141 WHERE ev_class = %(oid)s AND rulename <> '_RETURN'::name
142 """
143 - def __init__(self, table_name, row, new_name = None):
144 self.table_name = table_name
145 self.name = row['name']
146 self.defn = row['def']
147
149 if not new_table_name:
150 return self.defn
151 rx = r"\bTO[ ][a-z0-9._]+[ ]DO[ ]"
152 pnew = "TO %s DO " % new_table_name
153 return rx_replace(rx, self.defn, pnew)
154
156 return 'DROP RULE %s ON %s' % (self.name, self.table_name)
157
159 """Info about trigger."""
160 type = T_TRIGGER
161 SQL = """
162 SELECT tgname as name, pg_get_triggerdef(oid) as def
163 FROM pg_trigger
164 WHERE tgrelid = %(oid)s AND NOT tgisconstraint
165 """
167 self.table_name = table_name
168 self.name = row['name']
169 self.defn = row['def'] + ';'
170
172 if not new_table_name:
173 return self.defn
174
175 rx = r"\bON[ ][a-z0-9._]+[ ]"
176 pnew = "ON %s " % new_table_name
177 return rx_replace(rx, self.defn, pnew)
178
180 return 'DROP TRIGGER %s ON %s' % (self.name, self.table_name)
181
183 """Info about table owner."""
184 type = T_OWNER
185 SQL = """
186 SELECT pg_get_userbyid(relowner) as owner FROM pg_class
187 WHERE oid = %(oid)s
188 """
189 - def __init__(self, table_name, row, new_name = None):
190 self.table_name = table_name
191 self.name = 'Owner'
192 self.owner = row['owner']
193
195 if not new_name:
196 new_name = self.table_name
197 return 'ALTER TABLE %s OWNER TO %s;' % (new_name, self.owner)
198
200 """Info about permissions."""
201 type = T_GRANT
202 SQL = "SELECT relacl FROM pg_class where oid = %(oid)s"
203 acl_map = {
204 'r': 'SELECT', 'w': 'UPDATE', 'a': 'INSERT', 'd': 'DELETE',
205 'R': 'RULE', 'x': 'REFERENCES', 't': 'TRIGGER', 'X': 'EXECUTE',
206 'U': 'USAGE', 'C': 'CREATE', 'T': 'TEMPORARY'
207 }
209 if acl == "arwdRxt":
210 return "ALL"
211 return ", ".join([ self.acl_map[c] for c in acl ])
212
214 if relacl is None:
215 return []
216 if len(relacl) > 0 and relacl[0] == '{' and relacl[-1] == '}':
217 relacl = relacl[1:-1]
218 list = []
219 for f in relacl.split(','):
220 user, tmp = f.strip('"').split('=')
221 acl, who = tmp.split('/')
222 list.append((user, acl, who))
223 return list
224
225 - def __init__(self, table_name, row, new_name = None):
226 self.name = table_name
227 self.acl_list = self.parse_relacl(row['relacl'])
228
230 if not new_name:
231 new_name = self.name
232
233 list = []
234 for user, acl, who in self.acl_list:
235 astr = self.acl_to_grants(acl)
236 sql = "GRANT %s ON %s TO %s;" % (astr, new_name, user)
237 list.append(sql)
238 return "\n".join(list)
239
241 list = []
242 for user, acl, who in self.acl_list:
243 sql = "REVOKE ALL FROM %s ON %s;" % (user, self.name)
244 list.append(sql)
245 return "\n".join(list)
246
248 """Info about table column."""
249 SQL = """
250 select a.attname as name,
251 a.attname || ' '
252 || format_type(a.atttypid, a.atttypmod)
253 || case when a.attnotnull then ' not null' else '' end
254 || case when a.atthasdef then ' ' || d.adsrc else '' end
255 as def
256 from pg_attribute a left join pg_attrdef d
257 on (d.adrelid = a.attrelid and d.adnum = a.attnum)
258 where a.attrelid = %(oid)s
259 and not a.attisdropped
260 and a.attnum > 0
261 order by a.attnum;
262 """
264 self.name = row['name']
265 self.column_def = row['def']
266
268 """Info about table only (columns)."""
269 type = T_TABLE
270 - def __init__(self, table_name, col_list):
271 self.name = table_name
272 self.col_list = col_list
273
275 if not new_name:
276 new_name = self.name
277 sql = "create table %s (" % new_name
278 sep = "\n\t"
279 for c in self.col_list:
280 sql += sep + c.column_def
281 sep = ",\n\t"
282 sql += "\n);"
283 return sql
284
286 return "DROP TABLE %s;" % self.name
287
288
289
290
291
293 """Collects and manages all info about table.
294
295 Allow to issue CREATE/DROP statements about any
296 group of elements.
297 """
299 """Initializes class by loading info about table_name from database."""
300
301 self.table_name = table_name
302
303
304 schema, name = fq_name_parts(table_name)
305 args = {
306 'schema': schema,
307 'table': name,
308 'oid': get_table_oid(curs, table_name),
309 'pg_class_oid': get_table_oid(curs, 'pg_catalog.pg_class'),
310 }
311
312
313 self.col_list = self._load_elem(curs, args, TColumn)
314 self.object_list = [ TTable(table_name, self.col_list) ]
315
316
317 to_load = [TConstraint, TIndex, TTrigger, TRule, TGrant, TOwner]
318 for eclass in to_load:
319 self.object_list += self._load_elem(curs, args, eclass)
320
322 list = []
323 curs.execute(eclass.SQL % args)
324 for row in curs.dictfetchall():
325 list.append(eclass(self.table_name, row))
326 return list
327
328 - def create(self, curs, objs, new_table_name = None, log = None):
329 """Issues CREATE statements for requested set of objects.
330
331 If new_table_name is giver, creates table under that name
332 and also tries to rename all indexes/constraints that conflict
333 with existing table.
334 """
335
336 for o in self.object_list:
337 if o.type & objs:
338 sql = o.get_create_sql(curs, new_table_name)
339 if not sql:
340 continue
341 if log:
342 log.info('Creating %s' % o.name)
343 log.debug(sql)
344 curs.execute(sql)
345
346 - def drop(self, curs, objs, log = None):
347 """Issues DROP statements for requested set of objects."""
348 for o in self.object_list:
349 if o.type & objs:
350 sql = o.get_drop_sql(curs)
351 if not sql:
352 continue
353 if log:
354 log.info('Dropping %s' % o.name)
355 log.debug(sql)
356 curs.execute(sql)
357
359 """Returns list of column names the table has."""
360
361 res = []
362 for c in self.col_list:
363 res.append(c.name)
364 return res
365
377
378 if __name__ == '__main__':
379 test()
380