export_pg_schema.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. # -*- coding: utf-8 -*-
  2. """导出 PostgreSQL 的建表结构(无数据)。
  3. 本机没有 pg_dump,因此用系统目录 + pg_get_*def 生成等价 DDL。
  4. 用法:python docs/db/export_pg_schema.py [输出文件]
  5. """
  6. import datetime
  7. import os
  8. import sys
  9. import psycopg2
  10. DB = dict(host="192.168.0.106", port=5432, dbname="zsjz-ai",
  11. user="postgres", password="postgres")
  12. DEFAULT_OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
  13. "zsjz-ai-schema-20261028.sql")
  14. def ident(name):
  15. return '"' + str(name).replace('"', '""') + '"'
  16. def lit(value):
  17. return "'" + str(value).replace("'", "''") + "'"
  18. def seq_opts(cur, seq_oid):
  19. cur.execute("""select seqtypid::regtype::text, seqstart, seqincrement, seqmin, seqmax,
  20. seqcache, seqcycle
  21. from pg_sequence where seqrelid=%s""", (seq_oid,))
  22. row = cur.fetchone()
  23. if not row:
  24. return ""
  25. typ, start, inc, mn, mx, cache, cycle = row
  26. parts = []
  27. if typ not in ("bigint", "integer", "smallint"):
  28. parts.append("AS " + typ)
  29. parts.append("START WITH %s" % start)
  30. parts.append("INCREMENT BY %s" % inc)
  31. if (inc > 0 and mn == 1) or (inc < 0 and mn == -(2 ** 63)):
  32. parts.append("NO MINVALUE")
  33. else:
  34. parts.append("MINVALUE %s" % mn)
  35. if (inc > 0 and mx == 2 ** 63 - 1) or (inc < 0 and mx == -1):
  36. parts.append("NO MAXVALUE")
  37. else:
  38. parts.append("MAXVALUE %s" % mx)
  39. parts.append("CACHE %s" % cache)
  40. if cycle:
  41. parts.append("CYCLE")
  42. return " ".join(parts)
  43. def main():
  44. out_path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_OUT
  45. conn = psycopg2.connect(**DB)
  46. cur = conn.cursor()
  47. def q(sql, args=None):
  48. cur.execute(sql, args)
  49. return cur.fetchall()
  50. lines = []
  51. lines.append("-- PostgreSQL database schema dump (structure only, no data)")
  52. lines.append("-- Database: " + DB["dbname"])
  53. lines.append("-- Server: " + q("select version()")[0][0])
  54. lines.append("-- Generated: " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  55. lines.append("-- Note: generated from PostgreSQL catalogs because pg_dump is not installed on this host.")
  56. lines.append("")
  57. lines.append("SET client_encoding = 'UTF8';")
  58. lines.append("SET standard_conforming_strings = on;")
  59. lines.append("SET search_path = public, pg_catalog;")
  60. lines.append("")
  61. lines.append("-- Extensions")
  62. for ext, _ver in q("select extname, extversion from pg_extension "
  63. "where extname <> 'plpgsql' order by 1"):
  64. lines.append("CREATE EXTENSION IF NOT EXISTS %s;" % ident(ext))
  65. lines.append("")
  66. # 序列:identity 序列由列内联创建,其余(serial)先创建再建表
  67. seqs = q("""select c.relname, c.oid from pg_class c
  68. join pg_namespace n on n.oid=c.relnamespace
  69. where n.nspname='public' and c.relkind='S' order by c.relname""")
  70. identity_seq_oids = {r[0] for r in q("""select d.objid from pg_depend d
  71. where d.classid='pg_class'::regclass and d.refclassid='pg_class'::regclass
  72. and d.deptype='i'""")}
  73. serial_seq_names = []
  74. if seqs:
  75. lines.append("-- Sequences (serial columns; identity sequences are created inline)")
  76. for name, oid in seqs:
  77. if oid in identity_seq_oids:
  78. continue
  79. serial_seq_names.append(name)
  80. lines.append("CREATE SEQUENCE public.%s" % ident(name))
  81. lines.append(" %s;" % seq_opts(cur, oid))
  82. lines.append("")
  83. tables = [r[0] for r in q("""select c.relname from pg_class c
  84. join pg_namespace n on n.oid=c.relnamespace
  85. where n.nspname='public' and c.relkind='r' order by c.relname""")]
  86. lines.append("-- Tables")
  87. for tname in tables:
  88. cols = q("""select a.attname, format_type(a.atttypid,a.atttypmod), a.attnotnull,
  89. a.attidentity, a.attgenerated,
  90. pg_get_expr(ad.adbin,ad.adrelid) as def,
  91. a.attnum
  92. from pg_attribute a
  93. left join pg_attrdef ad on ad.adrelid=a.attrelid and ad.adnum=a.attnum
  94. where a.attrelid=%s::regclass and a.attnum>0 and not a.attisdropped
  95. order by a.attnum""", ("public." + ident(tname),))
  96. lines.append("CREATE TABLE public.%s (" % ident(tname))
  97. col_lines = []
  98. for attname, typ, notnull, identity, generated, default, attnum in cols:
  99. s = " %s %s" % (ident(attname), typ)
  100. if generated == "s":
  101. s += " GENERATED ALWAYS AS (%s) STORED" % default
  102. elif identity in ("d", "a"):
  103. seq = q("""select s.relname, s.oid from pg_depend d
  104. join pg_class s on s.oid=d.objid
  105. where d.classid='pg_class'::regclass and d.refclassid='pg_class'::regclass
  106. and d.refobjid=%s::regclass and d.refobjsubid=%s and d.deptype='i'""",
  107. ("public." + ident(tname), attnum))
  108. if seq:
  109. sname, soid = seq[0]
  110. s += " GENERATED %s AS IDENTITY (SEQUENCE NAME public.%s %s)" % (
  111. "ALWAYS" if identity == "a" else "BY DEFAULT",
  112. ident(sname), seq_opts(cur, soid))
  113. else:
  114. s += " GENERATED %s AS IDENTITY" % (
  115. "ALWAYS" if identity == "a" else "BY DEFAULT")
  116. elif default is not None:
  117. s += " DEFAULT " + default
  118. if notnull:
  119. s += " NOT NULL"
  120. col_lines.append(s)
  121. lines.append(",\n".join(col_lines))
  122. lines.append(");")
  123. lines.append("")
  124. lines.append("-- Constraints")
  125. cons = q("""select c.relname, con.conname, con.contype,
  126. pg_get_constraintdef(con.oid), con.convalidated
  127. from pg_constraint con join pg_class c on c.oid=con.conrelid
  128. join pg_namespace n on n.oid=c.relnamespace
  129. where n.nspname='public' and con.contype in ('p','u','f','c')
  130. order by c.relname, con.conname""")
  131. # FK 依赖被引用表的主键/唯一约束:先建 PK/UNIQUE,再建 FK/CHECK
  132. cons.sort(key=lambda x: (0 if x[2] in ("p", "u") else 1, x[0], x[1]))
  133. for tname, cname, ctype, cdef, valid in cons:
  134. s = "ALTER TABLE ONLY public.%s ADD CONSTRAINT %s %s" % (
  135. ident(tname), ident(cname), cdef)
  136. if not valid:
  137. s += " NOT VALID"
  138. lines.append(s + ";")
  139. lines.append("")
  140. lines.append("-- Indexes (excluding constraint-backed indexes)")
  141. for ddl, _t, _i in q("""select pg_get_indexdef(i.indexrelid), c.relname, ic.relname
  142. from pg_index i join pg_class c on c.oid=i.indrelid
  143. join pg_class ic on ic.oid=i.indexrelid
  144. join pg_namespace n on n.oid=c.relnamespace
  145. left join pg_constraint con on con.conindid=i.indexrelid
  146. where n.nspname='public' and con.oid is null
  147. order by c.relname, ic.relname"""):
  148. lines.append(ddl + ";")
  149. lines.append("")
  150. lines.append("-- Sequence ownership")
  151. for name in serial_seq_names:
  152. row = q("""select d.refobjid::regclass::text, a.attname from pg_depend d
  153. join pg_class s on s.oid=d.objid
  154. join pg_attribute a on a.attrelid=d.refobjid and a.attnum=d.refobjsubid
  155. where d.classid='pg_class'::regclass and d.refclassid='pg_class'::regclass
  156. and d.deptype='a' and s.relname=%s""", (name,))
  157. if row:
  158. tbl, col = row[0]
  159. lines.append("ALTER SEQUENCE public.%s OWNED BY %s.%s;" % (
  160. ident(name), tbl, ident(col)))
  161. lines.append("")
  162. lines.append("-- Comments")
  163. for tname in tables:
  164. row = q("select obj_description(%s::regclass,'pg_class')", ("public." + ident(tname),))
  165. if row and row[0][0]:
  166. lines.append("COMMENT ON TABLE public.%s IS %s;" % (ident(tname), lit(row[0][0])))
  167. for attname, comment in q("""select a.attname, col_description(a.attrelid,a.attnum)
  168. from pg_attribute a
  169. where a.attrelid=%s::regclass and a.attnum>0
  170. and not a.attisdropped
  171. and col_description(a.attrelid,a.attnum) is not null
  172. order by a.attnum""", ("public." + ident(tname),)):
  173. lines.append("COMMENT ON COLUMN public.%s.%s IS %s;" % (
  174. ident(tname), ident(attname), lit(comment)))
  175. lines.append("")
  176. with open(out_path, "w", encoding="utf-8", newline="\n") as f:
  177. f.write("\n".join(lines).rstrip() + "\n")
  178. conn.close()
  179. print("written %s (%d bytes, %d lines)" % (out_path, os.path.getsize(out_path), len(lines)))
  180. if __name__ == "__main__":
  181. main()