USE hahn;
PRAGMA yt.PublishedErasureCodec = "lrc_12_2_2";
PRAGMA yt.OptimizeFor = "scan";

$PY_SCRIPT = @@
# -*- coding: utf8 -*-
import struct


# (List<Double>, Uint32, Uint32) -> List<Double>
def slice_list(xs, start, step):
    return list(xs)[start::step]


# (String?) -> String
def normalize_txt(text):
    if not text:
        return ""
    text = text.decode("utf-8").lower().split()
    return " ".join(sorted({w.strip() for w in text if w}))


def idx_max_of(xs, default=None):
    u"""
    ([String?]) -> Int32?
    Возвращаем индекс максимального элемента (> 0) в массиве xs
    """
    try:
        xs = [float(x.strip()) if x else None for x in xs]
    except ValueError:
        return default
    if xs:
        max_val = max(xs)
        return xs.index(max_val) if max_val > 0 else default
    else:
        return default


def parse_dsv(s, val_sep, kv_sep):
    """
    (String?, String, String) -> Dict

    >>> parse_dsv("desktop=false&isMobile=true", "&", "=")
    {'desktop': 'false', 'isMobile': 'true'}
    """
    if not s:
        return dict()

    s = s.split(val_sep)
    s = [x.split(kv_sep, 1) for x in s if x]

    return dict([x for x in s if len(x) == 2])


def decode_regular_coord(encoded_points):
    """
    (String?) -> List<Double>
    Расшифровывает регулярные координаты пользователя, сохраненные как
      бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    В июне 2016 считалось, что координата считается регулярной, если
      пользователь накопил 4 часа за 44 дня в этой точке (радиус?).

    user_regular_location - регулярные геокоординаты, формат:
        {{4 байта: Lat * 1E7 - широта точки (целое число)}{4 байта: Lon * 1E7 - долгота точки (целое число)}}

    Числа в big-endian (начиная со старшего байта)

    Example:
    >>> decode_regular_coord("H7GLshFmotYfs9i4EWpaDA==")
    [53.172933, 29.193903, 53.188012, 29.218254]

    >>> decode_regular_coord("IY4YiBooMVo=")
    [56.296052, 43.884169]

    >>> decode_regular_coord(None), decode_regular_coord("")
    []
    """

    if not encoded_points:
        return []

    try:
        decoded = encoded_points.decode("base64")
        lat_lon = map(lambda x: x/1e7, struct.unpack(">%si" % (len(decoded) / 4), decoded))
        return lat_lon
    except:
        return []


def decode_search_cats(encoded_cats):
    """decode_search_cats
    (String?)->List<Int64>
    Расшифровывает мкб-категории, сохраненные как бинарные данные 
      в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник категорий https://aw-admin.yandex-team.ru/administrator/mcb_category/index.jsp

    user_search_cats - поисковые (МКБ?) категории, формат:
        {4 байта: номер категории (целое число)}...;

    Числа в big-endian (начиная со старшего байта).

    Example:
    >>> decode_search_cats("BfXhbwX14XIF9eF2BfXhjgX14Y8F9eGVBfXhqQX14jcF9eI8BfXiQQX14mwF9eKk"))
    [100000111, 100000114, 100000118, 100000142, 100000143, 100000149, 100000169, 100000311, 100000316, 100000321, 100000364, 100000420]
    """

    if not encoded_cats:
        return []
    try:
        decoded = encoded_cats.decode("base64")
        return sorted(struct.unpack(">%si" % (len(decoded) / 4), decoded))
    except:
        return [-1, ]


