diff options
Diffstat (limited to 'src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py')
-rw-r--r-- | src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py new file mode 100644 index 0000000..97f9de6 --- /dev/null +++ b/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py @@ -0,0 +1,461 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +# Haketilo proxy data and configuration (update of dependency tree in the db). +# +# This file is part of Hydrilla&Haketilo. +# +# Copyright (C) 2022 Wojtek Kosior +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# +# I, Wojtek Kosior, thereby promise not to sue for violation of this +# file's license. Although I request that you do not make use of this +# code in a proprietary program, I am not going to enforce this in +# court. + +""" +.... +""" + +import sqlite3 +import typing as t + +from .... import item_infos +from ... import simple_dependency_satisfying as sds +from .. import base +from .pull_missing_files import pull_missing_files, FileResolver, \ + DummyFileResolver + + +AnyInfoVar = t.TypeVar( + 'AnyInfoVar', + item_infos.ResourceInfo, + item_infos.MappingInfo +) + +def _get_infos_of_type(cursor: sqlite3.Cursor, info_type: t.Type[AnyInfoVar],) \ + -> t.Mapping[int, AnyInfoVar]: + join_mapping_statuses = 'JOIN mapping_statuses AS ms USING (item_id)' + condition = "i.type = 'M' AND ms.enabled != 'D'" + if info_type is item_infos.ResourceInfo: + join_mapping_statuses = '' + condition = "i.type = 'R'" + + cursor.execute( + f''' + SELECT + ive.item_version_id, + ive.definition, + ive.repo, + ive.repo_iteration + FROM + item_versions_extra AS ive + JOIN items AS i USING (item_id) + {join_mapping_statuses} + WHERE + {condition}; + ''' + ) + + result: dict[int, AnyInfoVar] = {} + + for item_version_id, definition, repo_name, repo_iteration \ + in cursor.fetchall(): + info = info_type.load(definition, repo_name, repo_iteration) + if info.compatible: + result[item_version_id] = info + + return result + +def _get_current_required_state( + cursor: sqlite3.Cursor, + unlocked_required_mappings: t.Sequence[int] +) -> tuple[list[sds.MappingRequirement], list[sds.ResourceVersionRequirement]]: + # For mappings explicitly enabled by the user (+ all mappings they + # recursively depend on) let's make sure that their exact same versions will + # be enabled after the change. Make exception for mappings specified by the + # caller. + # The mappings to make exception for are passed by their item_id's. First, + # we compute a set of their corresponding item_version_id's. + with base.temporary_ids_tables( + cursor = cursor, + tables = [ + ('__work_ids_0', unlocked_required_mappings), + ('__work_ids_1', []), + ('__unlocked_ids', []) + ] + ): + cursor.execute( + ''' + INSERT INTO + __work_ids_1 + SELECT + item_version_id + FROM + item_versions + WHERE + item_id IN __work_ids_0; + ''' + ) + + # Recursively update the our unlocked ids collection with all mapping + # version ids that are required by mapping versions already referenced + # there. + work_tab = '__work_ids_1' + next_tab = '__work_ids_0' + + while True: + cursor.execute(f'SELECT COUNT(*) FROM {work_tab};') + + (count,), = cursor.fetchall() + + if count == 0: + break + + cursor.execute(f'DELETE FROM {next_tab};') + + cursor.execute( + f''' + INSERT INTO + {next_tab} + SELECT + item_version_id + FROM + item_versions AS iv + JOIN items AS i + USING (item_id) + JOIN mapping_statuses AS ms + USING (item_id) + JOIN resolved_required_mappings AS rrm + ON iv.item_version_id = rrm.required_mapping_id + WHERE + ms.enabled != 'E' AND + rrm.requiring_mapping_id IN {work_tab} AND + rrm.requiring_mapping_id NOT IN __unlocked_ids; + ''' + ) + + cursor.execute( + f''' + INSERT OR IGNORE INTO + __unlocked_ids + SELECT + id + FROM + {work_tab}; + ''' + ) + + work_tab, next_tab = next_tab, work_tab + + # Describe all required mappings using requirement objects. + cursor.execute( + ''' + SELECT + ive.definition, ive.repo, ive.repo_iteration + FROM + item_versions_extra AS ive + JOIN items AS i USING (item_id) + WHERE + i.type = 'M' AND + ive.item_version_id NOT IN __unlocked_ids AND + ive.active = 'R'; + ''', + ) + + rows = cursor.fetchall() + + mapping_requirements: list[sds.MappingRequirement] = [] + + for definition, repo, iteration in rows: + mapping_info = \ + item_infos.MappingInfo.load(definition, repo, iteration) + mapping_req = sds.MappingVersionRequirement( + identifier = mapping_info.identifier, + version_info = mapping_info + ) + mapping_requirements.append(mapping_req) + + # Describe all required resources using requirement objects. + cursor.execute( + ''' + SELECT + i_m.identifier, + ive_r.definition, ive_r.repo, ive_r.repo_iteration + FROM + resolved_depended_resources AS rdd + JOIN item_versions_extra AS ive_r + ON rdd.resource_item_id = ive_r.item_version_id + JOIN payloads AS p + USING (payload_id) + JOIN item_versions AS iv_m + ON p.mapping_item_id = iv_m.item_version_id + JOIN items AS i_m + ON iv_m.item_id = i_m.item_id + WHERE + iv_m.item_version_id NOT IN __unlocked_ids AND + iv_m.active = 'R'; + ''', + ) + + rows = cursor.fetchall() + + resource_requirements: list[sds.ResourceVersionRequirement] = [] + + for mapping_identifier, definition, repo, iteration in rows: + resource_info = \ + item_infos.ResourceInfo.load(definition, repo, iteration) + resource_req = sds.ResourceVersionRequirement( + mapping_identifier = mapping_identifier, + version_info = resource_info + ) + resource_requirements.append(resource_req) + + return (mapping_requirements, resource_requirements) + +def _mark_version_installed(cursor: sqlite3.Cursor, version_id: int) -> None: + cursor.execute( + ''' + UPDATE + item_versions + SET + installed = 'I' + WHERE + item_version_id = ?; + ''', + (version_id,) + ) + +def _recompute_dependencies_no_state_update_no_pull_files( + cursor: sqlite3.Cursor, + unlocked_required_mappings: base.NoLockArg = [], +) -> None: + cursor.execute('DELETE FROM payloads;') + + ids_to_resources = _get_infos_of_type(cursor, item_infos.ResourceInfo) + ids_to_mappings = _get_infos_of_type(cursor, item_infos.MappingInfo) + + resources_to_ids = dict((info, id) for id, info in ids_to_resources.items()) + mappings_to_ids = dict((info, id) for id, info in ids_to_mappings.items()) + + if unlocked_required_mappings != 'all_mappings_unlocked': + mapping_reqs, resource_reqs = _get_current_required_state( + cursor = cursor, + unlocked_required_mappings = unlocked_required_mappings + ) + else: + mapping_reqs, resource_reqs = [], [] + + cursor.execute( + ''' + SELECT + i.identifier + FROM + mapping_statuses AS ms + JOIN items AS i USING(item_id) + WHERE + ms.enabled = 'E' AND ms.frozen = 'N'; + ''' + ) + + for mapping_identifier, in cursor.fetchall(): + mapping_reqs.append(sds.MappingRequirement(mapping_identifier)) + + cursor.execute( + ''' + SELECT + active_version_id, frozen + FROM + mapping_statuses + WHERE + enabled = 'E' AND frozen IN ('R', 'E'); + ''' + ) + + for active_version_id, frozen in cursor.fetchall(): + info = ids_to_mappings[active_version_id] + + requirement: sds.MappingRequirement + + if frozen == 'R': + requirement = sds.MappingRepoRequirement(info.identifier, info.repo) + else: + requirement = sds.MappingVersionRequirement(info.identifier, info) + + mapping_reqs.append(requirement) + + mapping_choices = sds.compute_payloads( + resources = ids_to_resources.values(), + mappings = ids_to_mappings.values(), + mapping_requirements = mapping_reqs, + resource_requirements = resource_reqs + ) + + cursor.execute( + ''' + UPDATE + mapping_statuses + SET + active_version_id = NULL + WHERE + enabled != 'E'; + ''' + ) + + cursor.execute("UPDATE item_versions SET active = 'N';") + + cursor.execute('DELETE FROM payloads;') + + cursor.execute('DELETE FROM resolved_required_mappings;') + + for choice in mapping_choices.values(): + mapping_ver_id = mappings_to_ids[choice.info] + + if choice.required: + _mark_version_installed(cursor, mapping_ver_id) + + cursor.execute( + ''' + SELECT + item_id + FROM + item_versions + WHERE + item_version_id = ?; + ''', + (mapping_ver_id,) + ) + + (mapping_item_id,), = cursor.fetchall() + + cursor.execute( + ''' + UPDATE + mapping_statuses + SET + active_version_id = ? + WHERE + item_id = ?; + ''', + (mapping_ver_id, mapping_item_id) + ) + + cursor.execute( + ''' + UPDATE + item_versions + SET + active = ? + WHERE + item_version_id = ?; + ''', + ('R' if choice.required else 'A', mapping_ver_id) + ) + + for depended_mapping_info in choice.mapping_dependencies: + cursor.execute( + ''' + INSERT INTO resolved_required_mappings( + requiring_mapping_id, + required_mapping_id + ) + VALUES (?, ?); + ''', + (mapping_ver_id, mappings_to_ids[depended_mapping_info]) + ) + + for num, (pattern, payload) in enumerate(choice.payloads.items()): + cursor.execute( + ''' + INSERT INTO payloads( + mapping_item_id, + pattern, + eval_allowed, + cors_bypass_allowed + ) + VALUES (?, ?, ?, ?); + ''', + ( + mapping_ver_id, + pattern, + payload.allows_eval, + payload.allows_cors_bypass + ) + ) + + cursor.execute( + ''' + SELECT + payload_id + FROM + payloads + WHERE + mapping_item_id = ? AND pattern = ?; + ''', + (mapping_ver_id, pattern) + ) + + (payload_id,), = cursor.fetchall() + + for res_num, resource_info in enumerate(payload.resources): + resource_ver_id = resources_to_ids[resource_info] + + if choice.required: + _mark_version_installed(cursor, resource_ver_id) + + cursor.execute( + ''' + INSERT INTO resolved_depended_resources( + payload_id, + resource_item_id, + idx + ) + VALUES(?, ?, ?); + ''', + (payload_id, resource_ver_id, res_num) + ) + + new_status = 'R' if choice.required else 'A' + + cursor.execute( + ''' + UPDATE + item_versions + SET + active = ( + CASE + WHEN active = 'R' OR ? = 'R' THEN 'R' + WHEN active = 'A' OR ? = 'A' THEN 'A' + ELSE 'N' + END + ) + WHERE + item_version_id = ?; + ''', + (new_status, new_status, resource_ver_id) + ) + + +def _recompute_dependencies_no_state_update( + cursor: sqlite3.Cursor, + unlocked_required_mappings: base.NoLockArg = [], + semirepo_file_resolver: FileResolver = DummyFileResolver() +) -> None: + _recompute_dependencies_no_state_update_no_pull_files( + cursor = cursor, + unlocked_required_mappings = unlocked_required_mappings + ) + + pull_missing_files(cursor, semirepo_file_resolver) |