From d9401b78f11b47c5daa95a4fa60925b3fed50203 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 2 Aug 2005 17:08:24 +0000 Subject: [PATCH] Added first stab at 'django-admin.py inspectdb', which takes a database name and introspects the tables, outputting a Django model. Works in PostgreSQL and MySQL. It's missing some niceties at the moment, such as detection of primary-keys and relationships, but it works. Refs #90. git-svn-id: http://code.djangoproject.com/svn/django/trunk@384 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/bin/django-admin.py | 12 ++++++++++++ django/core/db/__init__.py | 2 ++ django/core/db/backends/mysql.py | 25 +++++++++++++++++++++++++ django/core/db/backends/postgresql.py | 27 +++++++++++++++++++++++++++ django/core/db/backends/sqlite3.py | 19 ++++++++++++------- django/core/management.py | 26 ++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 7 deletions(-) diff --git a/django/bin/django-admin.py b/django/bin/django-admin.py index 9acb5cc2cf..8c4370f427 100755 --- a/django/bin/django-admin.py +++ b/django/bin/django-admin.py @@ -7,6 +7,7 @@ ACTION_MAPPING = { 'adminindex': management.get_admin_index, 'createsuperuser': management.createsuperuser, # 'dbcheck': management.database_check, + 'inspectdb': management.inspectdb, 'runserver': management.runserver, 'sql': management.get_sql_create, 'sqlall': management.get_sql_all, @@ -66,6 +67,17 @@ def main(): print_error("Your action, %r, was invalid." % action, sys.argv[0]) if action in ('createsuperuser', 'init'): ACTION_MAPPING[action]() + elif action == 'inspectdb': + try: + param = args[1] + except IndexError: + parser.print_usage_and_exit() + try: + for line in ACTION_MAPPING[action](param): + print line + except NotImplementedError: + sys.stderr.write("Error: %r isn't supported for the currently selected database backend." % action) + sys.exit(1) elif action in ('startapp', 'startproject'): try: name = args[1] diff --git a/django/core/db/__init__.py b/django/core/db/__init__.py index 7dca2a25fd..4ecf33770c 100644 --- a/django/core/db/__init__.py +++ b/django/core/db/__init__.py @@ -37,5 +37,7 @@ dictfetchall = dbmod.dictfetchall get_last_insert_id = dbmod.get_last_insert_id get_date_extract_sql = dbmod.get_date_extract_sql get_date_trunc_sql = dbmod.get_date_trunc_sql +get_table_list = dbmod.get_table_list OPERATOR_MAPPING = dbmod.OPERATOR_MAPPING DATA_TYPES = dbmod.DATA_TYPES +DATA_TYPES_REVERSE = dbmod.DATA_TYPES_REVERSE diff --git a/django/core/db/backends/mysql.py b/django/core/db/backends/mysql.py index 66d31369b1..aa99516209 100644 --- a/django/core/db/backends/mysql.py +++ b/django/core/db/backends/mysql.py @@ -68,6 +68,11 @@ def get_date_trunc_sql(lookup_type, field_name): subtractions.append(" - interval (DATE_FORMAT(%s, '%%%%m')-1) month" % field_name) return "(%s - %s)" % (field_name, ''.join(subtractions)) +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute("SHOW TABLES") + return [row[0] for row in cursor.fetchall()] + OPERATOR_MAPPING = { 'exact': '=', 'iexact': 'LIKE', @@ -115,3 +120,23 @@ DATA_TYPES = { 'USStateField': 'varchar(2)', 'XMLField': 'text', } + +DATA_TYPES_REVERSE = { + FIELD_TYPE.BLOB: 'TextField', + FIELD_TYPE.CHAR: 'CharField', + FIELD_TYPE.DECIMAL: 'FloatField', + FIELD_TYPE.DATE: 'DateField', + FIELD_TYPE.DATETIME: 'DateTimeField', + FIELD_TYPE.DOUBLE: 'FloatField', + FIELD_TYPE.FLOAT: 'FloatField', + FIELD_TYPE.INT24: 'IntegerField', + FIELD_TYPE.LONG: 'IntegerField', + FIELD_TYPE.LONGLONG: 'IntegerField', + FIELD_TYPE.SHORT: 'IntegerField', + FIELD_TYPE.STRING: 'TextField', + FIELD_TYPE.TIMESTAMP: 'DateTimeField', + FIELD_TYPE.TINY_BLOB: 'TextField', + FIELD_TYPE.MEDIUM_BLOB: 'TextField', + FIELD_TYPE.LONG_BLOB: 'TextField', + FIELD_TYPE.VAR_STRING: 'CharField', +} diff --git a/django/core/db/backends/postgresql.py b/django/core/db/backends/postgresql.py index 8f82e1b23f..cd8d9a064e 100644 --- a/django/core/db/backends/postgresql.py +++ b/django/core/db/backends/postgresql.py @@ -71,6 +71,17 @@ def get_date_trunc_sql(lookup_type, field_name): # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute(""" + SELECT c.relname + FROM pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'v', '') + AND n.nspname NOT IN ('pg_catalog', 'pg_toast') + AND pg_catalog.pg_table_is_visible(c.oid)""") + return [row[0] for row in cursor.fetchall()] + # Register these custom typecasts, because Django expects dates/times to be # in Python's native (standard-library) datetime/time format, whereas psycopg # use mx.DateTime by default. @@ -129,3 +140,19 @@ DATA_TYPES = { 'USStateField': 'varchar(2)', 'XMLField': 'text', } + +# Maps type codes to Django Field types. +DATA_TYPES_REVERSE = { + 16: 'BooleanField', + 21: 'SmallIntegerField', + 23: 'IntegerField', + 25: 'TextField', + 869: 'IPAddressField', + 1043: 'CharField', + 1082: 'DateField', + 1083: 'TimeField', + 1114: 'DateTimeField', + 1184: 'DateTimeField', + 1266: 'TimeField', + 1700: 'FloatField', +} diff --git a/django/core/db/backends/sqlite3.py b/django/core/db/backends/sqlite3.py index 70dc381d0d..b719b8f8cd 100644 --- a/django/core/db/backends/sqlite3.py +++ b/django/core/db/backends/sqlite3.py @@ -61,15 +61,15 @@ class SQLiteCursorWrapper(Database.Cursor): This fixes it -- but note that if you want to use a literal "%s" in a query, you'll need to use "%%s" (which I belive is true of other wrappers as well). """ - + def execute(self, query, params=[]): query = self.convert_query(query, len(params)) return Database.Cursor.execute(self, query, params) - + def executemany(self, query, params=[]): query = self.convert_query(query, len(params[0])) return Database.Cursor.executemany(self, query, params) - + def convert_query(self, query, num_params): # XXX this seems too simple to be correct... is this right? return query % tuple("?" * num_params) @@ -78,10 +78,10 @@ class SQLiteCursorWrapper(Database.Cursor): def get_last_insert_id(cursor, table_name, pk_name): return cursor.lastrowid - + def get_date_extract_sql(lookup_type, table_name): # lookup_type is 'year', 'month', 'day' - # sqlite doesn't support extract, so we fake it with the user-defined + # sqlite doesn't support extract, so we fake it with the user-defined # function _sqlite_extract that's registered in connect(), above. return 'django_extract("%s", %s)' % (lookup_type.lower(), table_name) @@ -109,8 +109,11 @@ def _sqlite_date_trunc(lookup_type, dt): elif lookup_type == 'day': return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) +def get_table_list(cursor): + raise NotImplementedError + # Operators and fields ######################################################## - + OPERATOR_MAPPING = { 'exact': '=', 'iexact': 'LIKE', @@ -127,7 +130,7 @@ OPERATOR_MAPPING = { 'iendswith': 'LIKE', } -# SQLite doesn't actually support most of these types, but it "does the right +# SQLite doesn't actually support most of these types, but it "does the right # thing" given more verbose field definitions, so leave them as is so that # schema inspection is more useful. DATA_TYPES = { @@ -157,3 +160,5 @@ DATA_TYPES = { 'USStateField': 'varchar(2)', 'XMLField': 'text', } + +DATA_TYPES_REVERSE = {} diff --git a/django/core/management.py b/django/core/management.py index 6314ca6ac0..b0ba55275a 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -430,6 +430,32 @@ def createsuperuser(): print "User created successfully." createsuperuser.args = '' +def inspectdb(db_name): + "Generator that introspects the tables in the given database name and returns a Django model, one line at a time." + from django.core import db + from django.conf import settings + settings.DATABASE_NAME = db_name + cursor = db.db.cursor() + yield 'from django.core import meta' + yield '' + for table_name in db.get_table_list(cursor): + object_name = table_name.title().replace('_', '') + object_name = object_name.endswith('s') and object_name[:-1] or object_name + yield 'class %s(meta.Model):' % object_name + yield ' db_table = %r' % table_name + yield ' fields = (' + cursor.execute("SELECT * FROM %s LIMIT 1" % table_name) + for row in cursor.description: + field_type = db.DATA_TYPES_REVERSE[row[1]] + field_desc = 'meta.%s(%r' % (field_type, row[0]) + if field_type == 'CharField': + field_desc += ', maxlength=%s' % (row[3]) + yield ' %s),' % field_desc + yield ' )' + yield '' +inspectdb.help_doc = "Introspects the database tables in the given database and outputs a Django model module." +inspectdb.args = "[dbname]" + def runserver(port): "Starts a lightweight Web server for development." from django.core.servers.basehttp import run, WSGIServerException