def decode_adhoc_prob(encoded_adhoc_prob):
    """
    (String?)->List<Int64>
    Расшифровывает вероятностные сегменты, сохраненные как бинарные
      данные в кодировке base64.


    Note:
    Описание формата
      https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник коммерческих сегментов
      https://wiki.yandex-team.ru/crypta/crypta-da/segments/segments/
      keyword 217 - вероятностные model_id; про остальные keyword
        https://wiki.yandex-team.ru/Crypta/OutputToBB/#formatstrokivnutripachki

    Равнозначные названия - model_id, <id сегментной категории>, <id категории>.

    user_adhoc_prob - вероятностные сегменты крипты, формат:
        {первый байт: количество model_id},
        {{2 байта: model_id}{1 байт: число сегментов в model_id}}^n, где n - количество model_id
        {{1 байт: вероятность (0-100) для i-го сегмента в model_id}^m}^n, где n - количество model_id, m - количество сегментов в model_id;

    Числа в big-endian (начиная со старшего байта).

    Вероятности сегментов и сами <сегментные индексы> - внутренняя кухня Крипты.
      Для подавляющего большинства сегментов/model_id будет одно значение
      вероятности. В логи крипты и логи авапса попадают только значения
      вероятностей, которые оказались выше порога. Поэтому если для этого yuid'а
      указан какой-то сегмент и соответствующая вероятность, то мы отнесли этого
      пользователя к этому сегменту.

    В справочнике выше могут быть не все сегменты. Например, 193, 316 и 343 это
      наши внутренние маркетинговые сегменты и статистика по ним для
      медиапланирования не нужна.
    """
    if not encoded_adhoc_prob:
        return []
    try:
        decoded = encoded_adhoc_prob.decode("base64")

        # {1 байт: число категорий}
        cat_count = struct.unpack(">B", decoded[0])[0]

        # [{2 байта: номер категории/model_id/id сегментной категории}{1 байт: число сегментов в категории}]
        cats_id_cnt = struct.unpack(">" + "HB" * cat_count, decoded[1:cat_count * 3 + 1]) # 2 байта + 1 байт = 3 байта
    except:
        return [-1, ]

    return sorted(cats_id_cnt[::2])


def decode_adhoc_strict(encoded_cat_strict):
    """
    (String?) -> List<Int64>
    Расшифровывает строгие сегменты, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник коммерческих сегментов https://wiki.yandex-team.ru/crypta/crypta-da/segments/segments/
      keyword 216 - строгие model_id; про остальные keyword https://wiki.yandex-team.ru/Crypta/OutputToBB/#formatstrokivnutripachki

    Равнозначные названия - model_id, <id сегментной категории>, <id категории>.

    user_adhoc_strict - строгие сегменты крипты, формат:
        {{2 байта: model_id}{1 байт: номер сегмента}};

    Числа в big-endian (начиная со старшего байта).

    В adhocs_strict попадают эвристические ("строгие") сегменты.
    У них нет значений веротностей, а есть только их наличие или отсутствие.
    В логах крипты (0 - нет, 1 - есть, в логи попадают только сегменты с "единичками"),
    в логах авапса (1 - нет, 2 - есть, в логи соответственно должны попадать
    только сегменты с "двоечками").
    На данный момент (2016-09-13) логика для эвристических сегментов такая.
    """
    if not encoded_cat_strict:
        return []
    try:
        decoded = encoded_cat_strict.decode("base64")
        cats_id_segm_id = struct.unpack(">" + (len(decoded) / 3) * "HB", decoded)
    except:
        return [-1, ]

    return sorted(cats_id_segm_id[::2])


def decode_goals(encoded_goals):
    """
    (String?) -> List<Int64>
    Расшифровывает номера достигнутых целей, сохраненные как бинарные данные в кодировке base64.


    Note:
    Описание формата https://wiki.yandex-team.ru/Awaps/SocialDemo/#formatxranenijaprofiljavbazedannyx
    Справочник для поведенческого ретаргетинга https://wiki.yandex-team.ru/Sales/supportofcommercialservices/support/display/tematarget/

    user_goals - цели Метрики для ретаргетинга:, формат:
        { {4 байта: номер цели}{2 байта: время в часах с достижения цели до записи в лог} };

    Числа в big-endian (начиная со старшего байта).

    Example:
    >>> decode_goals("ABs8fwVzAHo9jwg8AV+oYAAO")
    [1784959, 8011151, 23046240]

    >>> decode_goals(""), decode_goals(None)
    ([], [])
    """
    if not encoded_goals:
        return []
    try:
        decoded = encoded_goals.decode("base64")
        # 6 = 4 байта + 2 байта
        goals_id_hours = struct.unpack(">" + (len(decoded) / 6) * "IH", decoded)
    except:
        return [-1, ]

    return sorted(goals_id_hours[::2])

@@;

