Index: django/core/management/commands/inspectdb.py
===================================================================
--- django/core/management/commands/inspectdb.py	(revision 6863)
+++ django/core/management/commands/inspectdb.py	(working copy)
@@ -1,5 +1,181 @@
 from django.core.management.base import NoArgsCommand, CommandError
+import re
+import keyword
 
+class Database(object):
+    """Maintains a hash of all tables in the database.
+
+      Used for correcting any duplicate model names and resolving foreign key relationships.
+    """
+    def __init__(self):
+	self.models={}
+
+    def add_model(self, model):
+	self.models[model.table_name]=model
+
+    def get_model(self, table_name):
+	return self.models[table_name]
+
+    def __str__(self):
+	s=''
+	for m in self.models.values():
+		s+=str(m)+'\n\n'
+	return s	
+
+class TableModel(object):
+    def __init__(self, database, table_name):
+	self.table_name=table_name
+	self.model_name=self.model_name_for_table(table_name)
+	self.columns=[]
+	self.relations=[]
+
+	database.add_model(self)
+	self.database=database
+
+    def add_column(self, col_name, type, extra_params, comments):
+	"""Add a column."""
+	name=self.field_name_for_column(col_name)
+        self.columns.append({'name': name, 'col_name': col_name, 'type': type, 'extra_params': extra_params, 'comments': comments})
+
+    def add_relation(self, col_name, rel_to, null):
+	"""Adds a related column.
+
+	We don't resolve the table relation at this point.
+	"""
+	name=self.field_name_for_column(col_name)
+	self.relations.append({'name': name, 'col_name': col_name, 'rel_to': rel_to, 'null': null})
+
+    def group_fields(self):
+	"""Groups fields and performs other heuristic fixups.
+
+	"""
+
+	keys=[f for f in self.columns if f['type'] == 'AutoField' or 'primary_key' in f['extra_params']]
+	ids=[f for f in self.columns if f not in keys and f['name'].endswith('_id')]
+
+	if len(keys) == 0:
+		# use heuristics to locate a candidate primary key from amongst the ids
+		tests=[self.model_name.lower()+'id']
+		if self.model_name.lower().endswith('s'):
+			tests.append(self.model_name.lower()[:-1]+'id')
+
+		for id in ids:
+			if id['name'].replace('_', '') in tests:
+                                id['comments'].append('NOTE: selected as primary_key')
+				id['extra_params']['primary_key']=True
+				keys.append(id)
+				ids.remove(id)
+				break
+
+	#resolve relations
+	rels=self.relations[:]
+	for r in rels:
+            if r['rel_to'] == self.table_name:
+                related = "'self'"
+            else:
+                related = "'%s'"%self.database.get_model(r['rel_to']).model_name
+            r['related']=related
+
+	#upgrade integer primary keys to autofields if none exists
+	if len([k for k in keys if f['type'] == 'AutoField']) == 0:
+	    for k in keys:
+	        if k['type'] == 'IntegerField':
+                    k['comments'].append('upgraded from IntegerField')
+                    k['type']='AutoField'
+		    break
+
+	# rename the AutoFields, if it exists, to 'id'
+	for k in keys:
+            if k['type'] == 'AutoField':
+                k['name']='id'	#no comment should be necessary
+                break
+
+	other=[f for f in self.columns if f not in keys and f not in ids]
+
+	return (keys, rels, ids, other)
+
+    def model_name_for_table(self, table):
+	"""Compute a Python-friendly Model class name for a given table.
+	
+	- converts names to CamelCase 
+	- removes non-alphanumberic symbols
+	"""
+	model=re.sub(r'\b([a-z])', lambda x: x.group(1).upper(), table)
+	model=re.sub(r'[^A-Za-z0-9]', '', model)
+	return model
+
+    def field_name_for_column(self, col):
+	"""Compute a Python-friendly Field name for a given column.
+	
+	- converts CamelCase names into lower_case_with_underscores
+	- removes hypens
+	- converts any contiguous sequences of other non-alphanumberic symbols to _
+	- if the field name conflicts with a Python keyword, append '_field'
+	- if the field name starts with a digit, prepend 'f_'
+	"""
+
+	field=re.sub(r'[A-Z]+', lambda x: '_'+x.group(0).lower(), col) #convert from CamelCase
+	field=field.replace('-', '') #remove hyphen
+	field=re.sub(r'[^A-Za-z0-9]+', '_', field) #replace non-alphanumberics
+
+	field=re.sub('(^_)|(_$)', '', field) #strip leading underscore
+
+	field=re.sub(r'^([0-9])', r'f_\1', field) #fix field names starting with digits
+
+	if keyword.iskeyword(field):
+		field+='_field' #avoid conflict with Python keywords
+
+	return field
+
+    def _field_as_str(self, f):
+	params = ', '.join(['%s=%r'%p for p in f['extra_params'].items()])
+
+	if f['comments']:
+	    comments=' # '+'; '.join(f['comments'])
+	else:
+	    comments=''
+
+        return '%s = models.%s(%s)%s'%(f['name'], f['type'], params, comments)
+
+    def _rel_as_str(self, f):
+	if f['null']:
+	        return '%s = models.ForeignKey(%r, db_column=%r, null=True, blank=True)'%(f['name'], f['related'], f['col_name'])
+	else:
+	        return '%s = models.ForeignKey(%r, db_column=%r)'%(f['name'], f['related'], f['col_name'])
+
+    def __str__(self):
+        s='class %s(models.Model):\n' % self.model_name
+
+	keys, rels, ids, other=self.group_fields()
+        if not keys:
+	    s+='    # Warning: this model needs a field with primary_key=True\n\n'
+
+	for f in keys:
+            s+='    %s\n'%self._field_as_str(f)
+	if keys:
+	    s+='\n'
+
+	for f in ids:
+            s+='    %s\n'%self._field_as_str(f)
+	if ids:
+	    s+='\n'
+
+        for r in rels:
+            s+='    %s\n'%self._rel_as_str(f)
+        if rels:
+	    s+='\n'
+
+        for f in other:
+            s+='    %s\n'%self._field_as_str(f)
+        if other:
+            s+='\n'
+
+        s+='    class Meta:\n'
+        s+='        db_table = %r\n' % self.table_name
+
+	return s
+
+
 class Command(NoArgsCommand):
     help = "Introspects the database tables in the given database and outputs a Django model module."
 
