Skip to content

Commit

Permalink
Added first stab at 'django-admin.py inspectdb', which takes a databa…
Browse files Browse the repository at this point in the history
…se 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
  • Loading branch information
adrianholovaty committed Aug 2, 2005
1 parent 1510ca1 commit d9401b7
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 7 deletions.
12 changes: 12 additions & 0 deletions django/bin/django-admin.py
Expand Up @@ -7,6 +7,7 @@
'adminindex': management.get_admin_index, 'adminindex': management.get_admin_index,
'createsuperuser': management.createsuperuser, 'createsuperuser': management.createsuperuser,
# 'dbcheck': management.database_check, # 'dbcheck': management.database_check,
'inspectdb': management.inspectdb,
'runserver': management.runserver, 'runserver': management.runserver,
'sql': management.get_sql_create, 'sql': management.get_sql_create,
'sqlall': management.get_sql_all, 'sqlall': management.get_sql_all,
Expand Down Expand Up @@ -66,6 +67,17 @@ def main():
print_error("Your action, %r, was invalid." % action, sys.argv[0]) print_error("Your action, %r, was invalid." % action, sys.argv[0])
if action in ('createsuperuser', 'init'): if action in ('createsuperuser', 'init'):
ACTION_MAPPING[action]() 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'): elif action in ('startapp', 'startproject'):
try: try:
name = args[1] name = args[1]
Expand Down
2 changes: 2 additions & 0 deletions django/core/db/__init__.py
Expand Up @@ -37,5 +37,7 @@
get_last_insert_id = dbmod.get_last_insert_id get_last_insert_id = dbmod.get_last_insert_id
get_date_extract_sql = dbmod.get_date_extract_sql get_date_extract_sql = dbmod.get_date_extract_sql
get_date_trunc_sql = dbmod.get_date_trunc_sql get_date_trunc_sql = dbmod.get_date_trunc_sql
get_table_list = dbmod.get_table_list
OPERATOR_MAPPING = dbmod.OPERATOR_MAPPING OPERATOR_MAPPING = dbmod.OPERATOR_MAPPING
DATA_TYPES = dbmod.DATA_TYPES DATA_TYPES = dbmod.DATA_TYPES
DATA_TYPES_REVERSE = dbmod.DATA_TYPES_REVERSE
25 changes: 25 additions & 0 deletions django/core/db/backends/mysql.py
Expand Up @@ -68,6 +68,11 @@ def get_date_trunc_sql(lookup_type, field_name):
subtractions.append(" - interval (DATE_FORMAT(%s, '%%%%m')-1) month" % field_name) subtractions.append(" - interval (DATE_FORMAT(%s, '%%%%m')-1) month" % field_name)
return "(%s - %s)" % (field_name, ''.join(subtractions)) 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 = { OPERATOR_MAPPING = {
'exact': '=', 'exact': '=',
'iexact': 'LIKE', 'iexact': 'LIKE',
Expand Down Expand Up @@ -115,3 +120,23 @@ def get_date_trunc_sql(lookup_type, field_name):
'USStateField': 'varchar(2)', 'USStateField': 'varchar(2)',
'XMLField': 'text', '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',
}
27 changes: 27 additions & 0 deletions django/core/db/backends/postgresql.py
Expand Up @@ -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 # http://www.postgresql.org/docs/8.0/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
return "DATE_TRUNC('%s', %s)" % (lookup_type, field_name) 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 # Register these custom typecasts, because Django expects dates/times to be
# in Python's native (standard-library) datetime/time format, whereas psycopg # in Python's native (standard-library) datetime/time format, whereas psycopg
# use mx.DateTime by default. # use mx.DateTime by default.
Expand Down Expand Up @@ -129,3 +140,19 @@ def get_date_trunc_sql(lookup_type, field_name):
'USStateField': 'varchar(2)', 'USStateField': 'varchar(2)',
'XMLField': 'text', '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',
}
19 changes: 12 additions & 7 deletions django/core/db/backends/sqlite3.py
Expand Up @@ -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, 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). you'll need to use "%%s" (which I belive is true of other wrappers as well).
""" """

def execute(self, query, params=[]): def execute(self, query, params=[]):
query = self.convert_query(query, len(params)) query = self.convert_query(query, len(params))
return Database.Cursor.execute(self, query, params) return Database.Cursor.execute(self, query, params)

def executemany(self, query, params=[]): def executemany(self, query, params=[]):
query = self.convert_query(query, len(params[0])) query = self.convert_query(query, len(params[0]))
return Database.Cursor.executemany(self, query, params) return Database.Cursor.executemany(self, query, params)

def convert_query(self, query, num_params): def convert_query(self, query, num_params):
# XXX this seems too simple to be correct... is this right? # XXX this seems too simple to be correct... is this right?
return query % tuple("?" * num_params) return query % tuple("?" * num_params)
Expand All @@ -78,10 +78,10 @@ def convert_query(self, query, num_params):


def get_last_insert_id(cursor, table_name, pk_name): def get_last_insert_id(cursor, table_name, pk_name):
return cursor.lastrowid return cursor.lastrowid

def get_date_extract_sql(lookup_type, table_name): def get_date_extract_sql(lookup_type, table_name):
# lookup_type is 'year', 'month', 'day' # 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. # function _sqlite_extract that's registered in connect(), above.
return 'django_extract("%s", %s)' % (lookup_type.lower(), table_name) return 'django_extract("%s", %s)' % (lookup_type.lower(), table_name)


Expand Down Expand Up @@ -109,8 +109,11 @@ def _sqlite_date_trunc(lookup_type, dt):
elif lookup_type == 'day': elif lookup_type == 'day':
return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)


def get_table_list(cursor):
raise NotImplementedError

# Operators and fields ######################################################## # Operators and fields ########################################################

OPERATOR_MAPPING = { OPERATOR_MAPPING = {
'exact': '=', 'exact': '=',
'iexact': 'LIKE', 'iexact': 'LIKE',
Expand All @@ -127,7 +130,7 @@ def _sqlite_date_trunc(lookup_type, dt):
'iendswith': 'LIKE', '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 # thing" given more verbose field definitions, so leave them as is so that
# schema inspection is more useful. # schema inspection is more useful.
DATA_TYPES = { DATA_TYPES = {
Expand Down Expand Up @@ -157,3 +160,5 @@ def _sqlite_date_trunc(lookup_type, dt):
'USStateField': 'varchar(2)', 'USStateField': 'varchar(2)',
'XMLField': 'text', 'XMLField': 'text',
} }

DATA_TYPES_REVERSE = {}
26 changes: 26 additions & 0 deletions django/core/management.py
Expand Up @@ -430,6 +430,32 @@ def createsuperuser():
print "User created successfully." print "User created successfully."
createsuperuser.args = '' 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): def runserver(port):
"Starts a lightweight Web server for development." "Starts a lightweight Web server for development."
from django.core.servers.basehttp import run, WSGIServerException from django.core.servers.basehttp import run, WSGIServerException
Expand Down

0 comments on commit d9401b7

Please sign in to comment.