$ACTION_ID = (
    "0", "1", "11", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", 
    "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71",
    "72", "73", "74", "75", "76", "77", "78", "79", "80", "81", "82", "83",
    "84", "85", "86", "87", "88", "89", "90", "95", "130"
);


$src_path = "statbox/awaps-log";
$dst_path = "home/vipplanners/cooked_awaps/ext";
$dst_path_mini = "home/vipplanners/cooked_awaps/mini";
$dst_path_error = "home/vipplanners/cooked_awaps/other/ext_errors";
$dst_tmp_error = "home/vipplanners/cooked_awaps/other/ext_errors_tmp";

$windows_size = 15;
$windows_offset = 2;

DEFINE SUBQUERY $last_tables($path, $limit, $offset) AS
  SELECT TableName(Path) AS TableName
  FROM FOLDER($path)
  WHERE Type = "table"
  ORDER BY TableName DESC
  LIMIT $limit OFFSET $offset;
END DEFINE;

-- недостающие таблицы для расчета
$missed_table_dates = (
SELECT AGGREGATE_LIST_DISTINCT(src.TableName)
FROM $last_tables($src_path, $windows_size, $windows_offset) AS src
LEFT ONLY JOIN $last_tables($dst_path, $windows_size, $windows_offset) AS black_list
  USING (TableName)
);


$slice = ArcPython2::slice_list(Callable<(List<Double>, Uint32, Uint32) -> List<Double>>, $PY_SCRIPT);
$idx_max_of = ArcPython2::idx_max_of(Callable<(List<String>) -> Int32?>, $PY_SCRIPT);
$normalize_txt = ArcPython2::normalize_txt(Callable<(String?) -> String>, $PY_SCRIPT);
$UA = ($ua_str) -> {RETURN Dsv::Parse($ua_str, "&")};
$decode_regular_coord = ArcPython2::decode_regular_coord(Callable<(String?) -> List<Double>>, $PY_SCRIPT);
$decode_search_cats = ArcPython2::decode_search_cats(Callable<(String?) -> List<Int64>>, $PY_SCRIPT);
-- $decode_adhoc_prob = ArcPython2::decode_adhoc_prob(Callable<(String?) -> List<Int64>>, $PY_SCRIPT);
-- $decode_adhoc_strict = ArcPython2::decode_adhoc_strict(Callable<(String?) -> List<Int64>>, $PY_SCRIPT);
-- $decode_goals = ArcPython2::decode_goals(Callable<(String?) -> List<Int64>>, $PY_SCRIPT);

$parse_eventtime = DateTime::Parse("%Y-%m-%d %H:%M:%S");
$awaps_iso_eventtime_to_mcsec = ($eventtime) -> {
  RETURN DateTime::ToMicroseconds(DateTime::MakeTzDatetime(DateTime::Update($parse_eventtime($eventtime), "Europe/Moscow" AS Timezone)));
};

DEFINE ACTION $decode_awaps($table_name) AS

  $src_full_path = $src_path || '/' || $table_name;
  $dst_full_path_ext = $dst_path || '/' || $table_name;
  $dst_full_path_mini = $dst_path_mini || '/' || $table_name;
  $dst_full_path_error = $dst_tmp_error || '/' || $table_name;
  
  INSERT INTO $dst_full_path_ext WITH TRUNCATE
  SELECT
    _awaps_row_index
  , $table_name AS table_date
  , global_request_id
  , CAST(lat IS NOT NULL OR lon IS NOT NULL OR ListLength(user_regular_location) > 0 AS Int64) AS has_coord
  , `timestamp`
  , userid
  , user_id_type
  , yandexuid
  , adid
  , placementid
  , mime_type
  , height
  , width
  , actionid
  , geo_zone
  , rtb_request_id
  , rtb_resource_id
  , rtb_host_id
  , sectionid
  , lat
  , lon
  , rtb_stlm_price
  , rtb_bid_price
  , bandwidth
  , is_mobile
  , is_tablet
  , os_family
  , device_vendor
  , $slice(user_regular_location, 0, 2) AS user_regular_location_lat
  , $slice(user_regular_location, 1, 2) AS user_regular_location_lon
  , user_search_cats
  , user_adhoc_prob
  , user_adhoc_strict
  , user_goals
  , sd_gender
  , sd_age
  , sd_income
  FROM (
      SELECT
        TableRecordIndex() - 1 AS _awaps_row_index
      , CAST(global_request_id AS UInt64)??0 AS global_request_id
      , $awaps_iso_eventtime_to_mcsec(iso_eventtime)??0 AS `timestamp`
      , CAST(userid AS UInt64)??0 AS userid
      , CAST(user_id_type AS Int32)??-1 AS user_id_type  -- 1 - yandexuid, 2 - CryptaID, 3 - AwapsID, 0 - не имеет значение.
      , CAST(yandexuid AS UInt64)??0 AS yandexuid
      , CAST(adid AS Int64)??-1 AS adid
      , CAST(placementid AS Int64)??-1 AS placementid
      , CAST(mime_type AS Int64)??-1 AS mime_type
      , CAST(height AS Int64)??-1 AS height
      , CAST(width AS Int64)??-1 AS width
      , CAST(actionid AS Int64)??-1 AS actionid
      , CAST(geo_zone AS Int64)??10000 AS geo_zone
      , CAST(rtb_request_id AS UInt64)??0 AS rtb_request_id
      , CAST(rtb_resource_id AS Int64)??-1 AS rtb_resource_id
      , CAST(rtb_host_id AS Int64)??-1 AS rtb_host_id
      , CAST(sectionid AS Int64)??-1 AS sectionid
      , IF(CAST(lon AS Double) IS NULL, CAST(NULL AS Double), CAST(lat AS Double)) AS lat
      , IF(CAST(lat AS Double) IS NULL, CAST(NULL AS Double), CAST(lon AS Double)) AS lon
      , CAST(rtb_stlm_price AS Int64)??-1 AS rtb_stlm_price  -- милликопейки
      , CAST(rtb_bid_price AS Int64)??-1 AS rtb_bid_price  -- милликопейки
      , CAST(bandwidth AS Int64)??-1 AS bandwidth
      , CAST($UA(parameterstr)["isMobile"] IN ("1", "True", "true", "t", "T") AS Int32)??-1 AS is_mobile
      , CAST($UA(parameterstr)["isTablet"] IN ("1", "True", "true", "t", "T") AS Int32)??-1 AS is_tablet
      , $normalize_txt($UA(parameterstr)["OSFamily"])??"" AS os_family
      , $normalize_txt($UA(parameterstr)["DeviceVendor"])??"" AS device_vendor
      , $decode_regular_coord(user_regular_location) AS user_regular_location
      , $decode_search_cats(user_search_cats) AS user_search_cats
      -- https://st.yandex-team.ru/AWAPS-13599
      -- , $decode_adhoc_prob(user_adhoc_prob) AS user_adhoc_prob
      , ListCreate(Int64) AS user_adhoc_prob
      -- , $decode_adhoc_strict(user_adhoc_strict) AS user_adhoc_strict
      , ListCreate(Int64) AS user_adhoc_strict
      -- , $decode_goals(user_goals) AS user_goals
      , ListCreate(Int64) AS user_goals
      , $idx_max_of(AsList(sd_gender_f??"", sd_gender_m??""))??-1 AS sd_gender
      , $idx_max_of(AsList(sd_age_0??"", sd_age_18??"", sd_age_25??"", sd_age_35??"", "", sd_age_45_55??"", sd_age_56_plus??""))??-1 AS sd_age
      , $idx_max_of(AsList(sd_income_a??"", sd_income_b??"", sd_income_c??""))??-1 AS sd_income
      FROM $src_full_path
      WHERE
            actionid IN $ACTION_ID
        AND actionid IS NOT NULL
        AND actionid != ""
        AND sectionid NOT IN ("216", "217", "") AND sectionid IS NOT NULL
  )
  ORDER BY has_coord, userid;
  COMMIT;
  
  INSERT INTO $dst_full_path_mini WITH TRUNCATE
  SELECT
    _awaps_row_index
  , table_date
  , `timestamp`
  , userid
  , user_id_type
  , yandexuid
  , adid
  , placementid
  , actionid
  , geo_zone
  , lat
  , lon
  , rtb_stlm_price
  , rtb_bid_price
  , rtb_resource_id
  , rtb_host_id
  , sectionid
  , is_mobile
  , is_tablet
  , os_family
  , device_vendor
  , sd_gender
  , sd_age
  , sd_income
  FROM $dst_full_path_ext
  WHERE actionid IN (0, 1, 52, 53, 54, 55, 56, 57, 58, 62, 63, 83, 88, 89, 90)
    AND placementid > 1
  ORDER BY
    placementid
  , geo_zone;
  COMMIT;
  
  $now_fmt = DateTime::Format("%Y-%m-%d %H:%M:%S");
  $now = EvaluateExpr($now_fmt(CurrentUtcDatetime()));
  
  INSERT INTO $dst_full_path_error WITH TRUNCATE
  SELECT
    $table_name AS cooked_table_path
  , $now AS calc_datetime
  , actionid
  , COUNT(*) AS total_rows
  , COUNT_IF(`timestamp` = 0) AS timestamp_isnull
  , COUNT_IF(userid = 0) AS userid_isnull
  , COUNT_IF(user_id_type = -1) AS user_id_type_isnull
  , COUNT_IF(yandexuid = 0) AS yandexuid_isnull
  , COUNT_IF(adid = -1) AS adid_isnull
  , COUNT_IF(placementid = -1) AS placementid_isnull
  , COUNT_IF(mime_type = -1) AS mime_type_isnull
  , COUNT_IF(height = -1) AS height_isnull
  , COUNT_IF(width = -1) AS width_isnull
  , COUNT_IF(geo_zone = 10000) AS geo_zone_isnull
  , COUNT_IF(rtb_request_id = 0) AS rtb_request_id_isnull
  , COUNT_IF(rtb_resource_id = -1) AS rtb_resource_id_isnull
  , COUNT_IF(rtb_host_id = -1) AS rtb_host_id_isnull
  , COUNT_IF(sectionid = -1) AS sectionid_isnull
  , COUNT_IF(lat IS NULL) AS lat_isnull
  , COUNT_IF(lon IS NULL) AS lon_isnull
  , COUNT_IF(rtb_stlm_price = -1) AS rtb_stlm_price_isnull
  , COUNT_IF(rtb_bid_price = -1) AS rtb_bid_price_isnull
  , COUNT_IF(bandwidth = -1) AS bandwidth_isnull
  , COUNT_IF(is_mobile = -1) AS is_mobile_isnull
  , COUNT_IF(is_tablet = -1) AS is_tablet_isnull
  , COUNT_IF(os_family = '') AS os_family_isnull
  , COUNT_IF(device_vendor = '') AS device_vendor_isnull
  , COUNT_IF(ListLength(user_regular_location_lat) = 0) AS user_regular_location_lat_isnull
  , COUNT_IF(ListLength(user_regular_location_lon) = 0) AS user_regular_location_lon_isnull
  , COUNT_IF(ListLength(user_search_cats) = 0) AS user_search_cats_isnull
  , COUNT_IF(ListHas(user_search_cats, -1)) AS user_search_cats_isfailed
  , COUNT_IF(ListLength(user_adhoc_prob) = 0) AS user_adhoc_prob_isnull
  , COUNT_IF(ListHas(user_adhoc_prob, -1)) AS user_adhoc_prob_isfailed
  , COUNT_IF(ListLength(user_adhoc_strict) = 0) AS user_adhoc_strict_isnull
  , COUNT_IF(ListHas(user_adhoc_strict, -1)) AS user_adhoc_strict_isfailed
  , COUNT_IF(ListLength(user_goals) = 0) AS user_goals_isnull
  , COUNT_IF(ListHas(user_goals, -1)) AS user_goals_isfailed
  , COUNT_IF(sd_gender = -1) AS sd_gender_isnull
  , COUNT_IF(sd_age = -1) AS sd_age_isnull
  , COUNT_IF(sd_income = -1) AS sd_income_isnull
  , MIN(`timestamp`) AS timestamp_min
  , MAX(`timestamp`) AS timestamp_max
  FROM $dst_full_path_ext
  GROUP BY actionid;
  COMMIT;