@@ -14,18 +190,17 @@
 
     def handle_inspection(self):
         from django.db import connection, get_introspection_module
-        import keyword
 
         introspection_module = get_introspection_module()
 
-        table2model = lambda table_name: table_name.title().replace('_', '')
+        cursor = connection.cursor()
+	database=Database()
 
-        cursor = connection.cursor()
         yield "# This is an auto-generated Django model module."
         yield "# You'll have to do the following manually to clean this up:"
-        yield "#     * Rearrange models' order"
         yield "#     * Make sure each model has one field with primary_key=True"
-        yield "# Feel free to rename the models, but don't rename db_table values or field names."
+	yield "#     * Rename models if desired (by convention, model names are singular)"
+        yield "# Note: do NOT rename db_table or db_column values."
         yield "#"
         yield "# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]'"
         yield "# into your database."
@@ -33,7 +208,8 @@
         yield 'from django.db import models'
         yield ''
         for table_name in introspection_module.get_table_list(cursor):
-            yield 'class %s(models.Model):' % table2model(table_name)
+            model=TableModel(database, table_name)
+
             try:
                 relations = introspection_module.get_relations(cursor, table_name)
             except NotImplementedError:
@@ -43,29 +219,29 @@
             except NotImplementedError:
                 indexes = {}
             for i, row in enumerate(introspection_module.get_table_description(cursor, table_name)):
-                att_name = row[0].lower()
+
+		# This is from Python DB-API spec v2 http://www.python.org/dev/peps/pep-0249/
+		column_name, type_code, display_size, internal_size, precision, scale, null_ok = row
+
                 comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
-                extra_params = {}  # Holds Field parameters such as 'db_column'.
 
