| | 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 | |