END DEFINE;


DEFINE ACTION $move_error($table_path) AS

  INSERT INTO $dst_path_error
  SELECT *
  FROM $table_path;
  COMMIT;
  DROP TABLE $table_path;

END DEFINE;


DEFINE ACTION $report_of_errors() AS

  $dt_fmt = DateTime::Format("%Y-%m-%d %H:%M:%S");
  $fmt_mcsec = ($x) -> {RETURN $dt_fmt(DateTime::FromMicroseconds($x))};

  $last_stat = (
  SELECT *
  FROM (
      SELECT MAX_BY(TableRow(), calc_datetime) AS row
      FROM $dst_path_error
      GROUP BY cooked_table_path, actionid
  ) 
  FLATTEN COLUMNS
  );
  
  -- Decoding Errors
  SELECT
    cooked_table_path
  , SUM(total_rows) AS total_rows
  , SUM(user_adhoc_prob_isfailed) AS user_adhoc_prob_isfailed
  , SUM(user_goals_isfailed) AS user_goals_isfailed
  , SUM(user_search_cats_isfailed) AS user_search_cats_isfailed
  , SUM(userid_isnull) AS userid_isnull
  , SUM(timestamp_isnull) AS timestamp_isnull
  , $fmt_mcsec(
      MIN(IF(timestamp_min > 0, timestamp_min, CAST(NULL AS Uint64)))
    ) AS timestamp_min
  , $fmt_mcsec(
      MAX(IF(timestamp_max > 0, timestamp_max, CAST(NULL AS Uint64)))
    ) AS timestamp_max
  FROM $last_stat
  GROUP BY cooked_table_path
  ORDER BY cooked_table_path DESC
  LIMIT 99;
  
  -- Targeting Errors
  SELECT
    cooked_table_path
  , SUM(total_rows) AS total_rows
  , SUM(userid_isnull) AS userid_isnull
  , SUM(user_id_type_isnull) AS user_id_type_isnull
  , SUM(yandexuid_isnull) AS yandexuid_isnull
  , SUM(placementid_isnull) AS placementid_isnull
  , SUM(geo_zone_isnull) AS geo_zone_isnull
  , SUM(rtb_request_id_isnull) AS rtb_request_id_isnull
  , SUM(rtb_resource_id_isnull) AS rtb_resource_id_isnull
  , SUM(rtb_host_id_isnull) AS rtb_host_id_isnull
  , SUM(sectionid_isnull) AS sectionid_isnull
  , SUM(rtb_stlm_price_isnull) AS rtb_stlm_price_isnull
  , SUM(rtb_bid_price_isnull) AS rtb_bid_price_isnull
  , SUM(sd_gender_isnull) AS sd_gender_isnull
  , SUM(sd_age_isnull) AS sd_age_isnull
  , SUM(sd_income_isnull) AS sd_income_isnull
  , SUM(is_mobile_isnull) AS is_mobile_isnull
  , SUM(is_tablet_isnull) AS is_tablet_isnull
  , SUM(device_vendor_isnull) AS device_vendor_isnull
  , SUM(lat_isnull) AS lat_isnull
  , SUM(lon_isnull) AS lon_isnull
  , SUM(user_regular_location_lat_isnull) AS user_regular_location_lat_isnull
  , SUM(user_regular_location_lon_isnull) AS user_regular_location_lon_isnull
  , SUM(user_search_cats_isnull) AS user_search_cats_isnull
  , SUM(user_adhoc_prob_isnull) AS user_adhoc_prob_isnull
  , SUM(user_adhoc_strict_isnull) AS user_adhoc_strict_isnull
  , SUM(user_goals_isnull) AS user_goals_isnull
  FROM $last_stat
  WHERE actionid IN (11, 130)
  GROUP BY cooked_table_path
  ORDER BY cooked_table_path DESC;
END DEFINE;


EVALUATE FOR $date IN $missed_table_dates DO $decode_awaps($date);
COMMIT;

$error_paths = (SELECT AGGREGATE_LIST(Path) FROM Folder($dst_tmp_error) WHERE Type = "table");
EVALUATE FOR $table_path IN $error_paths DO $move_error($table_path);
COMMIT;

EVALUATE IF ListLength($missed_table_dates) > 0
  DO $report_of_errors()
;
