aboutsummaryrefslogtreecommitdiff
path: root/src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py')
-rw-r--r--src/hydrilla/proxy/state_impl/_operations/recompute_dependencies.py461
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)