Ticket #1561: inspectdb-ordering.3.diff

File inspectdb-ordering.3.diff, 6.9 KB (added by mir@…, 9 years ago)

replaces other patches (major clean-up)

  • django/core/management.py

    a b def inspectdb(db_name): 
    673673
    674674    introspection_module = get_introspection_module()
    675675
     676    def topological_sort(cursor, introspection_module, table_names):
     677        """
     678        Generator, Sorting the table names in a way that there are no forward references, if possible.
     679        Yields tuples ( table_name, forward_refs, comment_lines ) in an order that avoids forward references.
     680        Here, forward_refs is a set of table names that are referenced with a forward reference
     681        that could not be avoided since circular dependencies exist.
     682        comment_lines is a list of comment lines for this table (about forward references)
     683        """
     684        # tables_in is a list [ (table_name, [ referenced_table_name ] ], excluding self references
     685        tables_in = [(name, [val[1]
     686                             for val in introspection_module.get_relations(cursor, name).values()
     687                             if val[1] != name])
     688                     for name in table_names]
     689        # unhandled_tables contains all unhandled table_names as a set for quick lookup
     690        unhandled_tables = set(table_names)
     691        # tables_in contains all table_names that still need to be handled.
     692        # Initially, sort all tables by number of references and lexically
     693        # (to get a defined order that doesn't change from inspect to inspect)
     694        tables_in.sort(lambda (name1,refs1),(name2,refs2): cmp(len(refs1),len(refs2)) or cmp(name1,name2))
     695        # go through list until everything is handled
     696        while tables_in != []:
     697            for i, (table_name, refs) in enumerate(tables_in):
     698                # does it have references to unhandled tables?
     699                if not unhandled_tables.intersection(refs):
     700                    # no, take it
     701                    unhandled_tables.remove(table_name)
     702                    del tables_in[i]
     703                    yield (table_name, set(), [])
     704                    break;
     705            else:
     706                # There is no element without forward references in tables_in
     707                # i.e. we have circular references
     708                # strategy: use the table that is referenced most often
     709                # This will most probably break the cycle
     710                # with the least number of trouble makers.
     711                # --> First, build a cross-ref:
     712                #     referrers[table_name] is a set of all unhandled tables referencing table_name
     713                referrers = {}
     714                max_refcount = -1
     715                for table_name, refs in tables_in:
     716                    for ref in refs:
     717                        referrers.setdefault(ref,set()).add(table_name)
     718                # first entry in table_name getting maximal references
     719                for i, (table_name, refs) in enumerate(tables_in):
     720                    refcount = len(referrers.get(table_name,set()))
     721                    if refcount > max_refcount:
     722                        max_refcount, candidate = (refcount, i)
     723                # got it.
     724                bad_table_name, forward_references = tables_in[candidate]
     725                forward_references = unhandled_tables.intersection(forward_references)
     726                bad_referrers = list(unhandled_tables.intersection(referrers[bad_table_name]))
     727                bad_referrers.sort()
     728                comments = ["# cyclic references detected at this point",
     729                            "# %s contains these forward references: %s"
     730                                % (bad_table_name, ", ".join(forward_references)),
     731                            "# and is referenced by %s" % ", ".join(bad_referrers)]
     732                unhandled_tables.remove(bad_table_name)
     733                del tables_in[candidate]
     734                yield (bad_table_name, set(forward_references), comments)
     735        return
     736
    676737    def table2model(table_name):
    677738        object_name = table_name.title().replace('_', '')
    678739        return object_name.endswith('s') and object_name[:-1] or object_name
    def inspectdb(db_name): 
    681742    cursor = connection.cursor()
    682743    yield "# This is an auto-generated Django model module."
    683744    yield "# You'll have to do the following manually to clean this up:"
    684     yield "#     * Rearrange models' order"
    685745    yield "#     * Make sure each model has one field with primary_key=True"
    686746    yield "# Feel free to rename the models, but don't rename db_table values or field names."
    687747    yield "#"
    def inspectdb(db_name): 
    689749    yield "# into your database."
    690750    yield ''
    691751    yield 'from django.db import models'
    692     yield ''
    693     for table_name in introspection_module.get_table_list(cursor):
     752    yield ''
     753    for (table_name, forward_referenced_tables, comments
     754        ) in (topological_sort(cursor, introspection_module,
     755                               introspection_module.get_table_list(cursor))):
     756        for comment in comments:
     757            yield comment
    694758        yield 'class %s(models.Model):' % table2model(table_name)
    695759        try:
    696760            relations = introspection_module.get_relations(cursor, table_name)
    697761        except NotImplementedError:
    698762            relations = {}
    699         try:
     763        # relation_count :: {other_table_name: ref_count}
     764        # counts for each other table, how often it is referred by this table.
     765        # (needed for the decision whether we need related_name attributes)
     766        relation_count = {}
     767        for (field_index, (field_index_other_table, other_table)) in relations.iteritems():
     768            relation_count[other_table] = relation_count.setdefault(other_table,0) + 1
     769        try:
    700770            indexes = introspection_module.get_indexes(cursor, table_name)
    701771        except NotImplementedError:
    702772            indexes = {}
    def inspectdb(db_name): 
    710780                att_name += '_field'
    711781                comment_notes.append('Field renamed because it was a Python reserved word.')
    712782
    713             if relations.has_key(i):
     783            if (relations.has_key(i)
     784                and relations[i][1] not in forward_referenced_tables):
    714785                rel_to = relations[i][1] == table_name and "'self'" or table2model(relations[i][1])
    715                 field_type = 'ForeignKey(%s' % rel_to
     786                field_type = "ForeignKey(%s" % rel_to
    716787                if att_name.endswith('_id'):
    717788                    att_name = att_name[:-3]
    718789                else:
    719790                    extra_params['db_column'] = att_name
    720             else:
     791                if relation_count[relations[i][1]] > 1:
     792                    extra_params['related_name'] = '%s_by_%s' % (table_name, att_name)
     793            else:
     794                if relations.has_key(i):
     795                    comment_notes.append('This is a forward foreign key reference to %s.' % table2model(relations[i][1]))
    721796                try:
    722797                    field_type = introspection_module.DATA_TYPES_REVERSE[row[1]]
    723798                except KeyError:
Back to Top