Advertisement
johnmahugu

w2p-extract_oracle_models.py

May 21st, 2016
183
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.58 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. """Create web2py model (python code) to represent Oracle 11g tables.by john kesh mahugu
  5.  
  6. Features:
  7.  
  8. * Uses Oracle's metadata tables
  9. * Detects legacy "keyed" tables (not having an "id" PK)
  10. * Connects directly to running databases, no need to do a SQL dump
  11. * Handles notnull, unique and referential constraints
  12. * Detects most common datatypes and default values
  13. * Documents alternative datatypes as comments
  14.  
  15. Requeriments:
  16.  
  17. * Needs Oracle cx_Oracle python connector (same as web2py)
  18.  
  19.  
  20. based on extract_pgsql_models
  21. based in turn on a script to "generate schemas from dbs" (mysql)
  22.  
  23. """
  24.  
  25.  
  26.  
  27. HELP = """
  28. USAGE: extract_oracle_models db host port user passwd
  29.  
  30. Call with Oracle database connection parameters,
  31. web2py model will be printed on standard output.
  32.  
  33. EXAMPLE: python extract_oracle_models.py ORCL localhost 1521 user password
  34. """
  35.  
  36. # Config options
  37. DEBUG = False       # print debug messages to STDERR
  38.  
  39. # Constant for Field keyword parameter order (and filter):
  40. KWARGS = ('type', 'length', 'default', 'required', 'ondelete',
  41.           'notnull', 'unique', 'label', 'comment')
  42.  
  43.  
  44. import sys
  45.  
  46.  
  47. def query(conn, sql, *args):
  48.     "Execute a SQL query and return rows as a list of dicts"
  49.     cur = conn.cursor()
  50.     ret = []
  51.     try:
  52.         if DEBUG:
  53.             print >> sys.stderr, "QUERY: ", sql , args
  54.         cur.execute(sql, args)
  55.         for row in cur:
  56.             dic = {}
  57.             for i, value in enumerate(row):
  58.                 field = cur.description[i][0]
  59.                 dic[field] = value
  60.             if DEBUG:
  61.                 print >> sys.stderr, "RET: ", dic
  62.             ret.append(dic)
  63.         return ret
  64.     except cx_Oracle.DatabaseError, exc:
  65.         error, = exc.args
  66.         print >> sys.stderr, "Oracle-Error-Message:", error.message
  67.     finally:
  68.         cur.close()
  69.  
  70.  
  71. def get_tables(conn):
  72.     "List table names in a given schema"
  73.     rows = query(conn, """SELECT TABLE_NAME FROM USER_TABLES
  74.        ORDER BY TABLE_NAME""")
  75.     return [row['TABLE_NAME'] for row in rows]
  76.  
  77.  
  78. def get_fields(conn, table):
  79.     "Retrieve field list for a given table"
  80.     if DEBUG:
  81.         print >> sys.stderr, "Processing TABLE", table
  82.     rows = query(conn, """
  83.        SELECT COLUMN_NAME, DATA_TYPE,
  84.            NULLABLE AS IS_NULLABLE,
  85.            CHAR_LENGTH AS CHARACTER_MAXIMUM_LENGTH,
  86.            DATA_PRECISION AS NUMERIC_PRECISION,
  87.            DATA_SCALE AS NUMERIC_SCALE,
  88.            DATA_DEFAULT AS COLUMN_DEFAULT
  89.        FROM USER_TAB_COLUMNS
  90.        WHERE TABLE_NAME=:t
  91.        """, table)
  92.  
  93.     return rows
  94.  
  95.  
  96. def define_field(conn, table, field, pks):
  97.     "Determine field type, default value, references, etc."
  98.     f = {}
  99.     ref = references(conn, table, field['COLUMN_NAME'])
  100.     # Foreign Keys
  101.     if ref:
  102.         f.update(ref)
  103.     # PK & Numeric & autoincrement => id
  104.     elif field['COLUMN_NAME'] in pks and \
  105.             field['DATA_TYPE'] in ('INT', 'NUMBER') and \
  106.             is_autoincrement(conn, table, field):
  107.         f['type'] = "'id'"
  108.     # Other data types
  109.     elif field['DATA_TYPE'] in ('BINARY_DOUBLE'):
  110.         f['type'] = "'double'"
  111.     elif field['DATA_TYPE'] in ('CHAR','NCHAR'):
  112.         f['type'] = "'string'"
  113.         f['comment'] = "'Alternative types: boolean, time'"
  114.     elif field['DATA_TYPE'] in ('BLOB', 'CLOB'):
  115.         f['type'] = "'blob'"
  116.         f['comment'] = "'Alternative types: text, json, list:*'"
  117.     elif field['DATA_TYPE'] in ('DATE'):
  118.         f['type'] = "'datetime'"
  119.         f['comment'] = "'Alternative types: date'"
  120.     elif field['DATA_TYPE'] in ('FLOAT'):
  121.         f['type'] = "'float'"
  122.     elif field['DATA_TYPE'] in ('INT'):
  123.         f['type'] = "'integer'"
  124.     elif field['DATA_TYPE'] in ('NUMBER'):
  125.         f['type'] = "'bigint'"
  126.     elif field['DATA_TYPE'] in ('NUMERIC'):
  127.         f['type'] = "'decimal'"
  128.         f['precision'] = field['NUMERIC_PRECISION']
  129.         f['scale'] = field['NUMERIC_SCALE'] or 0
  130.     elif field['DATA_TYPE'] in ('VARCHAR2','NVARCHAR2'):
  131.         f['type'] = "'string'"
  132.         if field['CHARACTER_MAXIMUM_LENGTH']:
  133.             f['length'] = field['CHARACTER_MAXIMUM_LENGTH']
  134.         f['comment'] = "'Other possible types: password, upload'"
  135.     else:
  136.         f['type'] = "'blob'"
  137.         f['comment'] = "'WARNING: Oracle Data Type %s was not mapped." % \
  138.                 str(field['DATA_TYPE']) + " Using 'blob' as fallback.'"
  139.  
  140.     try:
  141.         if field['COLUMN_DEFAULT']:
  142.             if field['COLUMN_DEFAULT'] == "sysdate":
  143.                 d = "request.now"
  144.             elif field['COLUMN_DEFAULT'].upper() == "T":
  145.                 d = "True"
  146.             elif field['COLUMN_DEFAULT'].upper() == "F":
  147.                 d = "False"
  148.             else:
  149.                 d = repr(eval(field['COLUMN_DEFAULT']))
  150.             f['default'] = str(d)
  151.     except (ValueError, SyntaxError):
  152.         pass
  153.     except Exception, e:
  154.         raise RuntimeError(
  155.             "Default unsupported '%s'" % field['COLUMN_DEFAULT'])
  156.  
  157.     if not field['IS_NULLABLE']:
  158.         f['notnull'] = "True"
  159.  
  160.     return f
  161.  
  162.  
  163. def is_unique(conn, table, field):
  164.     "Find unique columns"
  165.     rows = query(conn, """
  166.        SELECT COLS.COLUMN_NAME
  167.        FROM USER_CONSTRAINTS CONS, ALL_CONS_COLUMNS COLS
  168.        WHERE CONS.OWNER = COLS.OWNER
  169.          AND CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
  170.          AND CONS.CONSTRAINT_TYPE = 'U'
  171.          AND COLS.TABLE_NAME = :t
  172.          AND COLS.COLUMN_NAME = :c
  173.        """, table, field['COLUMN_NAME'])
  174.     return rows and True or False
  175.  
  176.  
  177. # Returns True when a "BEFORE EACH ROW INSERT" trigger is found and:
  178. #  a) it mentions the "NEXTVAL" keyword (used by sequences)
  179. #  b) it operates on the given table and column
  180. #
  181. # On some (inelegant) database designs, SEQUENCE.NEXTVAL is called directly
  182. # from each "insert" statement, instead of using triggers. Such cases cannot
  183. # be detected by inspecting Oracle's metadata tables, as sequences are not
  184. # logically bound to any specific table or field.
  185. def is_autoincrement(conn, table, field):
  186.     "Find auto increment fields (best effort)"
  187.     rows = query(conn, """
  188.        SELECT TRIGGER_NAME
  189.        FROM USER_TRIGGERS,
  190.          (SELECT NAME, LISTAGG(TEXT, ' ') WITHIN GROUP (ORDER BY LINE) TEXT
  191.           FROM USER_SOURCE
  192.           WHERE TYPE = 'TRIGGER'
  193.           GROUP BY NAME
  194.          ) TRIGGER_DEFINITION
  195.        WHERE TRIGGER_NAME = NAME
  196.          AND TRIGGERING_EVENT = 'INSERT'
  197.          AND TRIGGER_TYPE = 'BEFORE EACH ROW'
  198.          AND TABLE_NAME = :t
  199.          AND UPPER(TEXT) LIKE UPPER('%.NEXTVAL%')
  200.          AND UPPER(TEXT) LIKE UPPER('%:NEW.' || :c || '%')
  201.        """, table, field['COLUMN_NAME'])
  202.     return rows and True or False
  203.  
  204.  
  205. def primarykeys(conn, table):
  206.     "Find primary keys"
  207.     rows = query(conn, """
  208.        SELECT COLS.COLUMN_NAME
  209.        FROM USER_CONSTRAINTS CONS, ALL_CONS_COLUMNS COLS
  210.        WHERE COLS.TABLE_NAME = :t
  211.            AND CONS.CONSTRAINT_TYPE = 'P'
  212.            AND CONS.OWNER = COLS.OWNER
  213.            AND CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
  214.        """, table)
  215.  
  216.     return [row['COLUMN_NAME'] for row in rows]
  217.  
  218.  
  219. def references(conn, table, field):
  220.     "Find a FK (fails if multiple)"
  221.     rows1 = query(conn, """
  222.        SELECT COLS.CONSTRAINT_NAME,
  223.            CONS.DELETE_RULE,
  224.            COLS.POSITION AS ORDINAL_POSITION
  225.        FROM USER_CONSTRAINTS CONS, ALL_CONS_COLUMNS COLS
  226.        WHERE COLS.TABLE_NAME = :t
  227.            AND COLS.COLUMN_NAME = :c
  228.            AND CONS.CONSTRAINT_TYPE = 'R'
  229.            AND CONS.OWNER = COLS.OWNER
  230.            AND CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
  231.          """, table, field)
  232.  
  233.     if len(rows1) == 1:
  234.         rows2 = query(conn, """
  235.                SELECT COLS.TABLE_NAME, COLS.COLUMN_NAME
  236.                FROM USER_CONSTRAINTS CONS, ALL_CONS_COLUMNS COLS
  237.                WHERE CONS.CONSTRAINT_NAME = :k
  238.                   AND CONS.R_CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
  239.                ORDER BY COLS.POSITION ASC
  240.            """, rows1[0]['CONSTRAINT_NAME'])
  241.  
  242.         row = None
  243.         if len(rows2) > 1:
  244.             row = rows2[int(rows1[0]['ORDINAL_POSITION']) - 1]
  245.             keyed = True
  246.         if len(rows2) == 1:
  247.             row = rows2[0]
  248.             keyed = False
  249.         if row:
  250.             if keyed:  # THIS IS BAD, DON'T MIX "id" and primarykey!!!
  251.                 ref = {'type': "'reference %s.%s'" % (row['TABLE_NAME'],
  252.                                                       row['COLUMN_NAME'])}
  253.             else:
  254.                 ref = {'type': "'reference %s'" % (row['TABLE_NAME'],)}
  255.             if rows1[0]['DELETE_RULE'] != "NO ACTION":
  256.                 ref['ondelete'] = repr(rows1[0]['DELETE_RULE'])
  257.             return ref
  258.         elif rows2:
  259.             raise RuntimeError("Unsupported foreign key reference: %s" %
  260.                                str(rows2))
  261.  
  262.     elif rows1:
  263.         raise RuntimeError("Unsupported referential constraint: %s" %
  264.                            str(rows1))
  265.  
  266.  
  267. def define_table(conn, table):
  268.     "Output single table definition"
  269.     fields = get_fields(conn, table)
  270.     pks = primarykeys(conn, table)
  271.     print "db.define_table('%s'," % (table, )
  272.     for field in fields:
  273.         fname = field['COLUMN_NAME']
  274.         fdef = define_field(conn, table, field, pks)
  275.         if fname not in pks and is_unique(conn, table, field):
  276.             fdef['unique'] = "True"
  277.         if fdef['type'] == "'id'" and fname in pks:
  278.             pks.pop(pks.index(fname))
  279.         print "    Field('%s', %s)," % (fname,
  280.                         ', '.join(["%s=%s" % (k, fdef[k]) for k in KWARGS
  281.                                    if k in fdef and fdef[k]]))
  282.     if pks:
  283.         print "    primarykey=[%s]," % ", ".join(["'%s'" % pk for pk in pks])
  284.     print     "    migrate=migrate)"
  285.     print
  286.  
  287.  
  288. def define_db(conn, db, host, port, user, passwd):
  289.     "Output database definition (model)"
  290.     dal = 'db = DAL("oracle://%s/%s@%s:%s/%s", pool_size=10)'
  291.     print dal % (user, passwd, host, port, db)
  292.     print
  293.     print "migrate = False"
  294.     print
  295.     for table in get_tables(conn):
  296.         define_table(conn, table)
  297.  
  298.  
  299. if __name__ == "__main__":
  300.     if len(sys.argv) < 6:
  301.         print HELP
  302.     else:
  303.         # Parse arguments from command line:
  304.         db, host, port, user, passwd = sys.argv[1:6]
  305.  
  306.         # Make the database connection (change driver if required)
  307.         import cx_Oracle
  308.         dsn = cx_Oracle.makedsn(host, port, db)
  309.         cnn = cx_Oracle.connect(user, passwd, dsn)
  310.         # Start model code generation:
  311.         define_db(cnn, db, host, port, user, passwd)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement