| 25 | | def revisioned_table(name, generic_table, localized_table, locale_column=None): |
|---|
| 26 | | """ |
|---|
| 27 | | Create an aliased `Select` against which a mapper can be created that |
|---|
| 28 | | selects revisions of a content type composed of independently |
|---|
| 29 | | revisioned tables, according to the `Pagoda revision model`_. |
|---|
| 30 | | |
|---|
| 31 | | `name` is the alias of the resulting `Select`. |
|---|
| 32 | | |
|---|
| 33 | | `generic_table` is a `Table` or `Selectable` containing the |
|---|
| 34 | | locale-independent columns for this content type. |
|---|
| 35 | | |
|---|
| 36 | | `localized_table` is a `Table` or `Selectable` containing the |
|---|
| 37 | | locale-dependent columns for this content type. |
|---|
| 38 | | |
|---|
| 39 | | `locale_column` is the name or `Column` from `localized_table` where |
|---|
| 40 | | the locale name (e.g. 'en_US') is stored. If None (the default), a column |
|---|
| 41 | | named 'content_locale' or 'locale' is assumed (in that order), and if not |
|---|
| 42 | | found, ValueError is raised. |
|---|
| 43 | | |
|---|
| 44 | | .. _Pagoda revision model: http://exogen.case.edu/revisions.png |
|---|
| 45 | | |
|---|
| 46 | | """ |
|---|
| 47 | | if locale_column is None: |
|---|
| 48 | | locale_columns = ['content_locale', 'locale'] |
|---|
| 49 | | for col in locale_columns: |
|---|
| 50 | | if localized_table.c.has_key(col): |
|---|
| 51 | | locale_column = localized_table.c[col] |
|---|
| 52 | | break |
|---|
| 53 | | else: |
|---|
| 54 | | raise ValueError( |
|---|
| 55 | | "Locale column not specified or detected (tried %s). " |
|---|
| 56 | | "Indicate the locale column with the `locale_column` " |
|---|
| 57 | | "argument." % ', '.join(locale_columns) |
|---|
| 58 | | ) |
|---|
| 59 | | if isinstance(locale_column, basestring): |
|---|
| 60 | | locale_column = localized_table.c[locale_column] |
|---|
| 61 | | |
|---|
| 62 | | content_table = follow_foreign_key(revision_table.c.content_id).table |
|---|
| 63 | | |
|---|
| 64 | | def revision_subselect(revisioned_table, group_by=None, join_alias=None): |
|---|
| | 25 | class RevisionSelectable(object): |
|---|
| | 26 | def __init__(self, revision_table=revision_table, |
|---|
| | 27 | content_table=content_table): |
|---|
| | 28 | self.revision_table = revision_table |
|---|
| | 29 | self.content_table = content_table |
|---|
| | 30 | self.tables = util.OrderedSet() |
|---|
| | 31 | self.id_labels = {} |
|---|
| | 32 | self.subselects = {} |
|---|
| | 33 | |
|---|
| | 34 | def revision_subselect(self, table, group_by=None, join_alias=None): |
|---|
| 108 | | |
|---|
| 109 | | node_revisions = revision_subselect(node_table) |
|---|
| 110 | | generic_revisions = revision_subselect(generic_table) |
|---|
| 111 | | localized_revisions = revision_subselect(localized_table, |
|---|
| 112 | | group_by=locale_column |
|---|
| 113 | | ) |
|---|
| 114 | | |
|---|
| 115 | | # XXX: Danger! Alert! Caution! Hey! |
|---|
| 116 | | # |
|---|
| 117 | | # The order of these tables is important! We want to ensure that the |
|---|
| 118 | | # columns from `content_table` and `revision_table` are selected before |
|---|
| 119 | | # others! Not only should their names have precedence, but SQLite's |
|---|
| 120 | | # query planner actually returns different results if `revision_table` |
|---|
| 121 | | # does not come first in the FROM clause. |
|---|
| 122 | | # |
|---|
| 123 | | select_tables = [content_table, revision_table, node_table, generic_table, |
|---|
| 124 | | localized_table] |
|---|
| 125 | | |
|---|
| 126 | | select_columns = ColumnCollection() |
|---|
| 127 | | |
|---|
| 128 | | for select_table in select_tables: |
|---|
| 129 | | # It would be nice to use `select_columns.extend` here. |
|---|
| 130 | | # However, `ColumnCollection.extend` and `ColumnCollection.add` allow |
|---|
| 131 | | # later additions to overwrite (and change the order of) existing |
|---|
| 132 | | # columns with the same name. We want existing columns to take |
|---|
| 133 | | # precedence instead. |
|---|
| 134 | | for column in select_table.c: |
|---|
| 135 | | if not select_columns.has_key(column.name): |
|---|
| 136 | | select_columns.add(column) |
|---|
| 137 | | |
|---|
| 138 | | select_columns.extend([ |
|---|
| 139 | | node_table.c.revision_id.label('node_revision_id'), |
|---|
| 140 | | generic_table.c.revision_id.label('generic_revision_id'), |
|---|
| 141 | | localized_table.c.revision_id.label('localized_revision_id') |
|---|
| 142 | | ]) |
|---|
| 143 | | |
|---|
| 144 | | content_revisions = select( |
|---|
| 145 | | select_columns, |
|---|
| 146 | | and_( |
|---|
| 147 | | content_table.c.content_id == revision_table.c.content_id, |
|---|
| 148 | | node_table.c.revision_id.in_(node_revisions), |
|---|
| 149 | | generic_table.c.revision_id.in_(generic_revisions), |
|---|
| 150 | | localized_table.c.revision_id.in_(localized_revisions), |
|---|
| 151 | | # Ensure that the `revision_id` from `revision_table` matches |
|---|
| 152 | | # the `revision_id` from one of the content tables, to prevent |
|---|
| 153 | | # a higher `revision_id` from matching this query but not being |
|---|
| 154 | | # used. |
|---|
| 155 | | or_( |
|---|
| 156 | | revision_table.c.revision_id == node_table.c.revision_id, |
|---|
| 157 | | revision_table.c.revision_id == generic_table.c.revision_id, |
|---|
| 158 | | revision_table.c.revision_id == localized_table.c.revision_id |
|---|
| 159 | | ) |
|---|
| 160 | | ), |
|---|
| 161 | | correlate=False, |
|---|
| 162 | | # Group by the `revision_id` from each content table to prevent |
|---|
| 163 | | # duplicates! |
|---|
| 164 | | group_by=[node_table.c.revision_id, generic_table.c.revision_id, |
|---|
| 165 | | localized_table.c.revision_id], |
|---|
| 166 | | order_by=[revision_table.c.revision_id, node_table.c.revision_id, |
|---|
| 167 | | generic_table.c.revision_id, localized_table.c.revision_id] |
|---|
| 168 | | ) |
|---|
| 169 | | |
|---|
| 170 | | return content_revisions.alias(name) |
|---|
| 171 | | |
|---|
| 172 | | class RevisionableMapperExtension(MapperExtension): |
|---|
| | 77 | |
|---|
| | 78 | def add_table(self, table, id_label=None, locale_column=None, |
|---|
| | 79 | join_alias=None): |
|---|
| | 80 | if id_label is None: |
|---|
| | 81 | id_label = '%s_revision_id' % table.name |
|---|
| | 82 | |
|---|
| | 83 | if isinstance(locale_column, basestring): |
|---|
| | 84 | locale_column = table.c[locale_column] |
|---|
| | 85 | |
|---|
| | 86 | subselect = self.revision_subselect(table, locale_column, join_alias) |
|---|
| | 87 | |
|---|
| | 88 | self.tables.add(table) |
|---|
| | 89 | self.id_labels[table] = id_label |
|---|
| | 90 | self.subselects[table] = subselect |
|---|
| | 91 | |
|---|
| | 92 | def get_selectable(self, alias): |
|---|
| | 93 | # XXX: Danger! Alert! Caution! Hey! |
|---|
| | 94 | # |
|---|
| | 95 | # The order of these tables is important! We want to ensure that the |
|---|
| | 96 | # columns from `content_table` and `revision_table` are selected before |
|---|
| | 97 | # others! Not only should their names have precedence, but SQLite's |
|---|
| | 98 | # query planner actually returns different results if `revision_table` |
|---|
| | 99 | # does not come first in the FROM clause. |
|---|
| | 100 | # |
|---|
| | 101 | select_tables = [self.content_table, self.revision_table] |
|---|
| | 102 | select_tables += list(self.tables) |
|---|
| | 103 | |
|---|
| | 104 | select_columns = ColumnCollection() |
|---|
| | 105 | for select_table in select_tables: |
|---|
| | 106 | # It would be nice to use `select_columns.extend` here. |
|---|
| | 107 | # However, `ColumnCollection.extend` and `ColumnCollection.add` allow |
|---|
| | 108 | # later additions to overwrite (and change the order of) existing |
|---|
| | 109 | # columns with the same name. We want existing columns to take |
|---|
| | 110 | # precedence instead. |
|---|
| | 111 | for column in select_table.c: |
|---|
| | 112 | if not select_columns.has_key(column.name): |
|---|
| | 113 | select_columns.add(column) |
|---|
| | 114 | |
|---|
| | 115 | for table in self.tables: |
|---|
| | 116 | id_column = table.c.revision_id |
|---|
| | 117 | id_label = self.id_labels[table] |
|---|
| | 118 | select_columns.add(id_column.label(id_label)) |
|---|
| | 119 | |
|---|
| | 120 | content_table = self.content_table |
|---|
| | 121 | revision_table = self.revision_table |
|---|
| | 122 | |
|---|
| | 123 | in_clauses = [table.c.revision_id.in_(self.subselects[table]) for |
|---|
| | 124 | table in self.tables] |
|---|
| | 125 | |
|---|
| | 126 | id_clauses = [revision_table.c.revision_id == table.c.revision_id for |
|---|
| | 127 | table in self.tables] |
|---|
| | 128 | |
|---|
| | 129 | selectable = select( |
|---|
| | 130 | select_columns, |
|---|
| | 131 | and_( |
|---|
| | 132 | content_table.c.content_id == revision_table.c.content_id, |
|---|
| | 133 | # Ensure that the `revision_id` from `revision_table` matches |
|---|
| | 134 | # the `revision_id` from one of the content tables, to prevent |
|---|
| | 135 | # a higher `revision_id` from matching this query but not being |
|---|
| | 136 | # used. |
|---|
| | 137 | or_(*id_clauses), |
|---|
| | 138 | *in_clauses |
|---|
| | 139 | ), |
|---|
| | 140 | correlate=False, |
|---|
| | 141 | # Group by the `revision_id` from each content table to prevent |
|---|
| | 142 | # duplicates! |
|---|
| | 143 | group_by=[table.c.revision_id for table in self.tables], |
|---|
| | 144 | order_by=[revision_table.c.revision_id] + [ |
|---|
| | 145 | table.c.revision_id for table in self.tables |
|---|
| | 146 | ] |
|---|
| | 147 | ) |
|---|
| | 148 | return selectable.alias(alias) |
|---|
| | 149 | |
|---|
| | 150 | class RevisionMapperExtension(MapperExtension): |
|---|