-                if ' ' in att_name:
-                    extra_params['db_column'] = att_name
-                    att_name = att_name.replace(' ', '')
-                    comment_notes.append('Field renamed to remove spaces.')
-                if keyword.iskeyword(att_name):
-                    extra_params['db_column'] = att_name
-                    att_name += '_field'
-                    comment_notes.append('Field renamed because it was a Python reserved word.')
+                extra_params = {'db_column': column_name}  # Holds Field parameters
+                # Always specify the db column name - even if the field name matches the column,
+		# we expect users to fix/refactor their models. 
 
+
                 if i in relations:
-                    rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
-                    field_type = 'ForeignKey(%s' % rel_to
-                    if att_name.endswith('_id'):
-                        att_name = att_name[:-3]
-                    else:
-                        extra_params['db_column'] = att_name
+		    model.add_relation(column_name, relations[i][1], bool(null_ok))
+
+#                    rel_to = relations[i][1] == table_name and "'self'" or self.model_name_for_table(relations[i][1])
+#                    field_type = 'ForeignKey(%s' % rel_to
+#                    if att_name.endswith('_id'):
+#                        att_name = att_name[:-3]
+#                        if 'db_column' not in extra_params:
+#                        	extra_params['db_column'] = att_name
                 else:
                     try:
-                        field_type = introspection_module.DATA_TYPES_REVERSE[row[1]]
+                        field_type = introspection_module.DATA_TYPES_REVERSE[type_code]
                     except KeyError:
                         field_type = 'TextField'
                         comment_notes.append('This field type is a guess.')
@@ -77,44 +253,49 @@
                         extra_params.update(new_params)
 
                     # Add max_length for all CharFields.
-                    if field_type == 'CharField' and row[3]:
-                        extra_params['max_length'] = row[3]
+                    if field_type == 'CharField' and internal_size:
+                        extra_params['max_length'] = internal_size
 
                     if field_type == 'DecimalField':
-                        extra_params['max_digits'] = row[4]
-                        extra_params['decimal_places'] = row[5]
+                        extra_params['max_digits'] = precision
+                        extra_params['decimal_places'] = scale
 
                     # Add primary_key and unique, if necessary.
-                    column_name = extra_params.get('db_column', att_name)
                     if column_name in indexes:
                         if indexes[column_name]['primary_key']:
                             extra_params['primary_key'] = True
                         elif indexes[column_name]['unique']:
                             extra_params['unique'] = True
 
-                    field_type += '('
+#                    field_type += '('
 
                 # Don't output 'id = meta.AutoField(primary_key=True)', because
                 # that's assumed if it doesn't exist.
-                if att_name == 'id' and field_type == 'AutoField(' and extra_params == {'primary_key': True}:
-                    continue
+                #if att_name == 'id' and field_type == 'AutoField' and extra_params == {'primary_key': True}:
+                #    continue
 
                 # Add 'null' and 'blank', if the 'null_ok' flag was present in the
                 # table description.
-                if row[6]: # If it's NULL...
+                if null_ok: # If it's NULL...
                     extra_params['blank'] = True
-                    if not field_type in ('TextField(', 'CharField('):
+                    if not field_type in ('TextField', 'CharField'):
                         extra_params['null'] = True
 
-                field_desc = '%s = models.%s' % (att_name, field_type)
-                if extra_params:
-                    if not field_desc.endswith('('):
-                        field_desc += ', '
-                    field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()])
-                field_desc += ')'
-                if comment_notes:
-                    field_desc += ' # ' + ' '.join(comment_notes)
-                yield '    %s' % field_desc
-            yield '    class Meta:'
-            yield '        db_table = %r' % table_name
-            yield ''
+		model.add_column(column_name, field_type, extra_params, comment_notes)
+
+#                field_desc = '%s = models.%s' % (att_name, field_type)
+#                if extra_params:
+#                    if not field_desc.endswith('('):
+#                        field_desc += ', '
+#                    field_desc += ', '.join(['%s=%r' % (k, v) for k, v in extra_params.items()])
+#                field_desc += ')'
+#                if comment_notes:
+#                    field_desc += ' # ' + ' '.join(comment_notes)
+#                yield '    %s' % field_desc
+#
+#	     yield '    '
+#            yield '    class Meta:'
+#            yield '        db_table = %r' % table_name
+#            yield ''
+#            yield ''
+	yield str(database)
